@@ -28,6 +28,7 @@ use opencode_cloud_core::docker::{
2828} ;
2929use serde:: Deserialize ;
3030use 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 ! ( "\n Output:\n {}" , stop_output. trim( ) ) ) ;
1278+ }
1279+ msg. push_str ( "\n Hint: 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+
11961366async fn resolve_remote_commit ( client : & DockerClient , target_ref : & str ) -> Option < String > {
11971367 let output = exec_command (
11981368 client,
0 commit comments