feat: implement Windows service for parental control application
This commit is contained in:
+185
-40
@@ -1,4 +1,4 @@
|
||||
; Установщик «System Helper» для Windows x64.
|
||||
; Установщик «System Helper» для Windows x64.
|
||||
; Сборка: ISCC.exe /DAppVersion=0.1.0 installer\setup.iss
|
||||
; Результат кладётся в dist\syshelper-<версия>-setup.exe
|
||||
|
||||
@@ -10,11 +10,13 @@
|
||||
#define ExeSource "..\target\release\syshelper.exe"
|
||||
#endif
|
||||
|
||||
#define AppName "System Helper"
|
||||
#define AppExeName "syshelper.exe"
|
||||
#define AppUrl "http://127.0.0.1:8787"
|
||||
; Имя задачи автообновления — должно совпадать с TASK_NAME в src/update.rs.
|
||||
#define TaskName "SysHelperUpdate"
|
||||
#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 определяет программу при обновлении и удалении — не менять между версиями.
|
||||
@@ -38,8 +40,10 @@ WizardStyle=modern
|
||||
ArchitecturesAllowed=x64compatible
|
||||
ArchitecturesInstallIn64BitMode=x64compatible
|
||||
|
||||
; Program Files и автозапуск через HKLM требуют администратора.
|
||||
; Регистрация службы и запись в Program Files требуют администратора.
|
||||
PrivilegesRequired=admin
|
||||
; Страховка: службу мы останавливаем сами, но если файл всё же занят —
|
||||
; пусть Inno закроет процесс, а не просит перезагрузку.
|
||||
CloseApplications=yes
|
||||
RestartApplications=no
|
||||
|
||||
@@ -48,52 +52,193 @@ Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl"
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
|
||||
[Tasks]
|
||||
Name: "autostart"; \
|
||||
Description: "Запускать при входе в систему"; \
|
||||
GroupDescription: "Дополнительно:"
|
||||
; Автозапуск больше не задача: служба и так стартует вместе с системой.
|
||||
Name: "desktopicon"; \
|
||||
Description: "{cm:CreateDesktopIcon}"; \
|
||||
GroupDescription: "Дополнительно:"; \
|
||||
Flags: unchecked
|
||||
|
||||
[Dirs]
|
||||
; Сюда работающая от пользователя программа кладёт скачанный установщик, а
|
||||
; задача Планировщика (от SYSTEM) забирает его. Нужен доступ на запись обоим.
|
||||
Name: "{commonappdata}\SysHelper\update"; Permissions: users-modify
|
||||
; Каталог данных службы: правила, журнал, хеш пароля, скачанные обновления.
|
||||
; Права закрываем через icacls ниже — унаследованные от ProgramData позволяют
|
||||
; обычному пользователю создавать здесь файлы. Он не должен ни читать хеш
|
||||
; пароля, ни править правила, ни подменять установщик обновления.
|
||||
Name: "{#DataDir}"
|
||||
|
||||
[Files]
|
||||
Source: "{#ExeSource}"; DestDir: "{app}"; Flags: ignoreversion
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}"
|
||||
Name: "{group}\Веб-панель"; Filename: "{#AppUrl}"
|
||||
Name: "{autodesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Tasks: desktopicon
|
||||
|
||||
[Registry]
|
||||
; Автозапуск для всех пользователей. Запускается без повышения прав —
|
||||
; этого хватает, чтобы завершать процессы того же пользователя.
|
||||
Root: HKLM; Subkey: "Software\Microsoft\Windows\CurrentVersion\Run"; \
|
||||
ValueType: string; ValueName: "SysHelper"; \
|
||||
ValueData: """{app}\{#AppExeName}"""; \
|
||||
Flags: uninsdeletevalue; Tasks: autostart
|
||||
; Ярлыки ведут на панель: своего окна у программы нет, работает она службой.
|
||||
Name: "{group}\{#AppName}"; Filename: "{#AppUrl}"
|
||||
Name: "{group}\{cm:UninstallProgram,{#AppName}}"; Filename: "{uninstallexe}"
|
||||
Name: "{autodesktop}\{#AppName}"; Filename: "{#AppUrl}"; Tasks: desktopicon
|
||||
|
||||
[Run]
|
||||
; Задача автообновления: программа скачивает свежий установщик и просит
|
||||
; Планировщик запустить его от SYSTEM с наивысшими правами — так обновление
|
||||
; ставится тихо, без запроса UAC. Своего расписания у задачи нет (SC ONCE в
|
||||
; прошлом), её лишь запускают по требованию из программы.
|
||||
Filename: "{sys}\schtasks.exe"; \
|
||||
Parameters: "/Create /F /RU SYSTEM /RL HIGHEST /SC ONCE /SD 01/01/2000 /ST 00:00 /TN ""{#TaskName}"" /TR ""{commonappdata}\SysHelper\update\setup.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /NOCANCEL"""; \
|
||||
Flags: runhidden; StatusMsg: "Настройка автообновления..."
|
||||
Filename: "{app}\{#AppExeName}"; \
|
||||
Description: "Запустить {#AppName}"; \
|
||||
Flags: nowait postinstall skipifsilent
|
||||
; Права на каталог данных выставляются в [Code] до записи пароля и старта
|
||||
; службы — здесь было бы уже поздно: [Run] выполняется последним.
|
||||
Filename: "{#AppUrl}"; \
|
||||
Description: "Открыть веб-панель"; \
|
||||
Description: "Открыть панель"; \
|
||||
Flags: shellexec nowait postinstall skipifsilent
|
||||
|
||||
[UninstallRun]
|
||||
Filename: "{sys}\schtasks.exe"; Parameters: "/Delete /F /TN ""{#TaskName}"""; \
|
||||
Flags: runhidden; RunOnceId: "DelUpdateTask"
|
||||
Filename: "{sys}\taskkill.exe"; Parameters: "/f /im {#AppExeName}"; \
|
||||
Flags: runhidden; RunOnceId: "StopApp"
|
||||
[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;
|
||||
|
||||
procedure InstallService();
|
||||
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');
|
||||
RunSc('start {#ServiceName}');
|
||||
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);
|
||||
begin
|
||||
if CurStep <> ssPostInstall then
|
||||
Exit;
|
||||
|
||||
// Порядок важен: сначала закрываем каталог, потом кладём туда хеш пароля и
|
||||
// только затем поднимаем службу.
|
||||
LockDownDataDir();
|
||||
SetPassword();
|
||||
InstallService();
|
||||
end;
|
||||
|
||||
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
|
||||
begin
|
||||
if CurUninstallStep <> usUninstall then
|
||||
Exit;
|
||||
|
||||
StopService();
|
||||
RunSc('delete {#ServiceName}');
|
||||
end;
|
||||
|
||||
Reference in New Issue
Block a user