Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/botloftd/src/keep_awake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ mod tests {
assert_eq!(*calls.lock().expect("calls"), [true, false]);
}

// Off Windows the helper may be missing (a container); its own test
// covers it there.
#[cfg(windows)]
#[test]
fn a_keep_awake_request_can_be_set_and_cleared() {
let mut request = KeepAwake::new().expect("request");
Expand Down
16 changes: 2 additions & 14 deletions crates/botloftd/src/platform/unix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod job;
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
mod launchd;
mod orphans;
mod power;
#[cfg_attr(target_os = "macos", allow(dead_code))]
mod systemd;

Expand All @@ -23,6 +24,7 @@ pub use self::job::ProcessJob;
#[cfg(target_os = "macos")]
pub use self::launchd::{TASK_KIND, delete_task, find_task, register_task, run_task, stop_task};
pub use self::orphans::track_groups;
pub use self::power::KeepAwake;
#[cfg(not(target_os = "macos"))]
pub use self::systemd::{TASK_KIND, delete_task, find_task, register_task, run_task, stop_task};

Expand Down Expand Up @@ -91,20 +93,6 @@ pub async fn shutdown_signal() {
}
}

/// Nothing keeps a Unix machine awake yet.
#[derive(Debug)]
pub struct KeepAwake;

impl KeepAwake {
pub fn new() -> io::Result<Self> {
Ok(Self)
}

pub fn set(&mut self, _on: bool) -> io::Result<()> {
Ok(())
}
}

/// Nothing to leave: Unix never opens a console for the daemon.
pub fn leave_own_console() {}

Expand Down
115 changes: 115 additions & 0 deletions crates/botloftd/src/platform/unix/power.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
//! Keeps the computer from sleeping while bots work, off Windows (spec
//! 14.1): `caffeinate -i` on macOS, an idle lock of `systemd-inhibit` on
//! Linux, held by a helper process for as long as it is wanted. Like the
//! power request on Windows, it only stops sleep after inactivity: closing
//! the lid or choosing Sleep still works. The helper also ends when the
//! daemon does, so a crash does not keep the computer awake.

use std::io;
use std::process::{Child, Command, Stdio};
use std::time::Duration;

#[cfg_attr(target_os = "macos", allow(dead_code))]
const REASON: &str = "Botloft bots are working";
/// A helper that fails (no logind, say) exits at once; one still running
/// after this holds the lock.
const SETTLE: Duration = Duration::from_millis(200);

#[derive(Debug, Default)]
pub struct KeepAwake {
helper: Option<Child>,
}

impl KeepAwake {
pub fn new() -> io::Result<Self> {
Ok(Self::default())
}

pub fn set(&mut self, on: bool) -> io::Result<()> {
if on == self.running() {
return Ok(());
}
if !on {
self.stop();
return Ok(());
}
let mut helper = helper(std::process::id())
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
std::thread::sleep(SETTLE);
if let Some(status) = helper.try_wait()? {
return Err(io::Error::other(format!(
"the keep-awake helper stopped at once ({status})"
)));
}
self.helper = Some(helper);
Ok(())
}

/// Whether the helper holds the lock now.
fn running(&mut self) -> bool {
self.helper
.as_mut()
.is_some_and(|helper| matches!(helper.try_wait(), Ok(None)))
}

fn stop(&mut self) {
if let Some(mut helper) = self.helper.take() {
let _ = helper.kill();
let _ = helper.wait();
}
}
}

impl Drop for KeepAwake {
fn drop(&mut self) {
self.stop();
}
}

/// `-w` ends `caffeinate` with the daemon.
#[cfg(target_os = "macos")]
fn helper(daemon: u32) -> Command {
let mut command = Command::new("caffeinate");
command.args(["-i", "-w", &daemon.to_string()]);
command
}

