Skip to content

feat(scheduler): OS-level background task scheduling#196

Draft
kdroidFilter wants to merge 42 commits intomainfrom
feat/scheduler
Draft

feat(scheduler): OS-level background task scheduling#196
kdroidFilter wants to merge 42 commits intomainfrom
feat/scheduler

Conversation

@kdroidFilter
Copy link
Copy Markdown
Owner

@kdroidFilter kdroidFilter commented Apr 14, 2026

Summary

Add OS-level background task scheduling for JVM desktop apps, inspired by Android's WorkManager API. Tasks persist across app restarts and survive reboots.

New modules

Module Description
scheduler Core API + platform backends (Linux systemd, macOS launchd, Windows Task Scheduler 2.0)
service-management-macos SMAppService wrapper for sandboxed/App Store login items and agents (macOS 13+)
scheduler-demo Interactive Compose Desktop demo app
service-management-demo SMAppService demo (macOS only)

Platform implementations

Platform Backend Native layer
Linux systemd user timers (~/.config/systemd/user/) C — D-Bus via GLib/GIO (libnucleus_scheduler_linux.so)
macOS launchd user agents (~/Library/LaunchAgents/) Objective-C — JNI bridge with launchctl shell fallback (libnucleus_scheduler.dylib)
Windows Task Scheduler 2.0 COM API C++ — COM via JNI (nucleus_scheduler.dll)

No admin rights required on any platform.

API surface

  • DesktopTaskScheduler — facade: enqueue(), cancel(), cancelAll(), isScheduled(), getTaskInfo(), getAllTasks()
  • TaskRequest — immutable config with periodic(), calendar(), onBoot() factory methods
  • DesktopTasksuspend doWork(context): TaskResult (Success / Failure / Retry)
  • CronExpression — helpers: everyDayAt(), everyWeekdayAt(), everyHour(), custom()
  • RetryPolicyExponentialBackoff / Linear with configurable attempts
  • DesktopBootReceiver — detects scheduler invocations in main() args
  • TaskRegistry — maps task IDs to factories

service-management-macos API

  • AppServiceManagerregister(), unregister(), status(), openSystemSettingsLoginItems()
  • AppService sealed class — MainApp, LoginItem, Agent, Daemon
  • Gradle DSL: launchAgents { agent("label") { ... } } for bundling plists

Test plan

  • ./gradlew :scheduler:build passes (compilation + ktlint + detekt)
  • ./gradlew :scheduler-demo:build passes
  • ./gradlew :service-management-macos:build passes
  • ./gradlew :service-management-demo:build passes
  • Linux: enqueue periodic task → .timer + .service created in ~/.config/systemd/user/
  • Linux: systemctl --user list-timers shows scheduled task
  • macOS: enqueue task → plist created in ~/Library/LaunchAgents/
  • macOS: verify JNI bridge and launchctl fallback both work
  • Windows: enqueue task → visible in Task Scheduler (taskschd.msc)
  • All platforms: cancel task → backing files removed
  • Unsupported platform: noop behavior (no crash, returns false)
  • DesktopBootReceiver.isSchedulerInvocation() correctly detects scheduler args
  • Retry policy triggers on TaskResult.Retry

kdroidFilter and others added 30 commits April 14, 2026 08:44
Add scheduler module with WorkManager-inspired API for scheduling
background tasks via systemd user timers on Linux (noop on other
platforms). Includes scheduler-demo Jewel app for testing.
Add WindowsTaskScheduler using schtasks.exe for Windows support:
- Create/delete/query tasks with OS-level persistence
- Support periodic, calendar (cron), and on-boot schedules
- Automatic retry scheduling with configurable delays
- Platform-aware metadata storage (%LOCALAPPDATA% on Windows)
- Cron-to-schtasks expression conversion for common schedules

Routes Platform.Windows to WindowsTaskScheduler in DesktopTaskScheduler.
Updates DesktopBootReceiver and TaskMetadataStore for cross-platform compatibility.
- Fix getAllTaskIds() using locale-independent metadata store instead of parsing localized schtasks output
- Ensure metadata is always saved when enqueue() succeeds, including KEEP policy fast-path
- Use CSV column positions (not field names) for parseNextRun() to avoid locale dependency
- Support multiple datetime formats for Windows regional settings
- Update demo view to not hardcode "Linux systemd"
…scheduling

- scheduleRetry() now uses DateFormat.getDateInstance(SHORT) to match system locale
- parseNextRun() tries system DateFormat.getDateTimeInstance() before fallback patterns
- Ensures compatibility with all Windows regional settings (en-US, fr-FR, de-DE, etc.)
Add MacOSLaunchdScheduler using launchd user agents. Supports periodic,
calendar-based, and on-boot task scheduling with retry support via
one-shot agents. Metadata stored in ~/Library/Application Support/.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…kScheduler logging

- Enable multi-platform packaging (NSIS, DMG, Deb, AppX) with signing support
- Add advanced configuration for Windows installers (shortcuts, metadata)
- Fix escaping in WindowsTaskScheduler command paths
- Improve logging for schtasks execution failures
…gent DSL

Implement macOS background task scheduling via Apple's SMAppService framework with:
- New service-management-macos JNI binding module exposing SMAppService to Kotlin
- service-management-demo app showing login item and background agent patterns
- Type-safe Gradle DSL for declarative launch agent configuration (plist generation)
- Plugin integration for embedding and code-signing plists in macOS app bundles
- CI/CD native build steps for macOS (aarch64/x64)
…tion and open UI from scheduler

- Add CountDownLatch to MacOsDispatcher.send() to wait for UNUserNotificationCenter completion handler, preventing premature process exit
- Remove return after DesktopBootReceiver.handle() so scheduler-triggered app opens the UI with banner
- Simplify NotificationTask to just return Success, letting normal app startup continue
- Remove notification-common dependency from demo (no longer needed)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ation and streamline API

- Add complete scheduler documentation with quick start, usage patterns, API reference, and platform details
- Add service-management-macos documentation with Gradle DSL integration and Compose examples
- Auto-resolve bundleProgram from packageName in launch agent DSL
- Auto-add .plist suffix in AppService.Agent/Daemon for cleaner API
- Add .pkg validation to prevent scheduler use in sandboxed Mac App Store builds
- Update mkdocs.yml and runtime index with new modules

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…eduled on macOS

MacOSLaunchdScheduler.isScheduled() was incorrectly checking if the launchd agent
was currently loaded via launchctl list. This could return false for valid scheduled
tasks that weren't yet loaded (e.g. after reboot, before launchd loads them). The
plist file in ~/Library/LaunchAgents/ is the authoritative source.

Fix: isScheduled() now only checks plist existence. getTaskInfo() still uses
isLoaded() to determine task state (SCHEDULED vs INACTIVE).

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

- Throw exception for unsupported calendar expressions instead of silent degradation
- Fix retry scheduling: use RunAtLoad-only with daemon thread delay, add cleanup
- Fix Windows schtasks injection vulnerability by escaping taskId in /TR argument
- Make Linux OnBootSec dynamic (10% interval, clamped 60-300s)
- Remove unimplemented runOnce parameter from onBoot() API
- Update documentation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace escaped backslash-quotes with proper quotes in schtasks /TR arg
- Update NoopScheduler KDoc to say "unsupported platforms"
- ConvertCronToSchtasksTest: 9 tests covering daily, hourly, weekday,
  day range, whitespace, and unsupported expressions
- BuildTimerUnitTest: 8 tests covering periodic/calendar timer units,
  OnBootSec clamping, and systemd section structure
- AppendCalendarIntervalTest: 6 tests covering launchd plist calendar
  intervals, weekday mapping, day ranges, and unsupported expressions
- Make convertCronToSchtasks, buildTimerUnit, appendCalendarInterval
  internal for testability
…flags

- Validate taskId against [a-zA-Z0-9_-]+ to prevent command injection
- Switch parseNextRun from CSV to XML parsing for locale-independent dates
- Parse actual task state (Ready/Running/Disabled) from schtasks output
- Add /IT flag to prevent tasks running in detached sessions
- Add /Z flag to auto-delete retry tasks after execution
- Fix cancelAll to only delete metadata for successfully removed tasks

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…lace schtasks.exe

Replaces the fragile schtasks.exe subprocess approach with direct COM interop:
- Windows Task Scheduler API now called via JNI (nucleus_scheduler.dll)
- C++ implementation handles ITaskService, ITaskFolder, ITaskDefinition, ITrigger
- Eliminates quoting issues, locale-dependent XML parsing, process timeouts
- Per-trigger type JNI methods (periodic, daily, weekly, logon, once)
- Task enumeration via COM folder API instead of schtasks output parsing
- GraalVM reachability metadata included
- Fallback to NoopScheduler if native library not available
- All tests passing (cron conversion, Linux systemd, macOS launchd)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
VariantTimeToSystemTime returns local time but SystemTimeToFileTime
expects UTC, causing nextRunTime to be offset by the timezone delta.
…leanup on uninstall

Scheduled tasks now run via wrapper scripts that check if the app executable exists.
If missing (app uninstalled), the wrapper self-destructs: unregisters the task from
the OS (via COM API on Windows, systemctl on Linux, launchctl on macOS), deletes
metadata and script files, leaving no orphaned tasks.

Windows uses wscript.exe with VBScript (.vbs) — zero visible console window.
macOS/Linux use bash (.sh) scripts. DesktopBootReceiver also self-cancels tasks
when app runs but task not found in registry (handles app reinstalls).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…e wrapper scripts

On macOS, launchd plists that run a wrapper script caused System Settings > Login Items
to display the script filename (e.g. 'notification.sh') instead of the app name.

Solution: point ProgramArguments directly to the app executable with --nucleus-scheduler-run.
launchd now launches the app binary, so macOS displays the correct app name.

