274 lines
12 KiB
Plaintext
274 lines
12 KiB
Plaintext
; Установщик «System Helper» для Windows x64.
|
||
; Сборка: ISCC.exe /DAppVersion=0.1.0 installer\setup.iss
|
||
; Результат кладётся в dist\syshelper-<версия>-setup.exe
|
||
|
||
#ifndef AppVersion
|
||
#define AppVersion "0.0.0-dev"
|
||
#endif
|
||
|
||
#ifndef ExeSource
|
||
#define ExeSource "..\target\release\syshelper.exe"
|
||
#endif
|
||
|
||
#define AppName "System Helper"
|
||
#define AppExeName "syshelper.exe"
|
||
#define AppUrl "http://127.0.0.1:8787"
|
||
; Имя службы — должно совпадать с SERVICE_NAME в src/service.rs.
|
||
#define ServiceName "SysHelper"
|
||
; Каталог данных — должен совпадать с data_dir() в src/main.rs.
|
||
#define DataDir "{commonappdata}\SysHelper"
|
||
|
||
[Setup]
|
||
; AppId определяет программу при обновлении и удалении — не менять между версиями.
|
||
AppId={{B3F1C0A2-7E5D-4A19-9C3B-2D8E6F4A1B57}
|
||
AppName={#AppName}
|
||
AppVersion={#AppVersion}
|
||
AppVerName={#AppName} {#AppVersion}
|
||
DefaultDirName={autopf}\SysHelper
|
||
DefaultGroupName={#AppName}
|
||
DisableProgramGroupPage=yes
|
||
UninstallDisplayName={#AppName}
|
||
UninstallDisplayIcon={app}\{#AppExeName}
|
||
|
||
OutputDir=..\dist
|
||
OutputBaseFilename=syshelper-{#AppVersion}-setup
|
||
Compression=lzma2/max
|
||
SolidCompression=yes
|
||
WizardStyle=modern
|
||
|
||
; Ставим в 64-битный Program Files; программа собрана только под x64.
|
||
ArchitecturesAllowed=x64compatible
|
||
ArchitecturesInstallIn64BitMode=x64compatible
|
||
|
||
; Регистрация службы и запись в Program Files требуют администратора.
|
||
PrivilegesRequired=admin
|
||
; Страховка: службу мы останавливаем сами, но если файл всё же занят —
|
||
; пусть Inno закроет процесс, а не просит перезагрузку.
|
||
CloseApplications=yes
|
||
RestartApplications=no
|
||
|
||
[Languages]
|
||
Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl"
|
||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||
|
||
[Tasks]
|
||
; Автозапуск больше не задача: служба и так стартует вместе с системой.
|
||
Name: "desktopicon"; \
|
||
Description: "{cm:CreateDesktopIcon}"; \
|
||
GroupDescription: "Дополнительно:"; \
|
||
Flags: unchecked
|
||
|
||
[Dirs]
|
||
; Каталог данных службы: правила, журнал, хеш пароля, скачанные обновления.
|
||
; Права закрываем через icacls ниже — унаследованные от ProgramData позволяют
|
||
; обычному пользователю создавать здесь файлы. Он не должен ни читать хеш
|
||
; пароля, ни править правила, ни подменять установщик обновления.
|
||
Name: "{#DataDir}"
|
||
|
||
[Files]
|
||
Source: "{#ExeSource}"; DestDir: "{app}"; Flags: ignoreversion
|
||
|
||
[Icons]
|
||
; Ярлыки ведут на панель: своего окна у программы нет, работает она службой.
|
||
Name: "{group}\{#AppName}"; Filename: "{#AppUrl}"
|
||
Name: "{group}\{cm:UninstallProgram,{#AppName}}"; Filename: "{uninstallexe}"
|
||
Name: "{autodesktop}\{#AppName}"; Filename: "{#AppUrl}"; Tasks: desktopicon
|
||
|
||
[Run]
|
||
; Права на каталог данных выставляются в [Code] до записи пароля и старта
|
||
; службы — здесь было бы уже поздно: [Run] выполняется последним.
|
||
Filename: "{#AppUrl}"; \
|
||
Description: "Открыть панель"; \
|
||
Flags: shellexec nowait postinstall skipifsilent
|
||
|
||
[UninstallDelete]
|
||
; Правила, журнал и скачанный установщик сами не удалятся.
|
||
Type: filesandordirs; Name: "{#DataDir}"
|
||
|
||
[Code]
|
||
function ServiceExists(): Boolean;
|
||
var
|
||
Code: Integer;
|
||
begin
|
||
// sc query для несуществующей службы возвращает 1060
|
||
Result := Exec(ExpandConstant('{sys}\sc.exe'), 'query {#ServiceName}', '',
|
||
SW_HIDE, ewWaitUntilTerminated, Code) and (Code = 0);
|
||
end;
|
||
|
||
function RunSc(const Params: String): Boolean;
|
||
var
|
||
Code: Integer;
|
||
begin
|
||
Result := Exec(ExpandConstant('{sys}\sc.exe'), Params, '',
|
||
SW_HIDE, ewWaitUntilTerminated, Code) and (Code = 0);
|
||
end;
|
||
|
||
// Останавливаем через net stop, а не sc stop: net ждёт фактической остановки,
|
||
// тогда как sc возвращает управление сразу, оставив службу в STOP_PENDING.
|
||
// Если не дождаться, exe останется занят и обновление не встанет.
|
||
procedure StopService();
|
||
var
|
||
Code: Integer;
|
||
begin
|
||
if not ServiceExists() then
|
||
Exit;
|
||
|
||
Exec(ExpandConstant('{sys}\net.exe'), 'stop {#ServiceName}', '',
|
||
SW_HIDE, ewWaitUntilTerminated, Code);
|
||
// даём файловой системе отпустить exe
|
||
Sleep(1000);
|
||
end;
|
||
|
||
// Страница пароля. Показываем только при первой установке: при обновлении
|
||
// пароль уже задан, а тихая установка (автообновление) проходит без мастера.
|
||
var
|
||
PasswordPage: TInputQueryWizardPage;
|
||
|
||
function AuthConfigured(): Boolean;
|
||
begin
|
||
Result := FileExists(ExpandConstant('{#DataDir}\auth.hash'));
|
||
end;
|
||
|
||
procedure InitializeWizard();
|
||
begin
|
||
PasswordPage := CreateInputQueryPage(wpSelectTasks,
|
||
'Пароль панели',
|
||
'Защитите панель управления от того, кого она ограничивает.',
|
||
'Панель открывается в браузере по адресу ' + '{#AppUrl}' + '. Без пароля ' +
|
||
'её сможет открыть любой пользователь этого компьютера и отключить правила.');
|
||
PasswordPage.Add('Пароль:', True);
|
||
end;
|
||
|
||
function ShouldSkipPage(PageID: Integer): Boolean;
|
||
begin
|
||
Result := (PasswordPage <> nil) and (PageID = PasswordPage.ID) and AuthConfigured();
|
||
end;
|
||
|
||
function NextButtonClick(CurPageID: Integer): Boolean;
|
||
begin
|
||
Result := True;
|
||
if (PasswordPage <> nil) and (CurPageID = PasswordPage.ID)
|
||
and (Trim(PasswordPage.Values[0]) = '') then
|
||
begin
|
||
MsgBox('Задайте пароль — иначе панель останется открытой для всех.',
|
||
mbError, MB_OK);
|
||
Result := False;
|
||
end;
|
||
end;
|
||
|
||
function PrepareToInstall(var NeedsRestart: Boolean): String;
|
||
begin
|
||
StopService();
|
||
Result := '';
|
||
end;
|
||
|
||
// Пароль передаём файлом, а не аргументом командной строки: аргумент был бы
|
||
// виден в списке процессов. Файл пишем в UTF-8 явно — Rust читает его так же,
|
||
// иначе кириллический пароль исказился бы и вход перестал работать.
|
||
procedure SetPassword();
|
||
var
|
||
PwFile: String;
|
||
Code: Integer;
|
||
begin
|
||
// При тихой установке (автообновление) мастера нет и страницы тоже —
|
||
// обращение к ней уронило бы установщик посреди обновления.
|
||
if (PasswordPage = nil) or AuthConfigured() or (Trim(PasswordPage.Values[0]) = '') then
|
||
Exit;
|
||
|
||
PwFile := ExpandConstant('{tmp}\pw.txt');
|
||
if not SaveStringsToUTF8File(PwFile, [PasswordPage.Values[0]], False) then
|
||
Exit;
|
||
|
||
// exe сам удалит файл после чтения
|
||
Exec(ExpandConstant('{app}\{#AppExeName}'),
|
||
'--set-password-file "' + PwFile + '"', '',
|
||
SW_HIDE, ewWaitUntilTerminated, Code);
|
||
end;
|
||
|
||
// Запускаем через net start, а не sc start, по той же причине, что и остановку:
|
||
// net дожидается фактического запуска и возвращает ошибку, если он не удался.
|
||
// sc отдаёт управление сразу, и сломанная установка выглядела бы успешной.
|
||
function StartService(): Boolean;
|
||
var
|
||
Code: Integer;
|
||
begin
|
||
Result := Exec(ExpandConstant('{sys}\net.exe'), 'start {#ServiceName}', '',
|
||
SW_HIDE, ewWaitUntilTerminated, Code) and (Code = 0);
|
||
end;
|
||
|
||
function InstallService(): Boolean;
|
||
var
|
||
Bin: String;
|
||
begin
|
||
// Путь к exe содержит пробелы (Program Files), поэтому его кавычки должны
|
||
// попасть внутрь значения binPath — sc.exe ждёт их экранированными, иначе
|
||
// служба зарегистрируется с обрезанным путём и не запустится.
|
||
Bin := 'binPath= "\"' + ExpandConstant('{app}\{#AppExeName}') + '\" --service"';
|
||
|
||
if ServiceExists() then
|
||
// пути могли измениться при переустановке в другой каталог
|
||
RunSc('config {#ServiceName} ' + Bin + ' start= delayed-auto')
|
||
else
|
||
RunSc('create {#ServiceName} ' + Bin + ' start= delayed-auto ' +
|
||
'DisplayName= "{#AppName}"');
|
||
|
||
RunSc('description {#ServiceName} "Родительский контроль: следит за запуском программ."');
|
||
// Родительский контроль не должен исчезать из-за одной аварии.
|
||
RunSc('failure {#ServiceName} reset= 86400 actions= restart/5000/restart/5000/restart/60000');
|
||
|
||
Result := StartService();
|
||
end;
|
||
|
||
// Каталог данных закрываем от обычных пользователей: унаследованные от
|
||
// ProgramData права позволяют им создавать здесь файлы. Ребёнок не должен ни
|
||
// читать хеш пароля, ни править правила, ни подменять установщик обновления.
|
||
// SID вместо имён групп — имена локализованы, «Administrators» на русской
|
||
// системе не существует.
|
||
procedure LockDownDataDir();
|
||
var
|
||
Code: Integer;
|
||
begin
|
||
Exec(ExpandConstant('{sys}\icacls.exe'),
|
||
'"' + ExpandConstant('{#DataDir}') + '" /inheritance:r /grant:r ' +
|
||
'*S-1-5-18:(OI)(CI)F *S-1-5-32-544:(OI)(CI)F',
|
||
'', SW_HIDE, ewWaitUntilTerminated, Code);
|
||
end;
|
||
|
||
procedure CurStepChanged(CurStep: TSetupStep);
|
||
var
|
||
Started: Boolean;
|
||
begin
|
||
if CurStep <> ssPostInstall then
|
||
Exit;
|
||
|
||
// Порядок важен: сначала закрываем каталог, потом кладём туда хеш пароля и
|
||
// только затем поднимаем службу.
|
||
LockDownDataDir();
|
||
SetPassword();
|
||
Started := InstallService();
|
||
|
||
// Обе проверки — про молчаливый отказ: без них установка отрапортует об
|
||
// успехе, а защиты не будет. При тихой установке (автообновление) человека
|
||
// за экраном нет, и разбираться придётся по журналу Inno, /LOG= его пишет.
|
||
if WizardSilent() then
|
||
Exit;
|
||
|
||
if not AuthConfigured() then
|
||
MsgBox('Не удалось сохранить пароль панели.'#13#10 +
|
||
'Панель откроется без пароля, и задать его сможет любой, ' +
|
||
'кто зайдёт первым.', mbError, MB_OK);
|
||
|
||
if not Started then
|
||
MsgBox('Служба не запустилась — контроль сейчас не работает.'#13#10 +
|
||
'Откройте «Службы» и посмотрите System Helper.', mbError, MB_OK);
|
||
end;
|
||
|
||
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
|
||
begin
|
||
if CurUninstallStep <> usUninstall then
|
||
Exit;
|
||
|
||
StopService();
|
||
RunSc('delete {#ServiceName}');
|
||
end;
|