/// The lock lasts while the shell it runs does, and the shell watches the
/// daemon.
#[cfg(not(target_os = "macos"))]
fn helper(daemon: u32) -> Command {
let mut command = Command::new("systemd-inhibit");
command.args([
"--what=idle",
"--who=Botloft",
&format!("--why={REASON}"),
"--mode=block",
"sh",
"-c",
&format!("while kill -0 {daemon} 2>/dev/null; do sleep 5; done"),
]);
command
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn the_helper_runs_only_while_wanted() {
let mut awake = KeepAwake::new().expect("keep awake");
if let Err(err) = awake.set(true) {
// No `caffeinate` or no logind here (a container, say).
eprintln!("cannot keep this computer awake ({err}); skipping");
return;
}
assert!(awake.running());
awake.set(true).expect("again");
assert!(awake.running(), "asking twice keeps one helper");
awake.set(false).expect("off");
assert!(!awake.running());
}
}
3 changes: 2 additions & 1 deletion docs/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -711,7 +711,8 @@ Em fatias (18, item 8). O que já vale fora do Windows:
| Iniciar com o sistema (Linux) | `botloftd service install` grava a unit `~/.config/systemd/user/<nome>.service` (`botloft.service` para a pasta padrão, `botloft-<8 hex>.service` para outra, como a tarefa do Windows), com `Type=exec`, `ExecStart` com cada palavra entre aspas (`%` e `$` dobrados), `Restart=on-failure` com `RestartSec=5`, `KillMode=control-group`, `TimeoutStopSec=20` e `WantedBy=default.target`. `start_with_windows` vira `systemctl --user enable` (o gerenciador do usuário a sobe no login) ou `disable`. Depois de uma queda o systemd a traz de volta; um stop pedido (logout, `service stop`) termina com saída limpa e não volta. Por isso não há gatilho de minuto nem conferência de logon novo (`sign_in_id` é sempre vazio): sem `enable`, nada a sobe num login novo. Estado por `is-active` e `is-enabled`, cujas palavras não são traduzidas |
| Iniciar com o sistema (macOS) | Um launch agent em `~/Library/LaunchAgents/io.github.httpsphl.botloft.daemon[-<8 hex>].plist`, no domínio `gui/<uid>`. `RunAtLoad` segue `start_with_windows`. O `KeepAlive` é `PathState` com o arquivo `<home>/run/keep-alive`: o launchd traz o daemon de volta enquanto o arquivo existir. Um `KeepAlive` simples, ou com `SuccessfulExit`, implica rodar no login. O arquivo é criado ao iniciar (`service install`, `restart` e o daemon iniciado pelo agente) e apagado no stop pedido e na saída limpa do daemon (logout incluído). Iniciar recarrega o agente do arquivo (`bootout`, `bootstrap`, com novas tentativas logo depois do `bootout`) e dá `kickstart`; parar apaga o arquivo e manda `SIGTERM` (`launchctl kill`); gravar a escolha só reescreve o arquivo, que o launchd lê no próximo login. Estado por `launchctl print` (`state = running`) |
| Os dois | `service install`, `status`, `restart`, `stop` e `uninstall` iguais aos do Windows; as mensagens dizem "systemd user service" ou "launch agent" no lugar de "scheduled task". Um teste no CI (`tests/service.rs`, só com `BOTLOFT_SERVICE_TEST=1`) instala com o systemd e o launchd de verdade, mata o daemon com `SIGKILL` e o vê voltar, para e confere que não volta, reinicia e desinstala. O runner Ubuntu só tem gerenciador de usuário com `loginctl enable-linger` |
| Não suspender, Lixeira | Ainda não: avisam que só existem no Windows |
| Não suspender | Como no Windows, enquanto houver bot `busy` e `keep_awake = true`, mas com um processo auxiliar que segura o pedido enquanto vive: `caffeinate -i -w <pid do daemon>` no macOS; no Linux, `systemd-inhibit --what=idle --mode=block` com um `sh` que espera o daemon sumir (`kill -0` a cada 5 s). Os dois impedem só a suspensão por inatividade, não a pedida pelo dono (tampa, Suspender). O auxiliar sai quando o daemon morre, então uma queda não deixa o computador acordado. Um auxiliar que sai em 200 ms (sem logind, num contêiner) vira erro no log, e o daemon segue |
| Lixeira | Ainda não: avisa que só existe no Windows |

## 15. App desktop

Expand Down
Loading