3, 2, 1, ... старт!

© 2005 Алексей Морозов

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

Для решения вышеизложенной проблемы и была написана программа Launcher. Она запускает переданное ей в качестве параметра приложение, предварительно копируя его (при необходимости) на локальный диск. Копирование производится только в случае, если файл на сервере имеет другой размер или дату создания. Но у старого способа прямого запуска программы был один полезный побочный эффект: с помощью мониторинга сетевых ресурсов можно было определить, кто и сколько времени использует какую часть пакета программ. При запуске локальной копии эти сведения восстановить невозможно (хотя, так как сам Launcher находится на сервере, определить список активных пользователей все же возможно, но кто что запустил и когда - это вопрос без ответа). Поэтому Launcher создает (или открывает без блокировки) в том же каталоге, где находится исходное приложение, файл-"тень", а после завершения приложения закрывает его. По сетевым сессиям на файлы-"тени" можно установить, кто, что и когда запустил.

program Launcher;

{$R *.RES}

{$WARN SYMBOL_PLATFORM OFF}

uses
  Windows,
  SysUtils,
  Classes,
  IniFiles;

procedure Error(const Msg: string);
begin
  MessageBox(0, PChar(Msg), 'Launcher Error', MB_ICONERROR or MB_OK);
  Halt(1);
end;

procedure Launch(const AppName, CmdLine: string);
  procedure CacheFile(const SrcName, DestName: string);
    function IsCacheNeeded(const SrcName, DestName: string): Boolean;
      function FileLength(const FileName: string): Int64;
      var
        FileHandle: Integer;
      begin
        Result := -1;
        FileHandle := FileOpen(FileName, fmOpenRead or fmShareDenyWrite);
        if FileHandle = -1 then
          Exit;
        try
          Result := FileSeek(FileHandle, Int64(0), 2);
        finally
          FileClose(FileHandle);
        end;
      end;
    begin
      Result := (not FileExists(DestName)) or (FileLength(SrcName)
                <> FileLength(DestName)) or
        (FileAge(SrcName) <> FileAge(DestName));
    end;
  const
    BufSize = 32768;
  var
    SrcFile,
    DestFile: Integer;
    Buffer: Pointer;
    BufLen: Integer;
  begin
    if not IsCacheNeeded(SrcName, DestName) then
      Exit;
    SrcFile := FileOpen(SrcName, fmOpenRead or fmShareDenyWrite);
    if SrcFile = -1 then
      Error('Unable to open source file "' + SrcName + '"!');
    try
      DestFile := FileCreate(DestName);
      if DestFile = -1 then
        Error('Unable to create destination file "' + DestName + '"!');
      try
        GetMem(Buffer, BufSize);
        try
          repeat
            BufLen := FileRead(SrcFile, Buffer^, BufSize);
            if FileWrite(DestFile, Buffer^, BufLen) <> BufLen then
              Error('Unable to write to destination file "' + DestName + '"!');
          until BufLen = 0;
        finally
          FreeMem(Buffer, BufSize);
        end;
        FileSetDate(DestFile, FileGetDate(SrcFile));
      finally
        FileClose(DestFile);
      end;
    finally
      FileClose(SrcFile);
    end;
  end;
  procedure LaunchApp(const AppName, CmdLine, WorkDir: string; WaitForEnd: Boolean = True);
  var
    si: TStartupInfo;
    pi: TProcessInformation;
  begin
    FillChar(si, SizeOf(si), 0);
    si.cb := SizeOf(si);
    si.dwFlags := STARTF_USESHOWWINDOW;
    si.wShowWindow := SW_SHOWNORMAL;
    if not CreateProcess(nil, PChar(Trim(AppName + ' ' + CmdLine)), nil, nil, False,
      CREATE_NEW_CONSOLE or NORMAL_PRIORITY_CLASS, nil, PChar(WorkDir), si, pi) then
      Error('Unable to execute application "' + AppName + '"!');
    CloseHandle(pi.hThread);
    if WaitForEnd then
      begin
        SetProcessWorkingSetSize(GetCurrentProcess(), DWORD(-1), DWORD(-1));
        SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_IDLE);
        WaitForSingleObject(pi.hProcess, INFINITE);
      end;
    CloseHandle(pi.hProcess);
  end;
