Сайт разработчика Александр Климова

/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000

Запуск одного экземпляра приложения

Иногда перед разработчиком возникает вопрос: как сделать так, чтобы в любой момент у пользователя могло работать не более одного экземпляра приложения. Кроме того, нужно, чтобы при попытке запустить еще один экземпляр этого приложения текущий экземпляр уведомлялся об этом.

Рассмотрим, как можно обеспечить поддержку приложений, запускаемых только в одном экземпляре. Это можно реализовать разными способами. Принцип решения задачи основан на использовании некоего общего ресурса, создаваемого и блокируемого приложением. Если второй экземпляр приложения не может этого сделать, ему известно, что один экземпляр уже существует и работу следует завершить.

Чаще всего общим ресурсом, используемым при создании управляемых приложений, запускаемых только в одном экземпляре, служит мьютекс. Мьютекс - это синхронизирующий примитив, поддерживаемый операционными системами, которые допускают взаимодействие между потоками или процессами. Обычно мьютексы применяют для защиты общих ресурсов, чтобы обеспечить монопольный доступ к ним.

Когда один поток захватывает мьютекс, другие потоки не могут захватить его, пока поток , владеющий мьютексом, не освободит его. Поэтому на основе мьютексов можно реализовать приложения, запускаемые только в одном экземпляре. При запуске приложение пытается создать и захватить мьютекс с определенным именем. Если это удалось, данный экземпляр приложения - первый. Нет - значит, есть другой экземпляр приложения, который уже создал и захватил мьютекс, поэтому новый экземпляр должен завершить работу.

В .NET Framework мьютексы представлены классом System.Threading.Mutex. Он наследуется от WaitHandle и является тонкой оболочкой объекта ядра мьютекс, который создается функцией Windows API CreateMutex и открывается WIN-функцией OpenMutex.

Для начала вы должны придумать мьютексу какое-нибудь имя. При наличии имени создавать или захватывать один и тот же экземпляр мьютекса могут несколько экземпляров приложения, поэтому важно заранее выбрать уникальное имя. Типичными примерами имен, применяемых для этой цели, могут служить имя входной сборки (доступно через [Assembly].GetEntryAssemble().FullName), GUID библиотеки типов входной сборки (доступен через Marshal.GetTypeLibGuidForAssembly) или заранее определенный GUID, уникально идентифицирующий приложение. Можно также автоматически генерировать имя в период выполнения, но тогда надо разработать такой алгоритм генерации имен, чтобы для всех экземпляров одного приложения создавалось одно имя. После создания имени выполняется конструктор с тремя аргументами, создающий мьютекс.

Первый параметр конструктора - логическое значение, указывающее, надо ли приложению сразу же получить созданный мьютекс, а третий - логическое значение, передаваемое по ссылке после выполнения конструктора и указывающее, был ли получен мьютекс. Второй параметр - имя мьютекса. После создания мьютекса можно проверить параметр grantedOwnership, чтобы определить, удалось ли захватить мьютекс экземпляру приложения. Если да, значит, это первый экземпляр приложения и приложение должно выполняться как обычно. А если нет, существует другой экземпляр приложения, и новый экземпляр должен завершить свою работу. Однако следует отметить, что при таком подходе возможна атака типа Отказ в обслуживании (denial of service, Dos). Обычно, когда говорят о DoS-атаках, в голову приходят веб-приложения. Но DoS-атака - это фактически все, что мешает пользоваться сервисами или услугами. В данном случае злоумышленник может узнать имя мьютекса, используемого для поддержки запуска приложения в одном экземпляре, и первым захватить этот мьютекс. Тогда все экземпляры вашего приложения будут считать, что они не первые, и закрываться. Просто имейте это в виду.

Запрет на запуск второй копии программы. Первый способ

Многие программы запускаются в одном экземпляре: Outlook, ICQ и т.д. Если вам нужна подобная функциональность, то воспользуйтесь классом Mutex. Если при старте программы мьютекс уже существует, значит программа уже запущена. Для удобства выводим соответствующее сообщение. Этот код можно поместить в Program.cs


bool onlyInstance;

Mutex mtx = new Mutex(true, "AppName", out onlyInstance); // используйте имя вашего приложения

// Если другие процессы не владеют мьютексом, то
// приложение запущено в единственном экземпляре
if (onlyInstance)
{
   Application.Run(new Form1());
}
else
{
   MessageBox.Show(
      "Приложение уже запущено",
      "Сообщение",
      MessageBoxButtons.OK, MessageBoxIcon.Stop);
}

