Files
syshelper/installer/setup.iss
T

274 lines
12 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
; Установщик «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;