Является ли использование Mutex безопасным для нескольких экземпляров одной и той же программы?
Я использую этот код для предотвращения одновременного запуска второго экземпляра моей программы, это безопасно?
Mutex appSingleton = new System.Threading.Mutex(false, "MyAppSingleInstnceMutx");
if (appSingleton.WaitOne(0, false)) {
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
appSingleton.Close();
} else {
MessageBox.Show("Sorry, only one instance of MyApp is allowed.");
}
Я обеспокоен тем, что если что-то выдает исключение и приложение вылетает, Mutex все равно будет удерживаться. Это правда?
10 ответов
В общем, да, это будет работать. Однако дьявол кроется в деталях.
Во-первых, вы хотите закрыть мьютекс в finally
блок. В противном случае ваш процесс может внезапно завершиться и оставить его в сигнальном состоянии, как исключение. Это сделало бы так, чтобы будущие экземпляры процесса не могли запускаться.
К сожалению, хотя, даже с finally
В блоке вы должны иметь дело с тем, что процесс будет остановлен без освобождения мьютекса. Это может произойти, например, если пользователь убивает процесс через TaskManager. В вашем коде есть условие гонки, которое позволит второму процессу получить AbandonedMutexException
в WaitOne
вызов. Вам понадобится стратегия восстановления для этого.
Я призываю вас прочитать подробности о классе Mutex. Использовать его не всегда просто.
Расширение возможностей гонки:
Может произойти следующая последовательность событий, которая вызовет выброс второго экземпляра приложения:
- Нормальный запуск процесса.
- Второй процесс запускается и получает ручку к мьютексу, но отключается до того, как
WaitOne
вызов. - Процесс № 1 резко прекращается. Мьютекс не уничтожен, потому что процесс № 2 имеет дескриптор. Вместо этого он устанавливается в заброшенное состояние.
- Второй процесс запускается снова и получает
AbanonedMutexException
,
Для этого более привычно и удобно использовать события Windows. Например
static EventWaitHandle s_event ;
bool created ;
s_event = new EventWaitHandle (false,
EventResetMode.ManualReset, "my program#startup", out created) ;
if (created) Launch () ;
else Exit () ;
Когда ваш процесс завершается или завершается, Windows закроет событие для вас и уничтожит его, если не останется открытых дескрипторов.
Добавлено: управлять сессиями, использовать Local\
а также Global\
префиксы для имени события (или мьютекса). Если ваше приложение для каждого пользователя, просто добавьте к имени события подходящее искаженное имя вошедшего в систему пользователя.
Вы можете использовать мьютекс, но сначала убедитесь, что это действительно то, что вы хотите.
Потому что "избегать нескольких экземпляров" четко не определено. Это может означать
- Избегание нескольких экземпляров, запущенных в одном сеансе пользователя, независимо от того, сколько рабочих столов в этом сеансе пользователя, но позволяет одновременно запускать несколько экземпляров для разных сеансов пользователя.
- Избегание нескольких экземпляров, запущенных на одном рабочем столе, но позволяющих запускать несколько экземпляров, пока каждый находится на отдельном рабочем столе.
- Предотвращение запуска нескольких экземпляров для одной и той же учетной записи пользователя, независимо от того, сколько рабочих столов или сеансов запущено под этой учетной записью, но одновременное выполнение нескольких экземпляров для сеансов, запущенных под другой учетной записью пользователя.
- Избегание нескольких экземпляров, запущенных на одном компьютере. Это означает, что независимо от того, сколько рабочих столов используется произвольным числом пользователей, может быть запущен не более одного экземпляра программы.
Используя мьютекс, вы в основном используете определение числа 4.
Я использую этот метод, я считаю, что это безопасно, потому что Mutex уничтожается, если долго не удерживается каким-либо приложением (и приложения закрываются, если они не могут изначально создать Mutext). Это может или не может работать одинаково в "AppDomain-процессы" (см. Ссылку внизу):
// Make sure that appMutex has the lifetime of the code to guard --
// you must keep it from being collected (and the finalizer called, which
// will release the mutex, which is not good here!).
// You can also poke the mutex later.
Mutex appMutex;
// In some startup/initialization code
bool createdNew;
appMutex = new Mutex(true, "mutexname", out createdNew);
if (!createdNew) {
// The mutex already existed - exit application.
// Windows will release the resources for the process and the
// mutex will go away when no process has it open.
// Processes are much more cleaned-up after than threads :)
} else {
// win \o/
}
Вышесказанное страдает от замечаний в других ответах / комментариях о том, что вредоносные программы могут сидеть на мьютексе. Не беспокойся здесь. Также нефиксированный мьютекс создан в "локальном" пространстве. Это, наверное, правильно здесь.
Смотрите: http://ayende.com/Blog/archive/2008/02/28/The-mysterious-life-of-mutexes.aspx - поставляется с Джоном Скитом;-)
Да, это безопасно, я бы предложил следующую схему, потому что вам нужно убедиться, что Mutex
получает всегда освобожден.
using( Mutex mutex = new Mutex( false, "mutex name" ) )
{
if( !mutex.WaitOne( 0, true ) )
{
MessageBox.Show("Unable to run multiple instances of this program.",
"Error",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
else
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
}
}
В Windows завершение процесса приводит к следующим результатам:
- Все оставшиеся потоки в процессе помечены для завершения.
- Любые ресурсы, выделенные процессом, освобождаются.
- Все объекты ядра закрыты.
- Код процесса удаляется из памяти.
- Код завершения процесса установлен.
- Объект процесса сигнализируется.
Объекты Mutex являются объектами ядра, поэтому все удерживаемые процессом закрываются, когда процесс завершается (в любом случае, в Windows).
Но обратите внимание на следующий бит из документации CreateMutex():
Если вы используете именованный мьютекс для ограничения вашего приложения одним экземпляром, злонамеренный пользователь может создать этот мьютекс, прежде чем вы это сделаете, и запретить запуск вашего приложения.
Вот фрагмент кода
public enum ApplicationSingleInstanceMode
{
CurrentUserSession,
AllSessionsOfCurrentUser,
Pc
}
public class ApplicationSingleInstancePerUser: IDisposable
{
private readonly EventWaitHandle _event;
/// <summary>
/// Shows if the current instance of ghost is the first
/// </summary>
public bool FirstInstance { get; private set; }
/// <summary>
/// Initializes
/// </summary>
/// <param name="applicationName">The application name</param>
/// <param name="mode">The single mode</param>
public ApplicationSingleInstancePerUser(string applicationName, ApplicationSingleInstanceMode mode = ApplicationSingleInstanceMode.CurrentUserSession)
{
string name;
if (mode == ApplicationSingleInstanceMode.CurrentUserSession)
name = $"Local\\{applicationName}";
else if (mode == ApplicationSingleInstanceMode.AllSessionsOfCurrentUser)
name = $"Global\\{applicationName}{Environment.UserDomainName}";
else
name = $"Global\\{applicationName}";
try
{
bool created;
_event = new EventWaitHandle(false, EventResetMode.ManualReset, name, out created);
FirstInstance = created;
}
catch
{
}
}
public void Dispose()
{
_event.Dispose();
}
}
Вот как я подошел к этому
В классе Program: 1. Получите System.Diagnostics.Process ВАШЕГО приложения, используя Process.GetCurrentProcess(). 2. Выполните пошаговую процедуру сбора открытых процессов с текущим именем вашего приложения, используя Process.GetProcessesByName(thisProcess.ProcessName). 3. Проверьте каждый process.Id против thisProcess.Id, и если экземпляр уже открыт, то как минимум 1 будет соответствовать имени, но не Id, в противном случае продолжайте открывать экземпляр
using System.Diagnostics;
.....
static void Main()
{
Process thisProcess = Process.GetCurrentProcess();
foreach(Process p in Process.GetProcessesByName(thisProcess.ProcessName))
{
if(p.Id != thisProcess.Id)
{
// Do whatever u want here to alert user to multiple instance
return;
}
}
// Continue on with opening application
Приятно было бы закончить это, представив пользователю уже открытый экземпляр, скорее всего, они не знали, что он был открыт, поэтому давайте покажем им, что это было. Для этого я использую User32.dll для трансляции сообщения в цикл обмена сообщениями Windows, пользовательское сообщение, и мое приложение прослушивает его в методе WndProc, и если оно получает это сообщение, оно представляется пользователю, Form.Show() или еще много чего
Важно помнить, что эти мьютексы \ события можно легко увидеть с помощью таких программ, как handle.exe от Microsoft sysinternals, а затем, если используется постоянное имя, кто-то злой может использовать это, чтобы предотвратить запуск вашего приложения.
Вот цитата из рекомендаций Microsoft по безопасным альтернативам:
Однако злоумышленник может создать этот мьютекс раньше вас и предотвратить запуск вашего приложения. Чтобы предотвратить эту ситуацию, создайте мьютекс со случайным именем и сохраните имя, чтобы его мог получить только авторизованный пользователь. В качестве альтернативы вы можете использовать для этой цели файл. Чтобы ограничить ваше приложение одним экземпляром на пользователя, создайте заблокированный файл в каталоге профиля пользователя.
Взято отсюда:https://docs.microsoft.com/en-us/sysinternals/downloads/handle
Если вы хотите использовать мьютексный подход, вам действительно следует использовать локальный мьютекс, чтобы ограничить этот подход только сеансом входа текущего пользователя. Также обратите внимание на другое важное предостережение в этой ссылке, касающееся надежного удаления ресурсов с помощью мьютекса.
Одно предостережение заключается в том, что подход на основе мьютекса не позволяет вам активировать первый экземпляр приложения, когда пользователь пытается запустить второй экземпляр.
Альтернативой является PInvoke для FindWindow, за которым следует SetForegroundWindow в первом случае. Другой вариант - проверить ваш процесс по имени:
Process[] processes = Process.GetProcessesByName("MyApp");
if (processes.Length != 1)
{
return;
}
Обе эти последние альтернативы имеют гипотетическое состояние гонки, при котором два экземпляра приложения могут запускаться одновременно, а затем обнаруживать друг друга. Это вряд ли произойдет на практике - на самом деле, во время тестирования я не смог этого сделать.
Другая проблема с этими двумя последними альтернативами заключается в том, что они не будут работать при использовании служб терминалов.
Используйте приложение с настройками тайм-аута и безопасности, избегая AbandonedMutexException. Я использовал свой пользовательский класс:
private class SingleAppMutexControl : IDisposable
{
private readonly Mutex _mutex;
private readonly bool _hasHandle;
public SingleAppMutexControl(string appGuid, int waitmillisecondsTimeout = 5000)
{
bool createdNew;
var allowEveryoneRule = new MutexAccessRule(new SecurityIdentifier(WellKnownSidType.WorldSid, null),
MutexRights.FullControl, AccessControlType.Allow);
var securitySettings = new MutexSecurity();
securitySettings.AddAccessRule(allowEveryoneRule);
_mutex = new Mutex(false, "Global\\" + appGuid, out createdNew, securitySettings);
_hasHandle = false;
try
{
_hasHandle = _mutex.WaitOne(waitmillisecondsTimeout, false);
if (_hasHandle == false)
throw new System.TimeoutException();
}
catch (AbandonedMutexException)
{
_hasHandle = true;
}
}
public void Dispose()
{
if (_mutex != null)
{
if (_hasHandle)
_mutex.ReleaseMutex();
_mutex.Dispose();
}
}
}
и использовать это:
private static void Main(string[] args)
{
try
{
const string appguid = "{xxxxxxxx-xxxxxxxx}";
using (new SingleAppMutexControl(appguid))
{
//run main app
Console.ReadLine();
}
}
catch (System.TimeoutException)
{
Log.Warn("Application already runned");
}
catch (Exception ex)
{
Log.Fatal(ex, "Fatal Error on running");
}
}