Pull to refresh

Обработка повторных запусков приложения в Windows

Level of difficultyEasy
Reading time9 min
Views830

Добрый день, уважаемые читатели! В прошлой статье Механизмы взаимодействия пользователя и системы с приложением в Windows были разобраны некоторые из инструментов Windows, с помощью которых можно настроить передачу информации об открываемых файлах или ссылках прямо в ваше приложение, зарегистрированное в системе.

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

В этой статье постараюсь показать вариант решения этой проблемы, с корректной обработкой новых запусков через комбинацию использования Mutex и межпроцессного взаимодействия (IPC). В качестве примера буду использовать заглушку для эмуляции обработки протокола ASK для клиента ЛОЦМАН:PLM.
Как упоминалось в предыдущей статье, ссылка этого протокола имеет вид ask:Loodsman.URL?Action=Navigate,params=NTF8REJ.... Заглушка должна получать эту ссылку в виде параметра, а затем выводить в консоль имя базы и идентификатор объекта, который содержится в поле params ссылки.

Общая схема работы заглушки

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

  2. Передача данных в работающий экземпляр
    Если новый экземпляр пытается открыть ссылку, то до закрытия он должен передать данные уже работающему приложению. Это реализуется через именованные каналы (Named Pipes).

  3. Извлечение информации
    Из переданной ссылки приложение извлекает информацию и отображает ее в консоли для подтверждения корректной обработки или выводит информацию об ошибке.

Таким образом, приложение запускается единожды, а все последующие вызовы просто передают новые данные в уже открытый экземпляр.

Использование мютекс

Мьютекс (mutex) — это механизм синхронизации в многопоточных приложениях, обеспечивающий взаимное исключение доступа к общим ресурсам. Его задача — предотвратить ситуации, когда несколько потоков одновременно изменяют одни и те же данные, что может привести к повреждению информации. Принцип работы основан на блокировке: поток, захвативший мьютекс, получает эксклюзивный доступ к критическому участку кода, а остальные потоки ожидают освобождения мьютекса. Это гарантирует, что в каждый момент времени только один поток выполняет операции с защищаемым ресурсом, сохраняя его целостность.

Функции работы с Mutex:
// ...
var
  MutexHandle: THandle;
  AlreadyRunning: Boolean;
begin
  // Создание мьютекса
  MutexHandle := CreateMutex(nil, True, PChar('MyUniqueMutexExample'));
  // Проверка мьютекса
  AlreadyRunning := (GetLastError = ERROR_ALREADY_EXISTS);
  // Освобождtение мьютекса
  ReleaseMutex(MutexHandle);
  CloseHandle(MutexHandle);
end.

Использование Named Pipes для передачи данных между экземплярами приложения

После того как мы защитились от дублирования процессов с помощью мьютекса, нужно обеспечить передачу данных (например, URL или пути к файлу) из нового экземпляра в уже запущенный. Для этого отлично подходит механизм именованных каналов (Named Pipes).
Для реализации такого механизма:

  1. Основной экземпляр приложения создает именованный канал (\\.\pipe\ask_protocol_reader_pipe) и начинает асинхронно ожидать подключений.

  2. Новый экземпляр, обнаружив, что приложение уже запущено (через мьютекс), подключается к этому каналу и отправляет данные (например, ссылку, которую пытался открыть пользователь).

  3. Основное приложение получает данные, обрабатывает их и продолжает работу.

По этой теме есть интересная статья - Каналы передачи данных Pipe, где подробно разбираются функции работы с каналами.

Скрытие окна дубликата

При запуске второго экземпляра приложения может возникать неприятный визуальный эффект — моргание окна дубликата, которое кратковременно появляется на экране, прежде чем программа завершит свою работу или передаст управление основному экземпляру. Чтобы устранить этот эффект, можно скрыть окно дубликата.

Процедуры для управления отображением окна:
procedure ShowConsole;
var
  ConsoleHandle: THandle;
begin
  // Выделяем консоль
  AllocConsole;
  ConsoleHandle := GetStdHandle(STD_OUTPUT_HANDLE);
  SetConsoleTitle('Ask Protocol Reader');
