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
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 written to `/tmp/git-supervisor.pid` at startup and removed on exit. On bare metal:

```bash
kill -SIGUSR1 $(cat /tmp/git-supervisor.pid)
```

In Docker:

```bash
docker kill --signal=SIGUSR1 <container_name>
```

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`,
Expand Down
3 changes: 3 additions & 0 deletions deployment/build/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 <container>
STOPSIGNAL SIGTERM
CMD ["/bin/sh", "-c", "exec /git-supervisor watch --interval \"${TIMER_INTERVAL:-120}\" --webhook-port \"${WEBHOOK_PORT:-9870}\""]
61 changes: 52 additions & 9 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
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};

pub mod config;
pub mod console;
Expand All @@ -26,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('\'', "'\\''")
}
Expand Down Expand Up @@ -273,10 +297,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);
Expand Down Expand Up @@ -451,10 +476,21 @@ 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")?;

let _pid_guard = PidFile::create();
console::log_info(format!(
"watch: pid {} written to {} (send SIGUSR1 to trigger refresh)",
std::process::id(),
PID_FILE,
));

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");
Expand All @@ -473,10 +509,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() {
Expand All @@ -485,9 +521,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")
}
}
};
Expand All @@ -502,8 +544,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?;
Expand Down