Skip to content

feat(autolaunch): cross-platform auto-launch at user login#205

Merged
kdroidFilter merged 8 commits intomainfrom
feat/autolaunch
Apr 18, 2026
Merged

feat(autolaunch): cross-platform auto-launch at user login#205
kdroidFilter merged 8 commits intomainfrom
feat/autolaunch

Conversation

@kdroidFilter
Copy link
Copy Markdown
Owner

@kdroidFilter kdroidFilter commented Apr 17, 2026

Summary

New autolaunch runtime module: unified API — AutoLaunch.state / enable / disable / wasStartedAtLogin / openSystemSettings — across every supported packaging type on Windows, macOS, and Linux.

Windows

  • MSIX: WinRT StartupTask API via WRL on a dedicated MTA thread (AWT EDT is STA, avoids message-pump-dependent async completion). TaskId auto-injected by the Gradle plugin when appx { addAutoLaunchExtension = true } (exposed via NucleusApp.startupTaskId, overridable via AppXSettings.startupTaskId).
  • Win32 (MSI / NSIS): HKCU\…\Run + StartupApproved\Run parity rule (flag & 1 == 1DISABLED_BY_USER). Injects --nucleus-autostart CLI marker.
  • Login detection:
    • Win32: CLI marker check.
    • MSIX: walks the process ancestor chain (skipping self-spawned jpackage launcher), detects sihost.exe as external parent (empirical finding on Win11 — MSIX startup activations route through Shell Infrastructure Host, not taskhostw.exe as some docs suggest). Deterministic, no heuristics, no WinAppSDK / PSF dependency.
  • Native DLLs: x64 + ARM64 in nucleus_autolaunch.dll.

macOS

  • Unified backend using SMAppService.mainApp (macOS 13+) — same pattern as sindresorhus/LaunchAtLogin-Modern. Registers the main app itself as a login item (no helper app, no bundled plist, no build-time plugin changes). Entry appears under System Settings → General → Login Items → Open at Login (not "Allow in the Background").
  • Works identically for DMG (Developer ID) and PKG (Mac App Store sandboxed).
  • Login detection: Apple exposes no public API for SMAppService.mainApp launches (open feedback FB10207829). Empirical signal: launchd injects LaunchInstanceID into the environment of every managed job it spawns, including SMAppService.mainApp login items; Finder / Dock / Spotlight / open(1) launches go through LaunchServices and do not carry this variable. Deterministic, no heuristic scoring.
  • Reuses the existing service-management module (no new native code).

Linux

  • Dispatcher that picks the right backend at runtime:
    • Host (deb / rpm / AppImage / dev)SystemdUserBackend: writes ~/.config/systemd/user/<app>.service and toggles it via org.freedesktop.systemd1.Manager.EnableUnitFiles. Login detection via INVOCATION_ID env var injected by systemd for every unit invocation.
    • FlatpakFlatpakPortalBackend: the sandbox can't reach the host's systemd, so uses org.freedesktop.portal.Background.RequestBackground with autostart=true. Login detection via the --nucleus-autostart CLI marker passed through the portal's commandline (safe because flatpak run <id> has no spaces — sidesteps the portal's Exec= quoting bug).
  • Native shared lib nucleus_autolaunch_linux.so (x64 + aarch64), dlopen'd GLib — no compile-time dep.

Shared

  • GraalVM reachability metadata included.
  • Example app: new Auto-Launch tab with toggle, diagnostic view, and home-screen banner when started at login; macOS-specific launch diagnostic panel.
  • Docs: new runtime/autolaunch.md page wired into mkdocs nav.
  • CI: build + verify + upload in build-natives.yaml (Windows x64/ARM64, Linux x64/aarch64); download + EXPECTED entries added to all 6 consumer workflows.

Test plan

Windows

  • Win32 (MSI or NSIS): enable via switch → logoff/logon → banner appears → disable via switch → logoff/logon → banner gone.
  • Win32: toggle off via Task Manager → state() reports DISABLED_BY_USERenable() returns BLOCKED_BY_USERopenSystemSettings() opens ms-settings:startupapps.
  • MSIX (AppX): install, toggle on, logoff/logon → diagnostic shows external parent = sihost.exe → banner visible.
  • MSIX manual launch (Start menu) → diagnostic shows external parent = explorer.exe → no banner.
  • MSIX StartupTaskState.DisabledByUser after user toggled via Task Manager → enable() returns BLOCKED_BY_USER (no re-enable attempt).
  • Silent app update: existing StartupApproved DISABLED_BY_USER not overwritten.

macOS

  • DMG (Developer ID): enable → entry appears in System Settings → Login Items → Open at Login → logout/login → banner visible, diagnostic shows LaunchInstanceID present: true.
  • Manual launch from Finder / Dock / Spotlight → no banner, LaunchInstanceID present: false.
  • User flips toggle off in System Settings → state() reports DISABLED_BY_USERenable() returns BLOCKED_BY_USER.
  • PKG (sandboxed) build: same enable / disable / login-detection behavior.