var
  SrcPath,
  DestPath,
  WorkDir,
  DestName,
  ShadowName: string;
  PrecopyFiles: TStrings;
  UseShadow: Boolean;
  I: Integer;
begin
  PrecopyFiles := TStringList.Create;
  try
    with TIniFile.Create(ChangeFileExt(ParamStr(0), '.ini')) do
      try
        SrcPath := IncludeTrailingBackslash(ReadString('Source', 'Path', ExtractFileDir(AppName)));
        DestPath := IncludeTrailingBackslash(ReadString('Destination', 'Path', GetEnvironmentVariable('TEMP')));
        WorkDir := ExcludeTrailingBackslash(ReadString('Destination', 'WorkDir', DestPath));
        ReadSectionValues('Precopy', PrecopyFiles);
        UseShadow := ReadBool('General', 'UseShadow', True);
      finally
        Free;
      end;
    if not ForceDirectories(DestPath) then
      Error('Unable to create destination directory "' + DestPath + '"!');
    for I := 0 to PrecopyFiles.Count - 1 do
      CacheFile(ExpandUNCFileName(SrcPath + PrecopyFiles.ValueFromIndex[I]), ExpandUNCFileName(DestPath + PrecopyFiles.ValueFromIndex[I]));
  finally
    PrecopyFiles.Free;
  end;
  DestName := ExpandUNCFileName(DestPath + ExtractFileName(AppName));
  CacheFile(AppName, DestName);
  if UseShadow then
    begin
      ShadowName := AppName + '.shadow';
      if not FileExists(ShadowName) then
        begin
          I := FileCreate(ShadowName);
          if I = -1 then
            Error('Unable to create shadow file "' + ShadowName + '"!');
          FileClose(I);
          FileSetAttr(ShadowName, faHidden);
        end;
      I := FileOpen(ShadowName, fmOpenRead or fmShareDenyNone);
      if I = -1 then
        Error('Unable to open shadow file "' + ShadowName + '"!');
    end;
  try
    LaunchApp(DestName, CmdLine, WorkDir, UseShadow);
  finally
    if UseShadow then
      FileClose(I);
  end;
end;

var
  I: Integer;
  Cmd: string;
begin
  if ParamCount = 0 then
    Error('Missing parameter(s)!'#13'Usage: Launcher.exe application [cmdline]');
  Cmd := ParamStr(2);
  for I := 3 to ParamCount do
    Cmd := Cmd + ' ' + ParamStr(I);
  Launch(ExpandUNCFileName(ParamStr(1)), Cmd);
end.

Глобальные параметры Launcher'а хранятся в .ini файле в том же каталоге. Секция [General] может содержать параметр UseShadow, который определяет использование системы "теней" для мониторинга сетевых сессий. Секция [Source] может содержать параметр Path, определяющий исходный каталог запускаемого файла. Секция [Destination] может содержать параметры Path и WorkDir, которые определяют путь для копирования копии приложения и рабочий каталог для его запуска. Секция [Precopy] может содержать имена файлов, которые необходимо также скопировать для корректного запуска локальной копии программы (.ini файлы, динамические библиотеки и т.п.).

Программа создавалась в Delphi 7, поэтому может потребовать некоторых изменений исходного кода для компиляции под более старыми версиями Delphi. При желании, код может быть доработан для обеспечения дополнительной функциональности, но и в данном виде он уже около полугода помогает справляться с задачей поддержки программного пакета в сети без лишних нервов. Возможно, Launcher поможет и вам.

Исходные коды программы Launcher (3.12K).

Об авторе: Морозов Алексей Викторович (fox@europaplus.ru), MCSE, MCSD, Brainbench Delphi Professional, Brainbench C Professional

Специально для Delphi Plus

2011123456789101112
2010123456789101112
2009123456789101112
2008123456789101112
2007123456789101112
2006123456789101112
2005123456789101112
2004123456789101112
2003123456789101112
2002123456789101112
2001123456789101112
2000123456789101112
1999123456789101112

Последние статьи
Литература