end;

procedure HideConsole;
begin
  // Скрываем консоль
  ShowWindow(GetConsoleWindow, SW_HIDE);
end;

Извлечение данных из переданного параметра

В коде заглушки еще реализована обработка получаемых данных. В начале мы разбираем строку, в которой передаётся зашифрованный параметр params, если его нет — сразу выводим ошибку. Если параметр передан, то вытаскиваем всё, что идёт после params=. Дальше эту строку декодируем из BASE64 — получаем текст с набором данных, разделенных символом |. Берем из этих частей ID объекта и название базы. Детали по этому вопросу раскрыты в предыдущей статье.

Регистрация протокола в системе

Для работы с протоколом нужно зарегистрировать его обработку в системе. Предполагается, что заглушка будет помещена в папку c:\tmp\ask_protocol_plug\

Содержимое reg файла, для добавления записи в реестр:
Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\ask]
"URL Protocol"=""

[HKEY_CLASSES_ROOT\ask\DefaultIcon]
@="C:\\tmp\\ask_protocol_plug\\ask.exe,0"

[HKEY_CLASSES_ROOT\ask\Shell]

[HKEY_CLASSES_ROOT\ask\Shell\Open]

[HKEY_CLASSES_ROOT\ask\Shell\Open\Command]
@="\"C:\\tmp\\ask_protocol_plug\\ask.exe\" %1"

Итоговый код заглушки

Ниже приведён итоговый код заглушки с комментариями. Проект создан как консольное приложение.

Код заглушки (Ask_protocol_Reader.lpr)
program Ask_protocol_Reader;

{$mode objfpc}{$H+}
{$codePage UTF-8}
{$APPTYPE CONSOLE}
uses
  Classes, SysUtils, Windows, base64;

const
  MutexName: string = 'AskProtocolReaderAppMutex'; // Уникальное имя мьютекса
  PipeName: string = '\\.\pipe\ask_protocol_reader_pipe'; // Имя канала для передачи данных

var
  MutexHandle: THandle;
  AlreadyRunning: Boolean;
  CommandLineParams: string;
  i:integer;


procedure ShowConsole;
var
  ConsoleHandle: THandle;
begin
  // Выделяем консоль
  AllocConsole;
  ConsoleHandle := GetStdHandle(STD_OUTPUT_HANDLE);
  SetConsoleTitle('Ask Protocol Reader');
end;

procedure HideConsole;
begin
  // Скрываем консоль
  ShowWindow(GetConsoleWindow, SW_HIDE);
end;

function ExtractAndDecode(const input: string): boolean;
var
  paramsStart, paramsEnd: Integer;
  paramsValue, decodedString: string;
  elements: TStringArray;
begin
  // Находим начало и конец параметра
  paramsStart := Pos('params=', input);
  if paramsStart = 0 then
  begin
    Writeln('Ошибка: Параметр "params" не найден.');
    Exit(False);
  end;

  paramsStart := paramsStart + Length('params=');
  paramsEnd := Length(input); // Учитываем, что до конца строки

  // Извлекаем значение после params=
  paramsValue := Copy(input, paramsStart, paramsEnd - paramsStart + 1);

  // Декодируем BASE64
  decodedString := DecodeStringBase64(paramsValue);

  // Разбиваем строку на элементы
  elements := decodedString.Split(['|']);

  if Length(elements) < 3 then
  begin
    Writeln('Ошибка: Неверный формат декодированной строки.');
    Exit(False);
  end;

  // Выводим результат
  Writeln(Format('Объект id=%s из базы: %s', [elements[3], elements[1]]));

  Result := True;
end;

procedure SendParamsToRunningInstance(const Params: string);
var
  hPipe: THandle;
  dwWritten: DWORD;
