Добрый день, уважаемые читатели! В прошлой статье Механизмы взаимодействия пользователя и системы с приложением в 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.

Заключение

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

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

Комментарии (5)


  1. mayorovp
    05.06.2025 09:13

    Ну и каким таким образом скрытие консольного окна через HideConsole поможет вам избавиться от мерцания этого самого окна?

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

    Почему ReadParamsFromPipe ожидает подключения клиента только 1 раз? Почему вы делаете в цикле CreateNamedPipe, а не ConnectNamedPipe?


  1. oleg_km
    05.06.2025 09:13

    Ну потому что программируют как умеют...


  1. Alex_v99
    05.06.2025 09:13

    Интересно, а мьютекс освободится/разрушится если первый экземпляр программы упадет или будет убит извне?


    1. mayorovp
      05.06.2025 09:13

      Разумеется, все объекты ядра система "прибирает". Иначе бы любой "прибитый" процесс оставлял после себя тонны мусора.


    1. oleg_km
      05.06.2025 09:13

      В этом основное отличие именованных объектов ядра от допустим файлов. Как только релизится все экземпляры или завершаются процессы, создавший или открывший определенный объект, диспетчер объектов прибивает данный объект. Собственно сам по себе мютекс вообще не при делах. Эффект заключается просто в создании любого именованного объекта, просто при повторном создании система скажет, что объект с таким именем уже создан. Собственно хватило бы проверки создания самого именованного канала '\\.\pipe\ask_protocol_reader_pipe', создание мутанта в данном приложении совершенно лишнее, как указали выше