Второй способ. Использование возможностей Visual Basic

Пока описание поддержки приложений, запускаемых только в одном экземпляре, выглядит довольно просто, но создать надежную реализацию не так-то легко. Возникает множество проблем, связанных с защитой взаимодействия между экземплярами, уменьшением риска упомянутой DoS-атаки (для чего приходится активно использовать списки управления доступом), правильным выбором имени описателя объекта ожидания (wait handle) и т.д.

В Visual Basic для создания приложения Windows Forms, запускаемого только в одном экземпляре, нужно лишь отметить флажок на странице свойств приложения. И затем можно обрабатывать событие Me.StartupNextInstance в файле ApplicationEvent.vb.

Объект StartupNextInstanceEventArgs, передаваемый обработчику этого события, содержит аргументы командной строки другого экземпляра приложения, и вы можете обрабатывать их так, как считаете нужным. Аргументы командной строки принимаются в фоновом режиме, но Visual Basic выполняет маршаллинг вызовов этого события в GUI-поток. На внутреннем уровне Visual Basic применяет методику, аналогичную описанной выше, но гораздо более надежную и безопасную.

Вместо мьютекса Visual Basic использует EventWaitHandle, но действует аналогичным образом и преследует ту же цель (EventWaitHandle - новый базовый класс, введенный в .NET Framework 2.0). Имя этого описателя объекта ожидания формируется на основе GUID библиотеки типов входной сборки и версии сборки. Если экземпляр описателя объекта ожидания не существует, Visual Basic создает сервер удаленного взаимодействия и безопасный канал для приема аргументов командной строки от других экземпляров приложения. При получении аргументов командной строки Visual Basic генерирует событие StartupNextInstance, о котором уже говорилось выше. Но если EventWaitHandle уже есть, Visual Basic не создает сервер удаленного взаимодействия, а соединяется с существующим сервером и передает ему аргументы командной строки нового экземпляра. Чтобы можно было обнаружить URI сервера удаленного взаимодействия, первый экземпляр записывает URI в проецируемый на память файл с известным именем (генерируемым аналогично имени описателя); другие экземпляры могут получить URI из этого файла и корректно соединиться с сервером удаленного взаимодействия.

Эта функциональность для поддержи запуска приложения только в одном экземпляре может без проблем использоваться не только на Visual Basic, но и на C#.

Рассмотрим пример на C#. Вся описанная выше функциональность доступна через класс WindowsFormsApplicationBase, являющий частью пространства имен Microsoft.VisualBasic.ApplicationServices и производного от него класса My.Application. Конструктор этого класса присваивает значения различным защищенным свойствам базового класса, в том числе и свойству IsSingleInstance

Кроме того, у класса WindowsFormsApplicationBase есть открытое свойство MainForm, задающее экземпляр формы, который загружается и показывается при запуске, и открытое событие StartupNextInstance. Воспользовавшись всем этим, можно написать свой класс.

Класс SingleInstanceApplication на C#

public class SingleInstanceApplication : WindowsFormsApplicationBase
{
    private SingleInstanceApplication()
    { 
        base.IsSingleInstance = true; 
    }
    
    public static void Run(Form f, 
        StartupNextInstanceEventHandler startupHandler)
    {
        SingleInstanceApplication app = new SingleInstanceApplication();
        app.MainForm = f;
        app.StartupNextInstance += startupHandler;
        app.Run(Environment.GetCommandLineArgs());
    }
}

Тогда в методе Main приложения можно выполнить не стандартный код вывода основной формы

Стандартный код

static class Program
{
    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        Application.Run(new Form1());
    }
}

а следующий код

Код запуска приложения только в одном экземпляре

static class Program
{
    [STAThread]
    static void Main()
    { 
        Application.EnableVisualStyles();
        SingleInstanceApplication.Run(new Form1(), 
            StartupNextInstanceHandler);
    }

    static void StartupNextInstanceHandler(
        object sender, StartupNextInstanceEventArgs e)
    {
        // что-то делаем с e.CommandLine...
    }
}

В итоге вы получите надежное приложение Windows Forms, написанное на языке C# и запускаемое только в одном экземпляре. Не забудьте добавить ссылку на Microsoft.VisualBasic.dll и импортировать соответствующее пространство имен.

Источник: MSDN Magazine September 2005 (ноябрь 2005 в русском издании)

Перевод: Василий Котов

Реклама