feat(autostart): "Start with Windows" toggle
- PM.AutoStart wraps HKCU\Software\Microsoft\Windows\CurrentVersion\Run.
Value "PMServer" = "<exe>" -tray. Per-user, no admin required, shows
up in Task Manager → Startup so the user can override from there.
- UMainForm honours the -tray CLI flag (set by the registry entry):
after the server starts, MinimizeToTray via TThread.ForceQueue so the
app comes up directly in the tray with no visible window flash.
- Bridge cmd://autostart/{get,set} + Bridge.getAutoStart() /
setAutoStart() / onAutoStartStatus(). Settings exposes a toggle in
the Security section, visible only when Bridge.active (the PHP
frontend can't touch the registry).
- Toggle reads "on" only when the registered command matches the
current exe path, so a stale entry from a moved exe lets the user
re-enable to refresh.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ uses
|
||||
PM.Bridge in 'Source\PM.Bridge.pas',
|
||||
PM.QuickUnlock in 'Source\PM.QuickUnlock.pas',
|
||||
PM.UserPrefs in 'Source\PM.UserPrefs.pas',
|
||||
PM.AutoStart in 'Source\PM.AutoStart.pas',
|
||||
PM.ProcessLockdown in 'Source\PM.ProcessLockdown.pas',
|
||||
PM.Handler.Ping in 'Handlers\PM.Handler.Ping.pas',
|
||||
PM.Handler.Auth in 'Handlers\PM.Handler.Auth.pas',
|
||||
|
||||
@@ -220,6 +220,7 @@ $(PreBuildEvent)]]></PreBuildEvent>
|
||||
<DCCReference Include="Source\PM.QuickUnlock.pas"/>
|
||||
<DCCReference Include="Source\PM.UserPrefs.pas"/>
|
||||
<DCCReference Include="Source\PM.SingleInstance.pas"/>
|
||||
<DCCReference Include="Source\PM.AutoStart.pas"/>
|
||||
<DCCReference Include="Handlers\PM.Handler.Ping.pas"/>
|
||||
<DCCReference Include="Handlers\PM.Handler.Auth.pas"/>
|
||||
<DCCReference Include="Handlers\PM.Handler.Folders.pas"/>
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
unit PM.AutoStart;
|
||||
|
||||
{
|
||||
Start-with-Windows toggle.
|
||||
|
||||
Mechanism: HKCU\Software\Microsoft\Windows\CurrentVersion\Run
|
||||
- Per-user (no admin needed)
|
||||
- Visible in Task Manager → Startup tab (user can disable from there)
|
||||
- Standard pattern used by 1Password, Bitwarden, Slack, Discord, etc.
|
||||
|
||||
Command line: launched with --tray so PMServer starts minimized to the
|
||||
tray instead of popping a window during Windows login.
|
||||
|
||||
Value name "PMServer" — fixed string, not exe path-derived, so moving
|
||||
the exe + flipping the toggle off/on cleans up the old entry.
|
||||
}
|
||||
|
||||
interface
|
||||
|
||||
function IsAutoStartEnabled: Boolean;
|
||||
|
||||
// Returns True on success. Failures (registry locked, etc.) are silent —
|
||||
// caller surfaces the resulting status via IsAutoStartEnabled.
|
||||
function SetAutoStart(AEnabled: Boolean): Boolean;
|
||||
|
||||
implementation
|
||||
|
||||
uses
|
||||
System.SysUtils, System.Win.Registry, Winapi.Windows;
|
||||
|
||||
const
|
||||
RUN_KEY = 'Software\Microsoft\Windows\CurrentVersion\Run';
|
||||
VALUE_NAME = 'PMServer';
|
||||
TRAY_ARG = '-tray'; // single dash so Delphi's FindCmdLineSwitch picks it up
|
||||
|
||||
function BuildAutoStartCommand: string;
|
||||
begin
|
||||
// Quote the exe path (may contain spaces) and append the silent-start
|
||||
// arg. Single string written to a REG_SZ value — Windows splits args
|
||||
// the same way CommandLineToArgvW does.
|
||||
Result := '"' + ParamStr(0) + '" ' + TRAY_ARG;
|
||||
end;
|
||||
|
||||
function IsAutoStartEnabled: Boolean;
|
||||
var
|
||||
R: TRegistry;
|
||||
LCurrent: string;
|
||||
begin
|
||||
Result := False;
|
||||
R := TRegistry.Create(KEY_READ);
|
||||
try
|
||||
R.RootKey := HKEY_CURRENT_USER;
|
||||
if not R.OpenKeyReadOnly(RUN_KEY) then Exit;
|
||||
try
|
||||
if not R.ValueExists(VALUE_NAME) then Exit;
|
||||
LCurrent := R.ReadString(VALUE_NAME);
|
||||
// Consider the toggle "on" only if the registered command points
|
||||
// to OUR exe — a stale entry from a moved exe should read as off
|
||||
// so the user can re-enable to refresh the path.
|
||||
Result := SameText(LCurrent, BuildAutoStartCommand);
|
||||
finally
|
||||
R.CloseKey;
|
||||
end;
|
||||
finally
|
||||
R.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
function SetAutoStart(AEnabled: Boolean): Boolean;
|
||||
var
|
||||
R: TRegistry;
|
||||
begin
|
||||
Result := False;
|
||||
R := TRegistry.Create(KEY_READ or KEY_WRITE);
|
||||
try
|
||||
R.RootKey := HKEY_CURRENT_USER;
|
||||
if not R.OpenKey(RUN_KEY, True) then Exit;
|
||||
try
|
||||
if AEnabled then
|
||||
begin
|
||||
R.WriteString(VALUE_NAME, BuildAutoStartCommand);
|
||||
Result := True;
|
||||
end
|
||||
else
|
||||
begin
|
||||
if R.ValueExists(VALUE_NAME) then
|
||||
R.DeleteValue(VALUE_NAME);
|
||||
Result := True;
|
||||
end;
|
||||
finally
|
||||
R.CloseKey;
|
||||
end;
|
||||
finally
|
||||
R.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
end.
|
||||
@@ -11,7 +11,7 @@ uses
|
||||
FMX.Dialogs, FMX.DialogService,
|
||||
FMX.TMSFNCTypes, FMX.TMSFNCUtils, FMX.TMSFNCGraphics, FMX.TMSFNCGraphicsTypes,
|
||||
FMX.TMSFNCCustomControl, FMX.TMSFNCWebBrowser,
|
||||
PM.HTTPServer, PM.Bridge, PM.QuickUnlock, PM.UserPrefs;
|
||||
PM.HTTPServer, PM.Bridge, PM.QuickUnlock, PM.UserPrefs, PM.AutoStart;
|
||||
|
||||
type
|
||||
TMainForm = class(TForm)
|
||||
@@ -131,6 +131,20 @@ begin
|
||||
end;
|
||||
btnStartClick(Nil);
|
||||
btnToggleLogClick(nil);
|
||||
|
||||
// CLI flag --tray (set by the "Start with Windows" registry entry):
|
||||
// launch directly into the tray instead of popping a window during
|
||||
// the Windows login. Defer to ForceQueue so the form has finished its
|
||||
// initial Show before we hide it — minimises the visible flash.
|
||||
if FindCmdLineSwitch('tray', True) then
|
||||
begin
|
||||
TThread.ForceQueue(nil,
|
||||
procedure
|
||||
begin
|
||||
FBridge.MinimizeToTray;
|
||||
LogLine('Launched with --tray, minimised at startup');
|
||||
end);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TMainForm.FormDestroy(Sender: TObject);
|
||||
@@ -552,6 +566,28 @@ begin
|
||||
PM.UserPrefs.SetPref(LKey, GetParam('value'));
|
||||
end
|
||||
|
||||
// ---- Start with Windows (HKCU Run registry) --------------------------
|
||||
else if ACmd = 'autostart/get' then
|
||||
begin
|
||||
WebBrowser.ExecuteJavaScript(
|
||||
'if(window.Bridge&&Bridge.onAutoStartStatus)' +
|
||||
'Bridge.onAutoStartStatus(' +
|
||||
BoolToStr(PM.AutoStart.IsAutoStartEnabled, True).ToLower + ')');
|
||||
end
|
||||
|
||||
else if ACmd = 'autostart/set' then
|
||||
begin
|
||||
var LOk := PM.AutoStart.SetAutoStart(GetParam('enabled') = '1');
|
||||
LogLine(Format('Autostart set to %s (ok=%s)',
|
||||
[GetParam('enabled'), BoolToStr(LOk, True)]));
|
||||
// Echo back the resulting state so the UI re-syncs (covers the
|
||||
// case where the registry write was blocked silently).
|
||||
WebBrowser.ExecuteJavaScript(
|
||||
'if(window.Bridge&&Bridge.onAutoStartStatus)' +
|
||||
'Bridge.onAutoStartStatus(' +
|
||||
BoolToStr(PM.AutoStart.IsAutoStartEnabled, True).ToLower + ')');
|
||||
end
|
||||
|
||||
else
|
||||
LogLine('Bridge: unknown command "' + ACmd + '"');
|
||||
end;
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user