///|
/// The reason used when callers supply an empty or whitespace-only reason.
pub let default_reason : String = "MoonBit keepawake request"
///|
priv enum GuardBackend {
Native(NativeGuard)
Linux(@process.Process, @process.WriteToProcess)
}
///|
/// A keep-awake request owned by the task group passed to `acquire`.
/// Explicit release ends it early; leaving the group also releases it.
pub struct Guard {
priv backend : GuardBackend
priv requested_scope : Scope
priv lifetime : @async.Task[Unit]
}
///|
/// Acquires inhibition before returning. The task group owns its lifetime.
/// Linux waits asynchronously for systemd-inhibit to acquire the lock; missing
/// or failing backends raise KeepAwakeError. Cancellation stops and reaps the
/// helper. Callers can impose a startup deadline with `@async.with_timeout`.
/// macOS and Windows retain their native power assertions. On Windows, acquire
/// and release must run on the same OS thread.
pub async fn[G] acquire(
group : @async.TaskGroup[G],
reason~ : String = default_reason,
scope~ : Scope = Scope::PreventSystemSleep,
) -> Guard raise KeepAwakeError {
if @async.platform is Linux {
return acquire_linux(
group,
"systemd-inhibit",
normalize_reason(reason),
scope,
)
}
let handle = finish_acquire(
native_guard_create(
@ffi.to_cstr(normalize_reason(reason)),
scope.native_code(),
),
)
own_guard(group, Native(handle), scope)
}
///|
async fn[G] acquire_linux(
group : @async.TaskGroup[G],
tool : String,
reason : String,
scope : Scope,
) -> Guard raise KeepAwakeError {
let what = match scope {
PreventSystemSleep => "sleep"
PreventDisplaySleep => "idle"
PreventSystemAndDisplaySleep => "idle:sleep"
}
try {
let (stdin, writer) = @process.write_to_process()
let mut acquired = false
defer (if !acquired { writer.close() })
let (reader, stdout) = @process.read_from_process()
defer reader.close()
let child = @process.spawn(
group,
tool,
[
"--what", what, "--mode", "block", "--why", reason, "/bin/sh", "-c", "printf 'ready\\n'; exec /bin/cat >/dev/null",
],
stdin~,
stdout~,
no_wait=true,
cancel_handler=@process.hard_cancel(),
)
let keep_guard = own_guard(group, Linux(child, writer), scope)
defer (if !acquired { keep_guard.release() })
// systemd-inhibit runs our command only after acquiring the lock.
match reader.read_until("\n") {
Some("ready") => ()
None => {
let status = child.wait()
raise OperationFailed(
operation="acquire",
detail="systemd-inhibit exited before readiness (status \{status})",
)
}
Some(_) =>
raise OperationFailed(
operation="acquire",
detail="invalid systemd-inhibit readiness acknowledgement",
)
}
if child.try_wait() is Some(status) {
raise OperationFailed(
operation="acquire",
detail="systemd-inhibit exited at readiness (status \{status})",
)
}
acquired = true
keep_guard
} catch {
BackendUnavailable(..) as error => raise error
OperationFailed(..) as error => raise error
@os_error.OSError(_) as error if error.is_ENOENT() =>
raise BackendUnavailable(detail=error.to_string())
error =>
raise OperationFailed(operation="acquire", detail=error.to_string())
}
}
///|
/// Runs asynchronous work after acquiring inhibition, then releases it on
/// return, error or cancellation. On Linux, unexpected helper exit fails this
/// scope and cancels the work. No manual task group or guard is needed.
pub async fn[T] with_keepawake(
action : async () -> T,
reason~ : String = default_reason,
scope~ : Scope = Scope::PreventSystemSleep,
) -> T {
@async.with_task_group(group => {
let keep_guard = acquire(group, reason~, scope~)
keep_guard.with_action(action)
})
}
///|
async fn[T] Guard::with_action(self : Guard, action : async () -> T) -> T {
run_with_cleanup(
() => {
match self.backend {
Native(_) => action()
Linux(child, _) =>
@async.any([
action,
() => {
let status = child.wait()
raise OperationFailed(
operation="watch",
detail="systemd-inhibit exited while inhibition was required (status \{status})",
)
},
])
}
},
() => self.release(),
)
}
///|
/// Reports whether inhibition remains active. A Linux helper that has exited
/// is inactive even if `release` has not been called. This is a point-in-time
/// observation; `with_keepawake` also monitors helper exit during scoped work.
pub fn Guard::active(self : Guard) -> Bool {
match self.backend {
Native(handle) => native_guard_is_active(handle)
Linux(child, _) => (child.try_wait() catch { _ => Some(-1) }) is None
}
}
///|
/// Returns the requested scope.
pub fn Guard::scope(self : Guard) -> Scope {
match self.backend {
Native(handle) => scope_from_native_code(native_guard_scope_code(handle))
Linux(_, _) => self.requested_scope
}
}
///|
/// Releases inhibition and awaits Linux helper cleanup without blocking the
/// event loop. Safe to call repeatedly and during cancellation cleanup.
/// Failed native release can be retried without cancelling the owning group.
pub async fn Guard::release(
self : Guard,
) -> Unit raise KeepAwakeError + nocancel {
self.release_with(() => self.backend.release())
}
///|
async fn Guard::release_with(
self : Guard,
release : async () -> Unit raise KeepAwakeError + nocancel,
) -> Unit raise KeepAwakeError + nocancel {
// A failed explicit release belongs to the caller. Keep ownership alive so
// it can be retried, including during task-group cleanup.
release()
self.lifetime.cancel()
@async.protect_from_cancel(() => self.lifetime.wait()) catch {
error =>
raise OperationFailed(operation="release", detail=error.to_string())
}
}
///|
fn[G] own_guard(
group : @async.TaskGroup[G],
backend : GuardBackend,
scope : Scope,
) -> Guard {
// Finished tasks leave the group immediately; group defers would retain
// every released guard until application shutdown.
let lifetime = group.spawn(no_wait=true, () => {
defer backend.release()
@async.handle_cancellation(() => {
match backend {
Native(_) => @async.Semaphore(1, initial_value=0).acquire()
Linux(child, _) => ignore(child.wait())
}
})
|> ignore
})
{ backend, requested_scope: scope, lifetime, }
}
///|
async fn GuardBackend::release(
self : GuardBackend,
) -> Unit raise KeepAwakeError + nocancel {
match self {
Native(handle) =>
if native_guard_is_active(handle) {
finish_release(
native_guard_release(handle),
native_guard_last_error(handle),
)
}
Linux(child, writer) => {
writer.close()
child.cancel()
@async.protect_from_cancel(() => ignore(child.wait())) catch {
error =>
raise OperationFailed(operation="release", detail=error.to_string())
}
}
}
}