@@ -720,7 +720,10 @@ async fn run_selected_updates(
720720
721721#[ cfg( test) ]
722722mod tests {
723- use super :: parse_cargo_info_versions;
723+ use super :: {
724+ container_not_running_update_error, ensure_in_place_opencode_update_supported,
725+ non_systemd_in_place_update_error, parse_cargo_info_versions,
726+ } ;
724727
725728 #[ test]
726729 fn parse_cargo_info_versions_latest ( ) {
@@ -745,6 +748,46 @@ mod tests {
745748 assert_eq ! ( parsed. target, "11.0.0" ) ;
746749 assert ! ( parsed. compatible. is_none( ) ) ;
747750 }
751+
752+ #[ test]
753+ fn in_place_update_preflight_allows_systemd ( ) {
754+ assert ! ( ensure_in_place_opencode_update_supported( "systemd" ) . is_ok( ) ) ;
755+ }
756+
757+ #[ test]
758+ fn in_place_update_preflight_allows_unknown ( ) {
759+ assert ! ( ensure_in_place_opencode_update_supported( "unknown" ) . is_ok( ) ) ;
760+ }
761+
762+ #[ test]
763+ fn in_place_update_preflight_rejects_non_systemd ( ) {
764+ let err = ensure_in_place_opencode_update_supported ( "runuser" )
765+ . expect_err ( "runuser init should be rejected" ) ;
766+ let message = err. to_string ( ) ;
767+ assert ! ( message. contains( "runuser" ) ) ;
768+ assert ! ( message. contains( "occ update container" ) ) ;
769+ assert ! ( message. contains( "prebuilt image" ) ) ;
770+ assert ! ( message. contains( "occ config set image_source build" ) ) ;
771+ }
772+
773+ #[ test]
774+ fn non_systemd_error_includes_root_cause_and_remediation ( ) {
775+ let message = non_systemd_in_place_update_error ( "tini" ) . to_string ( ) ;
776+ assert ! ( message. contains( "non-systemd init" ) ) ;
777+ assert ! ( message. contains( "stop the whole container" ) ) ;
778+ assert ! ( message. contains( "occ update container" ) ) ;
779+ assert ! ( message. contains( "prebuilt image" ) ) ;
780+ assert ! ( message. contains( "from source" ) ) ;
781+ assert ! ( message. contains( "systemd-capable" ) ) ;
782+ }
783+
784+ #[ test]
785+ fn container_not_running_error_mentions_non_systemd_hint ( ) {
786+ let message = container_not_running_update_error ( ) . to_string ( ) ;
787+ assert ! ( message. contains( "Non-systemd init" ) ) ;
788+ assert ! ( message. contains( "occ update container" ) ) ;
789+ assert ! ( message. contains( "prebuilt image" ) ) ;
790+ }
748791}
749792
750793async fn cmd_update_cli (
@@ -1070,6 +1113,8 @@ pub(crate) async fn cmd_update_opencode(
10701113 let spinner = CommandSpinner :: new_maybe ( "Updating opencode..." , quiet) ;
10711114
10721115 ensure_container_running_for_update ( & client, & config, quiet) . await ?;
1116+ let pid1_comm = get_pid1_comm ( & client) . await ;
1117+ ensure_in_place_opencode_update_supported ( & pid1_comm) ?;
10731118
10741119 stop_opencode_for_update ( & client, quiet) . await ?;
10751120
@@ -1421,7 +1466,38 @@ fn container_not_running_update_error() -> anyhow::Error {
14211466 anyhow ! (
14221467 "Container is not running; cannot update opencode.\n \
14231468 Run:\n occ start\n \
1424- If it exits immediately, run:\n occ logs"
1469+ If it exits immediately, run:\n occ logs\n \
1470+ Hint: Non-systemd init (for example runuser/tini) can stop the container during in-place updates.\n \
1471+ Alternatives:\n \
1472+ - Update container with prebuilt image (default): occ update container\n \
1473+ - Update container from source (slower):\n occ config set image_source build\n occ update container"
1474+ )
1475+ }
1476+
1477+ fn ensure_in_place_opencode_update_supported ( pid1_comm : & str ) -> Result < ( ) > {
1478+ let normalized = pid1_comm. trim ( ) . to_ascii_lowercase ( ) ;
1479+ if normalized. is_empty ( ) || normalized == "systemd" || normalized == "unknown" {
1480+ return Ok ( ( ) ) ;
1481+ }
1482+
1483+ Err ( non_systemd_in_place_update_error ( pid1_comm) )
1484+ }
1485+
1486+ fn non_systemd_in_place_update_error ( pid1_comm : & str ) -> anyhow:: Error {
1487+ let trimmed = pid1_comm. trim ( ) ;
1488+ let init_name = if trimmed. is_empty ( ) {
1489+ "unknown"
1490+ } else {
1491+ trimmed
1492+ } ;
1493+
1494+ anyhow ! (
1495+ "In-place opencode update is not supported when container init is '{init_name}'.\n \
1496+ This environment is running a non-systemd init, so stopping opencode can stop the whole container.\n \
1497+ Alternatives:\n \
1498+ - Update container with prebuilt image (default): occ update container\n \
1499+ - Update container from source (slower):\n occ config set image_source build\n occ update container\n \
1500+ If you need commit-pinned in-place updates, use a systemd-capable container runtime."
14251501 )
14261502}
14271503
0 commit comments