begin
  // Создание именованного канала
  hPipe := CreateFile(PChar(PipeName),
                      GENERIC_WRITE,
                      0, // не разрешаем совместный доступ
                      nil,
                      OPEN_EXISTING,
                      0,
                      0);

  if hPipe <> INVALID_HANDLE_VALUE then
  begin
    WriteFile(hPipe, PChar(Params)^, Length(Params) * SizeOf(Char), dwWritten, nil);
    CloseHandle(hPipe);
  end
  else
  begin
    Writeln('Не удалось открыть именованный канал: ' + SysErrorMessage(GetLastError));
  end;
end;

procedure ReadParamsFromPipe;
var
  hPipe: THandle;
  dwRead: DWORD;
  Buffer: array[0..255] of Char;
begin
  hPipe := CreateNamedPipe(PChar(PipeName),
                            PIPE_ACCESS_INBOUND,
                            PIPE_TYPE_MESSAGE or PIPE_READMODE_MESSAGE or PIPE_WAIT,
                            1,
                            256 * SizeOf(Char),
                            256 * SizeOf(Char),
                            0,
                            nil);

  if hPipe <> INVALID_HANDLE_VALUE then
  begin
    if ConnectNamedPipe(hPipe, nil) then
    begin
      while ReadFile(hPipe, Buffer, SizeOf(Buffer), dwRead, nil) do
      begin
        // Обработка полученных параметров
        WriteLn('Получено через pipeline: ' + Buffer);
        ExtractAndDecode(Buffer);
      end;
    end;
    CloseHandle(hPipe);
  end;
end;

{$R *.res}

begin

  // Проверка наличия уже запущенного экземпляра приложения
  MutexHandle := CreateMutex(nil, True, PChar(MutexName));
  AlreadyRunning := (GetLastError = ERROR_ALREADY_EXISTS);

  // Получаем параметры командной строки
  CommandLineParams := '';
    for i:= 1 to ParamCount do
      CommandLineParams := CommandLineParams + ParamStr(i) + ' ';

  if AlreadyRunning then
  begin
    HideConsole;
    // Отправляем параметры запущенному экземпляру
    SendParamsToRunningInstance(CommandLineParams);
  end
  else
  begin
    ShowConsole;
    Writeln('Обработчик протокола ASK запущен');
    if CommandLineParams <> '' then
     begin
       Writeln('Получено через командную строку: '+ CommandLineParams);
       ExtractAndDecode(CommandLineParams);
     end;

    // Основной код приложения
    while true do
          ReadParamsFromPipe;
  end;

  // Освобождение ресурсов
  ReleaseMutex(MutexHandle);
  CloseHandle(MutexHandle);
end.

Заключение

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

Выбор подходящего инструмента зависит от архитектуры вашего приложения, объёма передаваемых данных и требований к скорости и надёжности.
Удачи в реализации ваших проектов— и до встречи в следующих статьях!

UPD: по замечаниям

Действительно можно обойтись без мьютекса, так как создание любого уникального именованного объекта (в нашем случае канала \\.\pipe\ask_protocol_reader_pipe) уже достаточно, чтобы понять — программа запущена или нет. Мьютекс в проекте был реализован на первых этапах при решении задачи исключения дублирования запуска приложения и остался уже при реализации работы с каналами.

Хотя в заглушке все работало и при повторном переходе по ссылке данные переправлялись нормально, но функцию ReadParamsFromPipe переделал в соответствии с рекомендацией. Количество подключений установил максимально возможным.

Функция HideConsole оказалась не нужна, так как проект переведён в GUI-режим ({$APPTYPE GUI}), чтобы окно консоли не появлялось автоматически. Консоль создаётся только при необходимости ShowConsole для вывода логов.

Обновленный код заглушки (Ask_protocol_Reader.lpr)
program Ask_protocol_Reader;

{$mode objfpc}{$H+}
{$codePage UTF-8}
{$APPTYPE CONSOLE}
uses
  Classes, SysUtils, Windows, base64;

const
  PipeName: string = '\\.\pipe\ask_protocol_reader_pipe'; // Имя канала для передачи данных

var
  CommandLineParams: string;
  i:Integer;


procedure ShowConsole;
var
  ConsoleHandle: THandle;
begin
  // Выделяем консоль
  AllocConsole;
  ConsoleHandle := GetStdHandle(STD_OUTPUT_HANDLE);
  SetConsoleTitle('Ask Protocol Reader');
