Skip to content

Commit 13e9f02

Browse files
committed
Refactor opencode update stop helpers
1 parent e51c894 commit 13e9f02

1 file changed

Lines changed: 191 additions & 21 deletions

File tree

packages/cli-rust/src/commands/update.rs

Lines changed: 191 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use opencode_cloud_core::docker::{
2828
};
2929
use serde::Deserialize;
3030
use std::process::Command;
31+
use tokio::time::{Duration, sleep};
3132

3233
/// Arguments for the update command
3334
#[derive(Args)]
@@ -1068,27 +1069,7 @@ pub(crate) async fn cmd_update_opencode(
10681069

10691070
let spinner = CommandSpinner::new_maybe("Updating opencode...", quiet);
10701071

1071-
// Stop opencode processes inside the container
1072-
let stop_cmd = r#"set -euo pipefail
1073-
pkill -f "/opt/opencode/bin/opencode" || true
1074-
pkill -f "opencode-broker" || true
1075-
"#;
1076-
let (stop_output, stop_status) =
1077-
exec_command_with_status(&client, CONTAINER_NAME, vec!["bash", "-lc", stop_cmd])
1078-
.await
1079-
.map_err(|e| anyhow!("Failed to stop opencode processes: {e}"))?;
1080-
if !quiet && !stop_output.trim().is_empty() {
1081-
eprintln!(
1082-
"{} Stop output:\n{}",
1083-
style("[info]").cyan(),
1084-
stop_output.trim()
1085-
);
1086-
}
1087-
if stop_status != 0 {
1088-
return Err(anyhow!(
1089-
"Failed to stop opencode processes (exit {stop_status}).\n{stop_output}"
1090-
));
1091-
}
1072+
stop_opencode_for_update(&client, quiet).await?;
10921073

10931074
let update_script = format!(
10941075
r#"set -euo pipefail
@@ -1193,6 +1174,195 @@ async fn get_current_opencode_commit(client: &DockerClient) -> Option<String> {
11931174
}
11941175
}
11951176

1177+
async fn stop_opencode_for_update(client: &DockerClient, quiet: bool) -> Result<()> {
1178+
let pid1_comm = get_pid1_comm(client).await;
1179+
let is_systemd = pid1_comm == "systemd";
1180+
let (running, pids) = check_opencode_running(client).await?;
1181+
1182+
if is_systemd {
1183+
return stop_opencode_systemd(client, quiet, running).await;
1184+
}
1185+
1186+
if !running {
1187+
print_already_stopped(quiet);
1188+
return Ok(());
1189+
}
1190+
1191+
stop_opencode_non_systemd(client, quiet, &pid1_comm, pids).await
1192+
}
1193+
1194+
async fn stop_opencode_systemd(client: &DockerClient, quiet: bool, running: bool) -> Result<()> {
1195+
if !running {
1196+
print_already_stopped(quiet);
1197+
return Ok(());
1198+
}
1199+
1200+
let (output, status) = exec_command_with_status(
1201+
client,
1202+
CONTAINER_NAME,
1203+
vec![
1204+
"bash",
1205+
"-lc",
1206+
"systemctl stop opencode.service opencode-broker.service",
1207+
],
1208+
)
1209+
.await
1210+
.map_err(|e| anyhow!("Failed to stop opencode services via systemd: {e}"))?;
1211+
1212+
if status != 0 {
1213+
warn_systemd_stop_failed(quiet, status, &output);
1214+
}
1215+
1216+
Ok(())
1217+
}
1218+
1219+
async fn stop_opencode_non_systemd(
1220+
client: &DockerClient,
1221+
quiet: bool,
1222+
pid1_comm: &str,
1223+
initial_pids: String,
1224+
) -> Result<()> {
1225+
if pid1_comm.contains("opencode") {
1226+
warn_pid1_skip(quiet);
1227+
return Ok(());
1228+
}
1229+
1230+
let stop_output = stop_opencode_processes(client).await?;
1231+
if let Some(still_pids) = wait_for_opencode_exit(client, initial_pids).await? {
1232+
return Err(build_stop_failure(pid1_comm, &still_pids, &stop_output));
1233+
}
1234+
1235+
Ok(())
1236+
}
1237+
1238+
async fn stop_opencode_processes(client: &DockerClient) -> Result<String> {
1239+
let (output, _status) = exec_command_with_status(
1240+
client,
1241+
CONTAINER_NAME,
1242+
vec![
1243+
"bash",
1244+
"-lc",
1245+
"pkill -TERM -f \"/opt/opencode/bin/opencode\"; pkill -TERM -f \"opencode-broker\"",
1246+
],
1247+
)
1248+
.await
1249+
.map_err(|e| anyhow!("Failed to stop opencode processes: {e}"))?;
1250+
Ok(output)
1251+
}
1252+
1253+
async fn wait_for_opencode_exit(
1254+
client: &DockerClient,
1255+
initial_pids: String,
1256+
) -> Result<Option<String>> {
1257+
let mut last_pids = initial_pids;
1258+
for _ in 0..10 {
1259+
sleep(Duration::from_millis(300)).await;
1260+
let (still_running, current_pids) = check_opencode_running(client).await?;
1261+
if !still_running {
1262+
return Ok(None);
1263+
}
1264+
if !current_pids.is_empty() {
1265+
last_pids = current_pids;
1266+
}
1267+
}
1268+
1269+
Ok(Some(last_pids))
1270+
}
1271+
1272+
fn build_stop_failure(pid1_comm: &str, still_pids: &str, stop_output: &str) -> anyhow::Error {
1273+
let mut msg = format!(
1274+
"Failed to stop opencode processes (init={pid1_comm}). Still running: {still_pids}."
1275+
);
1276+
if !stop_output.trim().is_empty() {
1277+
msg.push_str(&format!("\nOutput:\n{}", stop_output.trim()));
1278+
}
1279+
msg.push_str("\nHint: If opencode is PID 1, stopping it will stop the container.");
1280+
anyhow!(msg)
1281+
}
1282+
1283+
fn print_already_stopped(quiet: bool) {
1284+
if quiet {
1285+
return;
1286+
}
1287+
1288+
eprintln!(
1289+
"{} Opencode is already stopped; it will be restarted after the update.",
1290+
style("Note:").yellow()
1291+
);
1292+
}
1293+
1294+
fn warn_pid1_skip(quiet: bool) {
1295+
if quiet {
1296+
return;
1297+
}
1298+
1299+
eprintln!(
1300+
"{} Opencode is PID 1 in this container. Stopping it would stop the container, so skipping.\n{} The service will be restarted after the update.",
1301+
style("Warning:").yellow().bold(),
1302+
style("Note:").yellow()
1303+
);
1304+
}
1305+
1306+
fn warn_systemd_stop_failed(quiet: bool, status: i64, output: &str) {
1307+
if quiet {
1308+
return;
1309+
}
1310+
1311+
eprintln!(
1312+
"{} Failed to stop opencode via systemd (exit {status}). Continuing update.",
1313+
style("Warning:").yellow().bold()
1314+
);
1315+
if !output.trim().is_empty() {
1316+
eprintln!("{} {}", style("Output:").dim(), output.trim());
1317+
}
1318+
}
1319+
1320+
async fn get_pid1_comm(client: &DockerClient) -> String {
1321+
match exec_command(
1322+
client,
1323+
CONTAINER_NAME,
1324+
vec!["bash", "-lc", "ps -o comm= -p 1"],
1325+
)
1326+
.await
1327+
{
1328+
Ok(output) => {
1329+
let comm = output.lines().next().unwrap_or("").trim();
1330+
if comm.is_empty() {
1331+
"unknown".to_string()
1332+
} else {
1333+
comm.to_string()
1334+
}
1335+
}
1336+
Err(_) => "unknown".to_string(),
1337+
}
1338+
}
1339+
1340+
async fn check_opencode_running(client: &DockerClient) -> Result<(bool, String)> {
1341+
let (output, status) = exec_command_with_status(
1342+
client,
1343+
CONTAINER_NAME,
1344+
vec!["bash", "-lc", "pgrep -f \"/opt/opencode/bin/opencode\""],
1345+
)
1346+
.await
1347+
.map_err(|e| anyhow!("Failed to check opencode process status: {e}"))?;
1348+
1349+
match status {
1350+
0 => {
1351+
let pids: Vec<&str> = output
1352+
.lines()
1353+
.map(str::trim)
1354+
.filter(|l| !l.is_empty())
1355+
.collect();
1356+
let joined = pids.join(" ");
1357+
Ok((!joined.is_empty(), joined))
1358+
}
1359+
1 => Ok((false, String::new())),
1360+
_ => Err(anyhow!(
1361+
"Failed to check opencode process status (exit {status}).\n{output}"
1362+
)),
1363+
}
1364+
}
1365+
11961366
async fn resolve_remote_commit(client: &DockerClient, target_ref: &str) -> Option<String> {
11971367
let output = exec_command(
11981368
client,

0 commit comments

Comments
 (0)