From 0c4481cbcd1fe1b00ba48c2751dd9822ecd60ddb Mon Sep 17 00:00:00 2001 From: JF Ding Date: Sun, 12 Apr 2026 14:54:17 +0000 Subject: [PATCH 1/2] send USR1 signal to trigger OOB all refreshing Made-with: Cursor Signed-off-by: JF Ding --- README.md | 18 ++++++++++++++++++ deployment/build/Dockerfile | 3 +++ src/lib.rs | 36 +++++++++++++++++++++++++++--------- 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 347d54d..e8ebe50 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,24 @@ GITHUB_WEBHOOK_SECRET=MY_SECRET git-supervisor watch --webhook-port 8080 If `--webhook-port` is set without a secret, a warning is printed and webhook listening is skipped. +### Triggering refresh via SIGUSR1 + +Send `SIGUSR1` to the running `watch` process to trigger an immediate all-host refresh (same behavior as a webhook push event — skips polling, refreshes every host unconditionally). + +The PID is logged at startup. On bare metal: + +```bash +kill -SIGUSR1 +``` + +In Docker: + +```bash +docker kill --signal=SIGUSR1 +``` + +Multiple signals received while a cycle is already running are coalesced automatically. + ### Local mode (no deployments.yaml) When `watch` cannot find a config file (`--config`, `~/.config/git-supervisor/deployments.yaml`, diff --git a/deployment/build/Dockerfile b/deployment/build/Dockerfile index 6c96296..b466b63 100644 --- a/deployment/build/Dockerfile +++ b/deployment/build/Dockerfile @@ -25,4 +25,7 @@ ENV GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no" ENV TIMER_INTERVAL=120 ENV WEBHOOK_PORT=9870 + +# Send SIGUSR1 to trigger immediate refresh: docker kill --signal=SIGUSR1 +STOPSIGNAL SIGTERM CMD ["/bin/sh", "-c", "exec /git-supervisor watch --interval \"${TIMER_INTERVAL:-120}\" --webhook-port \"${WEBHOOK_PORT:-9870}\""] diff --git a/src/lib.rs b/src/lib.rs index e5c5155..c3caba6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ use anyhow::Context; use std::collections::{HashMap, HashSet}; use std::io::IsTerminal; use std::time::{Duration, Instant}; +use tokio::signal::unix::{signal, SignalKind}; pub mod config; pub mod console; @@ -273,10 +274,11 @@ fn run_cycle( round: u64, first_round: bool, skip_poll: bool, + trigger_label: &str, wh_tag: &str, ) { let (changed_repos, failed_repos) = if skip_poll { - console::log_info(format!("watch: round {} [webhook triggered] refreshing all hosts", round)); + console::log_info(format!("watch: round {} [{} triggered] refreshing all hosts", round, trigger_label)); // Update ref fingerprints so the next timer-triggered round won't // re-detect the same changes and cause a duplicate refresh. let _ = poll_changed_repos(config, last_remote_refs, true); @@ -451,10 +453,19 @@ pub async fn run_watch( let wh_tag = if webhook_rx.is_some() { " [+webhook]" } else { "" }; + let mut sigusr1 = signal(SignalKind::user_defined1()) + .context("failed to register SIGUSR1 handler")?; + + console::log_info(format!( + "watch: pid {} (send SIGUSR1 to trigger refresh)", + std::process::id() + )); + loop { - // For the first iteration, run immediately (no wait) - let skip_poll = if round == 0 { - false + // For the first iteration, run immediately (no wait). + // Each iteration yields (skip_poll, trigger_label) so run_cycle can log the source. + let (skip_poll, trigger_label) = if round == 0 { + (false, "timer") } else { if opts.interval_secs == 0 { console::log_highlight("watch: interval is 0, run once and quit"); @@ -473,10 +484,10 @@ pub async fn run_watch( None => interval, }; - // Wait for either timer or webhook signal, showing a countdown on terminals + // Wait for timer, webhook signal, or SIGUSR1 tokio::select! { _ = countdown_wait(sleep_duration, wh_tag) => { - false // timer-triggered: poll for changes + (false, "timer") } Some(()) = async { match webhook_rx.as_mut() { @@ -485,9 +496,15 @@ pub async fn run_watch( } } => { if std::io::stderr().is_terminal() { - eprint!("\r{:40}\r", ""); // clear countdown line + eprint!("\r{:40}\r", ""); + } + (true, "webhook") + } + _ = sigusr1.recv() => { + if std::io::stderr().is_terminal() { + eprint!("\r{:40}\r", ""); } - true // webhook-triggered: skip polling + (true, "signal") } } }; @@ -502,8 +519,9 @@ pub async fn run_watch( // run_cycle uses std::thread::scope (blocking SSH), so run in spawn_blocking let config_clone = config.clone(); let mut refs = std::mem::take(&mut last_remote_refs); + let trigger_label = trigger_label.to_string(); let returned_refs = tokio::task::spawn_blocking(move || { - run_cycle(&config_clone, &mut refs, round, first_round, skip_poll, wh_tag); + run_cycle(&config_clone, &mut refs, round, first_round, skip_poll, &trigger_label, wh_tag); refs }) .await?; From 33bad2fea4da1a4bdc81f2831dd27e9fc5bf2d0b Mon Sep 17 00:00:00 2001 From: JF Ding Date: Sun, 12 Apr 2026 15:06:22 +0000 Subject: [PATCH 2/2] save PID to pid file under /tmp Made-with: Cursor Signed-off-by: JF Ding --- README.md | 4 ++-- src/lib.rs | 29 +++++++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e8ebe50..98f42ee 100644 --- a/README.md +++ b/README.md @@ -84,10 +84,10 @@ If `--webhook-port` is set without a secret, a warning is printed and webhook li Send `SIGUSR1` to the running `watch` process to trigger an immediate all-host refresh (same behavior as a webhook push event — skips polling, refreshes every host unconditionally). -The PID is logged at startup. On bare metal: +The PID is written to `/tmp/git-supervisor.pid` at startup and removed on exit. On bare metal: ```bash -kill -SIGUSR1 +kill -SIGUSR1 $(cat /tmp/git-supervisor.pid) ``` In Docker: diff --git a/src/lib.rs b/src/lib.rs index c3caba6..a00a2ed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ use anyhow::Context; use std::collections::{HashMap, HashSet}; use std::io::IsTerminal; +use std::path::PathBuf; use std::time::{Duration, Instant}; use tokio::signal::unix::{signal, SignalKind}; @@ -27,6 +28,28 @@ pub struct WatchOpts { /// Embedded check-push.sh script, run on remote with sandbox env. pub const CHECK_PUSH_SCRIPT: &str = include_str!("../core/check-push.sh"); +const PID_FILE: &str = "/tmp/git-supervisor.pid"; + +/// RAII guard: writes PID on creation, removes the file on drop. +struct PidFile(PathBuf); + +impl PidFile { + fn create() -> Self { + let path = PathBuf::from(PID_FILE); + let pid = std::process::id(); + if let Err(e) = std::fs::write(&path, pid.to_string()) { + console::log_warning(format!("failed to write pid file {}: {}", path.display(), e)); + } + Self(path) + } +} + +impl Drop for PidFile { + fn drop(&mut self) { + let _ = std::fs::remove_file(&self.0); + } +} + fn escape_single_quoted(s: &str) -> String { s.replace('\'', "'\\''") } @@ -456,9 +479,11 @@ pub async fn run_watch( let mut sigusr1 = signal(SignalKind::user_defined1()) .context("failed to register SIGUSR1 handler")?; + let _pid_guard = PidFile::create(); console::log_info(format!( - "watch: pid {} (send SIGUSR1 to trigger refresh)", - std::process::id() + "watch: pid {} written to {} (send SIGUSR1 to trigger refresh)", + std::process::id(), + PID_FILE, )); loop {