feat(scheduler): OS-level background task scheduling with constraints, typed input data, and testing utilities#196
Merged
kdroidFilter merged 49 commits intomainfrom Apr 16, 2026
Merged
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>
…via NWPathMonitor
…Linux via D-Bus Idle time: X11 XScreenSaver (desktop) and D-Bus (Wayland): - X11: XScreenSaverQueryInfo on default root window - Wayland: org.gnome.Mutter.IdleMonitor (GNOME) and org.freedesktop.ScreenSaver (KDE) - All symbols loaded at runtime via dlopen—no hard compile-time deps Connectivity: xdg-desktop-portal NetworkMonitor (freedesktop spec): - Bus: org.freedesktop.portal.Desktop - GetAvailable() for isConnected, GetMetered() for metered status - Also loaded via dlopen for portability Both follow existing pattern from idle.c and macOS NWPathMonitor implementation.
…isconnected Metered/unmetered is meaningless without connectivity. Skip the native metered query and return NOT_AVAILABLE on all three platforms when isConnected is false.
Add execution-time constraint checking to scheduler module, allowing tasks to declare conditions that must be satisfied before doWork() executes: - NetworkType: NOT_REQUIRED, CONNECTED, UNMETERED - requiresBatteryNotLow (threshold: 15%) - requiresCharging (plugged in) - requiresDeviceIdle (5+ minutes) - minimumStorageBytes (configurable bytes, null = no constraint) Constraints are checked in DesktopBootReceiver after OS wakes the process: - Periodic tasks: silently skipped if constraints unsatisfied - Calendar/on-boot tasks: scheduled retry with backoff Integrate with system-info as transitive api dependency for battery, connectivity, idle time, and disk space queries. Graceful fallback: when SystemInfo APIs return null, constraint is satisfied. Add TestConstraintChecker to scheduler-testing for mocking system state in tests. TestDesktopTaskScheduler now respects constraints: runTask() returns null for skipped periodic tasks, advanceTimeBy() skips when constraints unsatisfied. Persist constraints and task type in TaskMetadataStore (.properties files) via _constraint_* and _taskType keys. Update all three platform schedulers (Linux/Windows/macOS) to save constraints on enqueue(). Update mkdocs scheduler.md with constraints section, usage examples, API tables for Constraints/NetworkType/TestConstraintChecker.
- Remove duplicate KDoc in TestDesktopTaskScheduler - Merge multiple continue guards to single condition - Add @Suppress for MagicNumber on enum with API constants - ktlintFormat and general code style cleanup
Replace Map<String, String> inputData with a typed TaskData class that provides: - Typed accessors (getString, getInt, getLong, getBoolean, getDouble) - No silent nulls or manual parsing in tasks - Builder DSL for ergonomic enqueue-time configuration - Backward-compatible equals/hashCode and operator get Update all usages across scheduler and testing modules, documentation, and fix a pre-existing smart cast issue in TestDesktopTaskScheduler.
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
Adds 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
schedulerscheduler-testingservice-management-macosscheduler-demoservice-management-demoExtended existing module:
system-infoPlatform implementations
~/.config/systemd/user/)libnucleus_scheduler_linux.so)~/Library/LaunchAgents/)libnucleus_scheduler.dylib)nucleus_scheduler.dll)No admin rights required on any platform. All backends migrated from CLI/shell fallbacks to native JNI.
API surface —
schedulerDesktopTaskScheduler— facade:enqueue(),cancel(),cancelAll(),isScheduled(),getTaskInfo(),getAllTasks()TaskRequest— immutable config withperiodic(),calendar(),onBoot()factory methods + DSLDesktopTask—suspend doWork(context): TaskResult(Success / Failure / Retry)TaskData— typed key-value input wrapper (getString,getInt,getLong,getBoolean,getDouble); backed by.propertiesfile, survives process restartsConstraints— WorkManager-style pre-execution guards:requiredNetworkType(NOT_REQUIRED / CONNECTED / UNMETERED),requiresBatteryNotLow,requiresCharging,requiresDeviceIdle,minimumStorageBytesCronExpression— helpers:everyDayAt(),everyWeekdayAt(),everyHour()RetryPolicy—ExponentialBackoff/Linearwith configurable attemptsDesktopBootReceiver— detects scheduler invocations inmain()argsTaskRegistry— maps task IDs to factoriesConstraint evaluation happens at runtime inside the app process — the OS still triggers on schedule, but
doWork()is only called when all constraints are satisfied. Unsatisfied periodic tasks are silently skipped; calendar/boot tasks retry with backoff.API surface —
scheduler-testingTestDesktopTaskScheduler— in-memory scheduler; no OS state, deterministicTestTaskRunner— executes tasks synchronously, captures resultsTestConstraintChecker— injectable stub for simulating constraint failuresExecutionRecordhistory for asserting scheduling behaviorAPI surface —
service-management-macosAppServiceManager—register(),unregister(),status(),openSystemSettingsLoginItems()AppServicesealed class —MainApp,LoginItem,Agent,DaemonlaunchAgents { agent("label") { ... } }for bundling plistssystem-info additions
isLowflag — macOS IOKit, Linux sysfs, Windows IOCTLorg.freedesktop.NetworkManager), WindowsHIDIdleTime), Linux D-Bus (org.freedesktop.ScreenSaver)NOT_AVAILABLEmetered status returned when network is disconnectedTest plan
./gradlew :scheduler:buildpasses./gradlew :scheduler-testing:testpasses./gradlew :scheduler-demo:buildpasses./gradlew :service-management-macos:buildpasses./gradlew :system-info:buildpasses.timer+.servicecreated in~/.config/systemd/user/systemctl --user list-timersshows scheduled task~/Library/LaunchAgents/taskschd.msc)TaskData: values survive process restart, typed accessors return correct typesDesktopBootReceiver.isSchedulerInvocation()correctly detects scheduler args