Linux

  • deb/rpm or AppImage (host): enable → ~/.config/systemd/user/<app>.service written and enabled → logout/login → banner visible, diagnostic shows INVOCATION_ID present: true.
  • Manual launch from desktop menu → no banner.
  • Flatpak build: enable → portal prompt → logout/login → banner visible (CLI marker carried through portal's commandline).
  • disable() on both backends cleanly removes the unit / revokes the portal permission.

Cross-cutting

  • ./gradlew :autolaunch:test passes (Windows-only tests).
  • GraalVM native image of the example still launches and detects auto-launch correctly on each OS.
  • All three backends: state() / enable() / disable() / wasStartedAtLogin() never throw; unsupported environments return UNSUPPORTED / false.

kdroidFilter and others added 8 commits April 17, 2026 14:33
New autolaunch module with unified API across MSIX and Win32 packaging.
MSIX uses WinRT StartupTask; Win32 uses HKCU\Run + StartupApproved parity rule.
Detects auto-launched starts via CLI marker (Win32) and parent-process walk
to sihost.exe (MSIX). Plugin auto-injects StartupTask TaskId into app
metadata when addAutoLaunchExtension is enabled. Example gains an
Auto-Launch tab and home banner when started at login.
Wires auto-launch at login for macOS (DMG and PKG alike) on top of the
existing service-management-macos bridge. The entry appears under
System Settings → Login Items → Open at Login, matching the behavior
of sindresorhus/LaunchAtLogin-Modern.

Runtime detection uses the official kAEOpenApplication AppleEvent with
the keyAELaunchedAsLogInItem marker: the native bridge installs an
observer at dylib-load time (__attribute__((constructor))) so the flag
is captured before AWT's NSApplication consumes the event.

The AutoLaunchBackend SPI gains a default wasStartedAtLogin(args) impl
based on the CLI marker, overridden by MSIX and macOS backends. The
dispatcher loads the macOS backend reflectively so consumers who don't
ship PKG/DMG on macOS don't need to add the service-management-macos
dependency.

Requires macOS 13+ (SMAppService); older releases return UNSUPPORTED.
AutoLaunch.preload() is the standard JVM-style warmup hook that resolves
the platform backend (and loads any JNI it requires) on the calling
thread, so apps can run it from a background daemon thread early in
main() to avoid first-touch latency on the EDT.

Also restructures the example to read startedAtLogin lazily from inside
NucleusContent rather than from main(): on macOS the kAEOpenApplication
AppleEvent is only delivered once NSApp.run() starts, so the early call
from main() always returns false there. Stashing the args and re-querying
from a Composable is the JVM equivalent of checking the flag in Cocoa's
applicationDidFinishLaunching delegate.
Use SMAppService directly instead of custom JNI wrapper.
…chInstanceID

Empirical signal: launchd injects LaunchInstanceID env var only for managed jobs
(SMAppService.mainApp login items), not for user-initiated launches (Finder/Dock).
Verified by comparing boot-time vs manual launch diagnostics on macOS 14.7.

Also adds MacLaunchDiagnostic utility to capture process bootstrap context
(launchctl print, ps tree, environment, sysctl) for debugging and analysis.
Example app displays diagnostic in Auto-Launch screen for inspection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…atpak portal

Implements cross-platform auto-launch at login for Linux:

- Systemd user services backend (~/.config/systemd/user/) for host installations
  (deb/rpm/AppImage) via org.freedesktop.systemd1 D-Bus API with proper ExecStart
  quoting for paths containing spaces.

- XDG Desktop Portal Background API for Flatpak sandboxes, with CLI marker injection
  for login-launch detection (portal provides no context signal).

- Detection via /proc/self/cgroup parsing (systemd host) or CLI marker (flatpak).

- Respects graphical-session.target ordering to ensure DISPLAY/WAYLAND_DISPLAY are
  exported before app launch, preventing HeadlessException.

- Diagnostic output includes portal availability, unit state, cgroup paths for
  troubleshooting.

New files:
- LinuxAutoLaunch.kt: dispatcher routing to SystemdUserBackend or FlatpakPortalBackend
- SystemdUserBackend.kt: manages ~/.config/systemd/user/<appId>.service lifecycle
- FlatpakPortalBackend.kt: XDG portal RequestBackground API with marker-based detection
- NativeAutoLaunchLinuxBridge.kt: JNI bridge exposing D-Bus and portal APIs
- nucleus_autolaunch_linux.c: C implementation of D-Bus/portal native calls
- build.sh: build script for Linux shared library compilation

Modified files:
- AutoLaunch.kt: added Linux dispatch in resolveBackend()
- AutoLaunchConfig.kt: added backgroundReason field for portal prompt customization
- build.gradle.kts: added buildNativeLinux task with prebuilt library detection
- reachability-metadata.json: added NativeAutoLaunchLinuxBridge for GraalVM native-image
@kdroidFilter kdroidFilter marked this pull request as ready for review April 18, 2026 21:59
@kdroidFilter kdroidFilter merged commit 87737b9 into main Apr 18, 2026
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant