feat(scheduler): OS-level background task scheduling#196
Draft
kdroidFilter wants to merge 42 commits intomainfrom
Draft
feat(scheduler): OS-level background task scheduling#196kdroidFilter wants to merge 42 commits intomainfrom
kdroidFilter wants to merge 42 commits intomainfrom
Conversation
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.
…d notification modules
…ng, swallowed exception, return count)
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
…nner and TestDesktopTaskScheduler
…t ExecutionRecord
…nHistory description
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
schedulerservice-management-macosscheduler-demoservice-management-demoPlatform implementations
~/.config/systemd/user/)libnucleus_scheduler_linux.so)~/Library/LaunchAgents/)libnucleus_scheduler.dylib)nucleus_scheduler.dll)No admin rights required on any platform.
API surface
DesktopTaskScheduler— facade:enqueue(),cancel(),cancelAll(),isScheduled(),getTaskInfo(),getAllTasks()TaskRequest— immutable config withperiodic(),calendar(),onBoot()factory methodsDesktopTask—suspend doWork(context): TaskResult(Success / Failure / Retry)CronExpression— helpers:everyDayAt(),everyWeekdayAt(),everyHour(),custom()RetryPolicy—ExponentialBackoff/Linearwith configurable attemptsDesktopBootReceiver— detects scheduler invocations inmain()argsTaskRegistry— maps task IDs to factoriesservice-management-macos API
AppServiceManager—register(),unregister(),status(),openSystemSettingsLoginItems()AppServicesealed class —MainApp,LoginItem,Agent,DaemonlaunchAgents { agent("label") { ... } }for bundling plistsTest plan
./gradlew :scheduler:buildpasses (compilation + ktlint + detekt)./gradlew :scheduler-demo:buildpasses./gradlew :service-management-macos:buildpasses./gradlew :service-management-demo:buildpasses.timer+.servicecreated in~/.config/systemd/user/systemctl --user list-timersshows scheduled task~/Library/LaunchAgents/taskschd.msc)false)DesktopBootReceiver.isSchedulerInvocation()correctly detects scheduler argsTaskResult.Retry