If the app is uninstalled, launchd silently fails (no popup, no CPU). Orphan plists
are cleaned up by DesktopBootReceiver on the next app run. Windows and Linux retain
wrapper scripts for self-destruct cleanup on uninstall (not needed on macOS).
… parameter

- Default behavior: first execution after full interval (consistent across platforms)
- Linux: OnActiveSec=interval, runImmediately uses OnActiveSec=0
- Windows: StartBoundary deferred by interval, runImmediately uses now
- macOS: RunAtLoad added only when runImmediately is true
- Removed hardcoded boot delay fraction logic (BOOT_DELAY_FRACTION, MIN/MAX constants)
- Updated tests to match new behavior

Fixes inconsistent first-run timing across platforms where dev has no control.
Replaces ProcessBuilder-based launchctl with native Objective-C JNI:
- NSDictionary + NSPropertyListSerialization for type-safe plist generation
- SMJobCopyDictionary for subprocess-free job state queries
- NSCalendar for next-fire-time computation (resolves nextRunMs=null)
- NSTask for launchctl load/unload with proper error handling
- dispatch_after for persistent retry scheduling

Introduces dual-path fallback: native path when dylib available, shell
fallback when unavailable (GraalVM, cross-compile, etc).

Adds schedule hint persistence to TaskMetadataStore for next-fire-time
calculation without plist re-parsing.

Updates CI workflows to build, verify, and distribute darwin-aarch64
and darwin-x64 dylibs alongside Windows scheduler DLLs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rk mode

On Linux, SkiaLayer clears to white (transparency is disabled to avoid
compositor artifacts). This masks the AWT window.background color, causing
the content area to appear white instead of the dark theme panelBackground.

macOS and Windows work correctly because their Skiko clear is transparent
(native on macOS, explicitly enabled on Windows in dark mode).

Solution: add an explicit Compose background modifier to the root Layout in
DecoratedWindowBody using titleBarBackground. This ensures consistent
rendering on all three platforms regardless of Skiko's clear behavior.

Fixes scheduler-demo background color mismatch between macOS and Linux.
Replace subprocess-based systemd control (systemctl --user) with direct
D-Bus calls via GIO/GDBus. Mirrors the Windows JNI pattern, reducing
overhead and subprocess spawning. Follows existing D-Bus infrastructure
in notification-linux, launcher-linux, and global-hotkey.

Changes:
- nucleus_scheduler_linux.c: 7 JNI functions (Reload, Enable/DisableUnitFiles,
  Start/GetUnitFileState, GetUnitActiveState, GetTimerNextElapseUSec)
- build.sh: Native compilation (gcc + pkg-config gio-2.0)
- LinuxSystemdSchedulerJni.kt: External function declarations with isLoaded gate
- LinuxSystemdScheduler.kt: All systemctl calls → JNI with isAvailable guard
- build.gradle.kts: buildNativeLinux task
- Reachability metadata for GraalVM native-image
- CI: build-natives.yaml (Linux build/verify/upload) + verify arrays in pre-merge/publish-maven
kdroidFilter and others added 12 commits April 16, 2026 01:02
…macOS

- Add BatteryInfo data class with charge state, capacity, voltage, temperature, and cycle count
- Add BatteryState enum: Charging, Discharging, Full, Unknown
- Implement macOS native battery API via IOKit IOPMPowerSource with Apple Silicon support
- Add Linux and Windows stub implementations (return null)
- Add BatteryPanel to demo app with charge, capacity, electrical, and device sections
- Update system-info-demo sidebar to show battery status with colored progress bar

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implement battery reading from /sys/class/power_supply/ sysfs, following
rust-battery's approach. Supports both energy-based (µWh) and charge-based
(µAh) batteries with proper unit conversions. Detects AC adapter via Mains
power supply type. Calculates time remaining from power draw with sanity
checks. Mirrors macOS implementation in Kotlin layer.
Queries battery state, capacity, voltage, temperature, and time-to-full/empty
using Windows Battery IOCTL API (SetupDi device enumeration + DeviceIoControl).
Logic and thresholds match rust-battery for consistency across platforms.

- Enumerate battery devices via GUID_DEVCLASS_BATTERY
- Query BATTERY_INFORMATION and BATTERY_STATUS
- Convert mWh to mAh, decikelvin to Celsius, rate to amperage
- Cap time calculations: 10 days discharge, 10 hours charge
- Skip relative-capacity batteries (same as macOS/Linux)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…de on GNOME

Only apply alpha reduction to border color in dark mode on Linux. In light mode,
the reduced alpha makes the border invisible against light backgrounds.
Implement ConnectivityInfo model with isConnected and meteredStatus detection.

Windows: INetworkListManager for connectivity, INetworkCostManager for metered status.
macOS/Linux: noop stubs (returns null/false) for future implementation.

Add connectivity info to demo app:
- New "Connectivity" card in Network panel showing Connected/Metered status
- Updated Overview Network section with connectivity summary
- Sidebar Network item shows Connected/Disconnected status

Include new connectivity C modules in native builds (Windows/macOS/Linux).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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