Files
tsunagi/dist/windows/tsunagi.iss
T

243 lines
8.4 KiB
Plaintext

; Inno Setup script for the Tsunagi Windows installer.
;
; One installer that leaves a working machine:
; * the agent (tsng.exe), as a Windows service that starts at boot
; * Wintun, the driver library behind the overlay interface
; * the tray application, started at sign-in
; * `tsng` on the PATH, and an inbound Defender Firewall rule for the agent
;
; Built in CI (see .github/workflows/release.yml); locally:
; ISCC.exe /DAppVersion=0.1.0 /DSourceDir=<dir with tsng.exe and tsunagi-tray.exe>
; /DWintunDir=<wintun\bin\amd64> /O<output dir> dist\windows\tsunagi.iss
; Silent install: tsunagi-setup-<version>-x86_64.exe /VERYSILENT /SUPPRESSMSGBOXES
; Pick tasks with /TASKS="!autostart,addtopath,firewall".
#ifndef AppVersion
#define AppVersion "0.0.0"
#endif
#ifndef SourceDir
#error SourceDir must point at the directory holding tsng.exe and tsunagi-tray.exe
#endif
#ifndef WintunDir
#error WintunDir must point at the directory holding wintun.dll
#endif
#define ServiceName "Tsunagi"
[Setup]
AppId={{F635E5DD-250D-499F-B0B3-AFC7CC64A15D}
AppName=Tsunagi
AppVersion={#AppVersion}
AppPublisher=Tsunagi
DefaultDirName={autopf}\Tsunagi
DefaultGroupName=Tsunagi
DisableProgramGroupPage=yes
PrivilegesRequired=admin
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
OutputBaseFilename=tsunagi-setup-{#AppVersion}-x86_64
Compression=lzma2
SolidCompression=yes
WizardStyle=modern
ChangesEnvironment=yes
UninstallDisplayName=Tsunagi
; The service and the tray are stopped by hand below, so the installer does
; not need to ask Windows to close applications.
CloseApplications=no
[Tasks]
Name: "autostart"; Description: "Start the tray icon when I sign in"; GroupDescription: "Tsunagi:"
Name: "addtopath"; Description: "Add tsng to the PATH"; GroupDescription: "Tsunagi:"
Name: "firewall"; Description: "Allow the agent through Windows Firewall (direct connections between devices)"; GroupDescription: "Tsunagi:"
[Files]
Source: "{#SourceDir}\tsng.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#SourceDir}\tsunagi-tray.exe"; DestDir: "{app}"; Flags: ignoreversion
; Wintun is the unmodified, signed library from wintun.net; its licence allows
; shipping it with an application and asks for the licence text beside it.
; In the archive the text sits at its root, two levels above bin\amd64.
Source: "{#WintunDir}\wintun.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#WintunDir}\..\..\LICENSE.txt"; DestDir: "{app}\licenses"; DestName: "wintun-LICENSE.txt"; Flags: ignoreversion
[Dirs]
; Identity and network secrets live here; locked down below.
Name: "{commonappdata}\Tsunagi\data"
Name: "{commonappdata}\Tsunagi\cache"
Name: "{commonappdata}\Tsunagi\logs"
[Icons]
Name: "{group}\Tsunagi"; Filename: "{app}\tsunagi-tray.exe"
Name: "{commonstartup}\Tsunagi"; Filename: "{app}\tsunagi-tray.exe"; Tasks: autostart
[Registry]
Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; \
ValueType: expandsz; ValueName: "Path"; ValueData: "{olddata};{app}"; \
Check: NeedsAddPath('{app}'); Tasks: addtopath
[Run]
; Only the system account and administrators may read the state: it holds the
; device's key and every network's secret. SIDs, so it works in any language.
Filename: "{sys}\icacls.exe"; \
Parameters: """{commonappdata}\Tsunagi\data"" /inheritance:r /grant:r *S-1-5-18:(OI)(CI)F *S-1-5-32-544:(OI)(CI)F"; \
Flags: runhidden; StatusMsg: "Protecting the agent's data..."
Filename: "{sys}\icacls.exe"; \
Parameters: """{commonappdata}\Tsunagi\cache"" /inheritance:r /grant:r *S-1-5-18:(OI)(CI)F *S-1-5-32-544:(OI)(CI)F"; \
Flags: runhidden
Filename: "{sys}\icacls.exe"; \
Parameters: """{commonappdata}\Tsunagi\logs"" /inheritance:r /grant:r *S-1-5-18:(OI)(CI)F *S-1-5-32-544:(OI)(CI)F"; \
Flags: runhidden
; The tray runs as the person who installed, not elevated, like it does at sign-in.
Filename: "{app}\tsunagi-tray.exe"; Description: "Start the Tsunagi tray"; \
Flags: nowait postinstall skipifsilent runasoriginaluser
[UninstallRun]
Filename: "{sys}\netsh.exe"; Parameters: "advfirewall firewall delete rule name=""Tsunagi"""; \
Flags: runhidden; RunOnceId: "RemoveFirewallRule"
[Code]
const
ServiceName = '{#ServiceName}';
EnvKey = 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment';
function Sc(const Params: String): Integer;
var
Code: Integer;
begin
if Exec(ExpandConstant('{sys}\sc.exe'), Params, '', SW_HIDE, ewWaitUntilTerminated, Code) then
Result := Code
else
Result := -1;
end;
function ServiceExists: Boolean;
begin
// 1060 is ERROR_SERVICE_DOES_NOT_EXIST.
Result := Sc('query ' + ServiceName) <> 1060;
end;
procedure StopService;
var
Code: Integer;
begin
// `net stop` waits until the service has stopped, or says it was not running.
Exec(ExpandConstant('{sys}\net.exe'), 'stop ' + ServiceName, '', SW_HIDE, ewWaitUntilTerminated, Code);
end;
procedure StopTray;
var
Code: Integer;
begin
Exec(ExpandConstant('{sys}\taskkill.exe'), '/F /IM tsunagi-tray.exe', '', SW_HIDE, ewWaitUntilTerminated, Code);
end;
// An agent started by hand (`tsng up`) holds the control pipe and the Wintun
// adapter, and the file about to be replaced: it cannot run beside the service.
procedure StopStrayAgents;
var
Code: Integer;
begin
Exec(ExpandConstant('{sys}\taskkill.exe'), '/F /IM tsng.exe', '', SW_HIDE, ewWaitUntilTerminated, Code);
end;
function NeedsAddPath(Param: String): Boolean;
var
Current: String;
begin
if not RegQueryStringValue(HKLM, EnvKey, 'Path', Current) then
begin
Result := True;
exit;
end;
Result := Pos(';' + Uppercase(ExpandConstant(Param)) + ';', ';' + Uppercase(Current) + ';') = 0;
end;
procedure RemoveFromPath;
var
Current, Entry: String;
At: Integer;
begin
if not RegQueryStringValue(HKLM, EnvKey, 'Path', Current) then
exit;
Entry := ';' + ExpandConstant('{app}');
At := Pos(Uppercase(Entry), Uppercase(Current));
if At > 0 then
begin
Delete(Current, At, Length(Entry));
RegWriteExpandStringValue(HKLM, EnvKey, 'Path', Current);
end;
end;
// Stops what is about to be replaced: the files are in use while it runs.
function PrepareToInstall(var NeedsRestart: Boolean): String;
begin
StopTray;
if ServiceExists then
StopService;
StopStrayAgents;
Result := '';
end;
procedure InstallService;
var
App: String;
begin
App := ExpandConstant('{app}\tsng.exe');
// The command line is `tsng service`: it keeps its state under
// %ProgramData%\Tsunagi and serves the system-wide control socket.
if ServiceExists then
Sc('config ' + ServiceName + ' binPath= "\"' + App + '\" service" start= auto obj= LocalSystem')
else
Sc('create ' + ServiceName + ' binPath= "\"' + App + '\" service" start= auto obj= LocalSystem DisplayName= "Tsunagi agent"');
Sc('description ' + ServiceName + ' "Tsunagi: private mesh network agent"');
// Restart after a crash, backing off, and forget the failures after a day.
Sc('failure ' + ServiceName + ' reset= 86400 actions= restart/5000/restart/5000/restart/30000');
Sc('start ' + ServiceName);
end;
procedure InstallFirewallRule;
var
Code: Integer;
Netsh: String;
begin
Netsh := ExpandConstant('{sys}\netsh.exe');
Exec(Netsh, 'advfirewall firewall delete rule name="Tsunagi"', '', SW_HIDE, ewWaitUntilTerminated, Code);
if WizardIsTaskSelected('firewall') then
Exec(Netsh,
'advfirewall firewall add rule name="Tsunagi" dir=in action=allow enable=yes profile=any program="' +
ExpandConstant('{app}\tsng.exe') + '"',
'', SW_HIDE, ewWaitUntilTerminated, Code);
end;
procedure CurStepChanged(CurStep: TSetupStep);
begin
if CurStep = ssPostInstall then
begin
InstallFirewallRule;
InstallService;
end;
end;
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
begin
if CurUninstallStep = usUninstall then
begin
StopTray;
if ServiceExists then
begin
StopService;
Sc('delete ' + ServiceName);
end;
StopStrayAgents;
RemoveFromPath;
end;
if CurUninstallStep = usPostUninstall then
begin
// The data is the device's identity and its networks: kept unless asked,
// and never removed by a silent uninstall.
if (not UninstallSilent) and
(MsgBox('Also delete this device''s identity, networks and logs?' + #13#10 +
'Choose No to keep them for a later reinstall.', mbConfirmation, MB_YESNO or MB_DEFBUTTON2) = IDYES) then
DelTree(ExpandConstant('{commonappdata}\Tsunagi'), True, True, True);
end;
end;