end;

function ExtractAndDecode(const input: string): boolean;
var
  paramsStart, paramsEnd: Integer;
  paramsValue, decodedString: string;
  elements: TStringArray;
begin
  // Находим начало и конец параметра
  paramsStart := Pos('params=', input);
  if paramsStart = 0 then
  begin
    Writeln('Ошибка: Параметр "params" не найден.');
    Exit(False);
  end;

  paramsStart := paramsStart + Length('params=');
  paramsEnd := Length(input); // Учитываем, что до конца строки

  // Извлекаем значение после params=
  paramsValue := Copy(input, paramsStart, paramsEnd - paramsStart + 1);

  // Декодируем BASE64
  decodedString := DecodeStringBase64(paramsValue);

  // Разбиваем строку на элементы
  elements := decodedString.Split(['|']);

  if Length(elements) < 3 then
  begin
    Writeln('Ошибка: Неверный формат декодированной строки.');
    Exit(False);
  end;

  // Выводим результат
  Writeln(Format('Объект id=%s из базы: %s', [elements[3], elements[1]]));

  Result := True;
end;

procedure SendParamsToRunningInstance(const Params: string);
var
  hPipe: THandle;
  dwWritten: DWORD;
begin
  // Создание именованного канала
  hPipe := CreateFile(PChar(PipeName),
                      GENERIC_WRITE,
                      0, // не разрешаем совместный доступ
                      nil,
                      OPEN_EXISTING,
                      0,
                      0);

  if hPipe <> INVALID_HANDLE_VALUE then
  begin
    WriteFile(hPipe, PChar(Params)^, Length(Params) * SizeOf(Char), dwWritten, nil);
    CloseHandle(hPipe);
  end
  else
  begin
    Writeln('Не удалось подключиться к запущенному экземпляру: ' + SysErrorMessage(GetLastError));
  end;
end;

procedure ReadParamsFromPipe;
var
  hPipe: THandle;
  dwRead: DWORD;
  Buffer: array[0..255] of Char;
begin
  hPipe := CreateNamedPipe(PChar(PipeName),
                            PIPE_ACCESS_INBOUND,
                            PIPE_TYPE_MESSAGE or PIPE_READMODE_MESSAGE or PIPE_WAIT,
                            255,// PIPE_UNLIMITED_INSTANCES
                            256 * SizeOf(Char),
                            256 * SizeOf(Char),
                            0,
                            nil);

  if hPipe = INVALID_HANDLE_VALUE then
  begin
    Writeln('Ошибка создания канала: ' + SysErrorMessage(GetLastError));
    Exit;
  end;

  while True do
  begin
    if ConnectNamedPipe(hPipe, nil) or (GetLastError = ERROR_PIPE_CONNECTED) then
    begin
      FillChar(Buffer, SizeOf(Buffer), 0);
      if ReadFile(hPipe, Buffer, SizeOf(Buffer), dwRead, nil) and (dwRead > 0) then
      begin

        Writeln('Получено через pipeline: ' + Buffer);
        ExtractAndDecode(Buffer);
      end;
    end;
    DisconnectNamedPipe(hPipe);
  end;

  CloseHandle(hPipe);
end;

begin
  // Собираем параметры
  CommandLineParams := '';
  for i := 1 to ParamCount do
    CommandLineParams := CommandLineParams + ParamStr(i) + ' ';

  // Пробуем подключиться к уже работающему экземпляру
  if WaitNamedPipe(PChar(PipeName), 0) then
  begin
    SendParamsToRunningInstance(CommandLineParams);
    Exit;
  end;

  // Если не удалось подключиться — запускаем основной экземпляр
  ShowConsole;
  Writeln('Обработчик протокола ASK запущен');

  if CommandLineParams <> '' then
  begin
    Writeln('Получено через командную строку: ' + CommandLineParams);
    ExtractAndDecode(CommandLineParams);
  end;

  ReadParamsFromPipe;
end.

Tags:
Hubs:
+4
Comments8

Articles