Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
57 changes: 57 additions & 0 deletions docs/cli/commands.json
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,63 @@
"hidden_aliases": [],
"examples": []
},
"restart": {
"full_cmd": [
"restart"
],
"usage": "restart [-a --all] [-q --quiet] [ID]…",
"subcommands": {},
"args": [
{
"name": "ID",
"usage": "[ID]…",
"help": "ID of the daemon(s) to restart",
"help_first_line": "ID of the daemon(s) to restart",
"required": false,
"double_dash": "Optional",
"var": true,
"hide": false
}
],
"flags": [
{
"name": "all",
"usage": "-a --all",
"help": "Restart all running daemons",
"help_first_line": "Restart all running daemons",
"short": [
"a"
],
"long": [
"all"
],
"hide": false,
"global": false
},
{
"name": "quiet",
"usage": "-q --quiet",
"help": "Suppress startup log output",
"help_first_line": "Suppress startup log output",
"short": [
"q"
],
"long": [
"quiet"
],
"hide": false,
"global": false
}
],
"mounts": [],
"hide": false,
"help": "Restarts a daemon (stops then starts it)",
"help_long": "Restarts a daemon (stops then starts it)\n\nSends SIGTERM to stop the daemon, then starts it again from the\npitchfork.toml configuration.\n\nExamples:\n pitchfork restart api Restart a single daemon\n pitchfork restart api worker Restart multiple daemons\n pitchfork restart --all Restart all running daemons",
"name": "restart",
"aliases": [],
"hidden_aliases": [],
"examples": []
},
"run": {
"full_cmd": [
"run"
Expand Down
1 change: 1 addition & 0 deletions docs/cli/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
- [`pitchfork enable <ID>`](/cli/enable.md)
- [`pitchfork list [--hide-header]`](/cli/list.md)
- [`pitchfork logs [FLAGS] [ID]…`](/cli/logs.md)
- [`pitchfork restart [-a --all] [-q --quiet] [ID]…`](/cli/restart.md)
- [`pitchfork run [FLAGS] <ID> [-- RUN]…`](/cli/run.md)
- [`pitchfork start [FLAGS] [ID]…`](/cli/start.md)
- [`pitchfork status <ID>`](/cli/status.md)
Expand Down
30 changes: 30 additions & 0 deletions docs/cli/restart.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<!-- @generated by usage-cli from usage spec -->
# `pitchfork restart`

- **Usage**: `pitchfork restart [-a --all] [-q --quiet] [ID]…`

Restarts a daemon (stops then starts it)

Sends SIGTERM to stop the daemon, then starts it again from the
pitchfork.toml configuration.

Examples:
pitchfork restart api Restart a single daemon
pitchfork restart api worker Restart multiple daemons
pitchfork restart --all Restart all running daemons

## Arguments

### `[ID]…`

ID of the daemon(s) to restart

## Flags

### `-a --all`

Restart all running daemons

### `-q --quiet`

Suppress startup log output
6 changes: 6 additions & 0 deletions pitchfork.usage.kdl
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ cmd logs help="Displays logs for daemon(s)" {
}
arg "[ID]…" help="Show only logs for the specified daemon(s)" required=#false var=#true
}
cmd restart help="Restarts a daemon (stops then starts it)" {
long_help "Restarts a daemon (stops then starts it)\n\nSends SIGTERM to stop the daemon, then starts it again from the\npitchfork.toml configuration.\n\nExamples:\n pitchfork restart api Restart a single daemon\n pitchfork restart api worker Restart multiple daemons\n pitchfork restart --all Restart all running daemons"
flag "-a --all" help="Restart all running daemons"
flag "-q --quiet" help="Suppress startup log output"
arg "[ID]…" help="ID of the daemon(s) to restart" required=#false var=#true
}
cmd run help="Runs a one-off daemon" {
alias r
long_help "Runs a one-off daemon\n\nRuns a command as a managed daemon without needing a pitchfork.toml.\nThe daemon is tracked by pitchfork and can be monitored with 'pitchfork status'.\n\nExamples:\n pitchfork run api -- npm run dev\n Run npm as daemon named 'api'\n pitchfork run api -f -- npm run dev\n Force restart if 'api' is running\n pitchfork run api --retry 3 -- ./server\n Restart up to 3 times on failure\n pitchfork run api -d 5 -- ./server\n Wait 5 seconds for ready check\n pitchfork run api -o 'Listening' -- ./server\n Wait for output pattern before ready\n pitchfork run api --http http://localhost:8080/health -- ./server\n Wait for HTTP endpoint to return 2xx\n pitchfork run api --port 8080 -- ./server\n Wait for TCP port to be listening"
Expand Down
3 changes: 3 additions & 0 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ mod disable;
mod enable;
mod list;
pub mod logs;
mod restart;
mod run;
mod start;
mod status;
Expand Down Expand Up @@ -39,6 +40,7 @@ enum Commands {
Enable(enable::Enable),
List(list::List),
Logs(logs::Logs),
Restart(restart::Restart),
Run(run::Run),
Start(start::Start),
Status(status::Status),
Expand All @@ -62,6 +64,7 @@ pub async fn run() -> Result<()> {
Commands::Enable(enable) => enable.run().await,
Commands::List(list) => list.run().await,
Commands::Logs(logs) => logs.run().await,
Commands::Restart(restart) => restart.run().await,
Commands::Run(run) => run.run().await,
Commands::Start(start) => start.run().await,
Commands::Status(status) => status.run().await,
Expand Down
216 changes: 216 additions & 0 deletions src/cli/restart.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
use crate::Result;
use crate::cli::logs::print_startup_logs;
use crate::daemon::RunOptions;
use crate::ipc::client::IpcClient;
use crate::pitchfork_toml::PitchforkToml;
use chrono::{DateTime, Local};
use miette::ensure;
use std::sync::Arc;

/// Restarts a daemon (stops then starts it)
#[derive(Debug, clap::Args)]
#[clap(
verbatim_doc_comment,
long_about = "\
Restarts a daemon (stops then starts it)

Sends SIGTERM to stop the daemon, then starts it again from the
pitchfork.toml configuration.

Examples:
pitchfork restart api Restart a single daemon
pitchfork restart api worker Restart multiple daemons
pitchfork restart --all Restart all running daemons"
)]
pub struct Restart {
/// ID of the daemon(s) to restart
id: Vec<String>,
/// Restart all running daemons
#[clap(long, short)]
all: bool,
/// Suppress startup log output
#[clap(short, long)]
quiet: bool,
}

impl Restart {
pub async fn run(&self) -> Result<()> {
ensure!(
self.all || !self.id.is_empty(),
"You must provide at least one daemon to restart, or use --all"
);

let pt = PitchforkToml::all_merged();
let ipc = Arc::new(IpcClient::connect(true).await?);

// Determine which daemons to restart
let ids: Vec<String> = if self.all {
// Get all running daemons
let active = ipc.active_daemons().await?;
active
.into_iter()
.filter(|d| d.pid.is_some())
.map(|d| d.id)
.collect()
} else {
self.id.clone()
};
Comment thread
cursor[bot] marked this conversation as resolved.

if ids.is_empty() {
info!("No daemons to restart");
return Ok(());
}

// Stop all daemons first
for id in &ids {
if let Err(e) = ipc.stop(id.clone()).await {
warn!("Failed to stop daemon {}: {}", id, e);
}
Comment thread
cursor[bot] marked this conversation as resolved.
}

// Brief delay to allow processes to terminate
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;

// Start all daemons
let disabled_daemons = ipc.get_disabled_daemons().await?;
let mut tasks = Vec::new();

for id in ids {
if disabled_daemons.contains(&id) {
warn!("Daemon {} is disabled, skipping start", id);
continue;
}

let daemon_data = match pt.daemons.get(&id) {
Some(d) => {
let run = d.run.clone();
let auto_stop = d
.auto
.contains(&crate::pitchfork_toml::PitchforkTomlAuto::Stop);
let dir = d
.path
.as_ref()
.and_then(|p| p.parent())
.map(|p| p.to_path_buf())
.unwrap_or_default();
let cron_schedule = d.cron.as_ref().map(|c| c.schedule.clone());
let cron_retrigger = d.cron.as_ref().map(|c| c.retrigger);
let retry = d.retry;
let ready_delay = d.ready_delay;
let ready_output = d.ready_output.clone();
let ready_http = d.ready_http.clone();
let ready_port = d.ready_port;

(
run,
auto_stop,
dir,
cron_schedule,
cron_retrigger,
retry,
ready_delay,
ready_output,
ready_http,
ready_port,
)
}
None => {
warn!("Daemon {} not found in config, skipping", id);
continue;
}
};

let (
run,
auto_stop,
dir,
cron_schedule,
cron_retrigger,
retry,
ready_delay,
ready_output,
ready_http,
ready_port,
) = daemon_data;

let ipc_clone = ipc.clone();

let task = tokio::spawn(async move {
let cmd = match shell_words::split(&run) {
Ok(c) => c,
Err(e) => {
error!("Failed to parse command for daemon {}: {}", id, e);
return (id, None, Some(1));
}
};

let start_time = Local::now();

let exit_code = match ipc_clone
.run(RunOptions {
id: id.clone(),
cmd,
shell_pid: None,
force: false,
autostop: auto_stop,
dir,
cron_schedule,
cron_retrigger,
retry,
retry_count: 0,
ready_delay: ready_delay.or(Some(3)),
ready_output,
ready_http,
ready_port,
wait_ready: true,
})
.await
{
Ok((_started, exit_code)) => exit_code,
Err(e) => {
error!("Failed to start daemon {}: {}", id, e);
Some(1)
}
};

(id, Some(start_time), exit_code)
});

tasks.push(task);
}

// Wait for all tasks to complete
let mut any_failed = false;
let mut successful_daemons: Vec<(String, DateTime<Local>)> = Vec::new();

for task in tasks {
match task.await {
Ok((id, start_time, exit_code)) => {
if exit_code.is_some() {
any_failed = true;
} else if let Some(start_time) = start_time {
successful_daemons.push((id, start_time));
}
Comment thread
cursor[bot] marked this conversation as resolved.
}
Err(e) => {
error!("Task panicked: {}", e);
any_failed = true;
}
}
}

// Show startup logs for successful daemons (unless --quiet)
if !self.quiet {
for (id, start_time) in successful_daemons {
if let Err(e) = print_startup_logs(&id, start_time) {
debug!("Failed to print startup logs for {}: {}", id, e);
}
}
}

if any_failed {
std::process::exit(1);
}
Ok(())
}
}