Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
100 changes: 93 additions & 7 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1746,6 +1746,14 @@ trusted_upstream_openshell_gateway_bin_for_service() {
esac
}

supported_openshell_gateway_user_service_candidate_exists() {
upstream_openshell_gateway_user_service_installed && return 0
local service_path
service_path="$(openshell_user_config_home)/systemd/user/${NEMOCLAW_GATEWAY_SERVICE_NAME}.service" \
|| return 1
[ -e "$service_path" ] || [ -L "$service_path" ]
}

inspect_upstream_openshell_gateway_user_service() {
local service_output service_status line fragment_path="" exec_start="" gateway_bin
local fragment_count=0 exec_start_count=0
Expand Down Expand Up @@ -3951,10 +3959,10 @@ stop_macos_openshell_gateway_user_service() {
return 0
}

stop_nemoclaw_openshell_gateway_user_service() {
inspect_nemoclaw_openshell_gateway_user_service() {
[ "$(uname -s)" = "Linux" ] || return 1

local gateway_port service_name service_path fragment_path gateway_bin
local mode="${1:-}" gateway_port service_name service_path fragment_path gateway_bin
gateway_port="$(resolve_nemoclaw_gateway_port)" || return 1
[ "$gateway_port" -eq 8080 ] || return 1
command_exists systemctl || return 1
Expand All @@ -3963,20 +3971,28 @@ stop_nemoclaw_openshell_gateway_user_service() {
service_path="$(openshell_user_config_home)/systemd/user/${service_name}"
[ -f "$service_path" ] || return 1
if [ -L "$service_path" ] || ! [ -O "$service_path" ]; then
error "Refusing to retire the OpenShell gateway from an untrusted user service: ${service_path}"
error "Refusing to control the OpenShell gateway through an untrusted user service: ${service_path}"
fi
is_nemoclaw_openshell_gateway_user_service "$service_path" \
|| error "Refusing to retire the OpenShell gateway from a non-NemoClaw user service: ${service_path}"
systemctl --user is-active --quiet "$service_name" 2>/dev/null || return 1
|| error "Refusing to control the OpenShell gateway through a non-NemoClaw user service: ${service_path}"
if [ "$mode" = "active" ]; then
systemctl --user is-active --quiet "$service_name" 2>/dev/null || return 1
fi

fragment_path="$(systemctl --user show "$service_name" --property=FragmentPath --value 2>/dev/null)" \
|| return 1
[ "$fragment_path" = "$service_path" ] \
|| error "Refusing to retire the OpenShell gateway because the active user service does not match ${service_path}."
|| error "Refusing to control the OpenShell gateway because the user service does not match ${service_path}."
gateway_bin="$(resolve_openshell_gateway_bin_for_user_service "$service_name")" \
|| return 1
trusted_openshell_gateway_bin_for_service "$gateway_bin" \
|| error "Refusing to retire an OpenShell gateway user service with an untrusted binary: ${gateway_bin}"
|| error "Refusing to control an OpenShell gateway user service with an untrusted binary: ${gateway_bin}"
}

stop_nemoclaw_openshell_gateway_user_service() {
local service_name
inspect_nemoclaw_openshell_gateway_user_service active || return 1
service_name="${NEMOCLAW_GATEWAY_SERVICE_NAME}.service"

systemctl --user stop "$service_name" \
|| error "Could not stop the trusted NemoClaw OpenShell gateway user service. Run 'systemctl --user status ${service_name}' for details."
Expand All @@ -3985,6 +4001,76 @@ stop_nemoclaw_openshell_gateway_user_service() {
return 0
}

stop_active_openshell_gateway_user_service() {
local selection_variable="${1:-}" platform inspect_status
[[ "$selection_variable" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] || return 1
platform="$(uname -s)" || return 1

if [ "$platform" = "Darwin" ]; then
if stop_macos_openshell_gateway_user_service; then
printf -v "$selection_variable" '%s' "homebrew:openshell"
return 0
fi
return 1
fi
[ "$platform" = "Linux" ] || return 1
command_exists systemctl || return 1
systemctl --user show-environment >/dev/null || return 2
if ! supported_openshell_gateway_user_service_candidate_exists; then
printf -v "$selection_variable" '%s' "unavailable"
return 0
fi

if inspect_upstream_openshell_gateway_user_service; then
if systemctl --user is-active --quiet openshell-gateway.service 2>/dev/null; then
systemctl --user stop openshell-gateway.service \
|| error "Could not stop the trusted upstream OpenShell gateway user service. Run 'systemctl --user status openshell-gateway.service' for details."
systemctl --user is-active --quiet openshell-gateway.service 2>/dev/null \
&& error "The trusted upstream OpenShell gateway user service remained active after the stop command."
printf -v "$selection_variable" '%s' "systemd:openshell-gateway.service"
return 0
fi
else
inspect_status=$?
[ "$inspect_status" -ne 2 ] || return 2
fi

if stop_nemoclaw_openshell_gateway_user_service; then
printf -v "$selection_variable" '%s' "systemd:${NEMOCLAW_GATEWAY_SERVICE_NAME}.service"
return 0
fi
return 1
}

restart_selected_openshell_gateway_user_service() {
local selection="${1:-}" inspect_status service_name
case "$selection" in
systemd:openshell-gateway.service)
if inspect_upstream_openshell_gateway_user_service; then
service_name="openshell-gateway.service"
else
inspect_status=$?
[ "$inspect_status" -ne 2 ] || return 2
return 1
fi
;;
"systemd:${NEMOCLAW_GATEWAY_SERVICE_NAME}.service")
service_name="${NEMOCLAW_GATEWAY_SERVICE_NAME}.service"
inspect_nemoclaw_openshell_gateway_user_service || return 1
;;
homebrew:openshell)
macos_openshell_homebrew_gateway_service_installed || return 1
brew services restart openshell
return 0
;;
*) return 1 ;;
esac

systemctl --user is-enabled "$service_name" >/dev/null || return 1
systemctl --user daemon-reload
systemctl --user restart "$service_name"
}

preinstall_backup_and_retire_legacy_gateway() {
local reg_file gateway_name
reg_file="$(nemoclaw_state_dir)/sandboxes.json"
Expand Down
123 changes: 102 additions & 21 deletions test/e2e/fixtures/phases/lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,13 @@ const NEMOCLAW_OPENSHELL_INSTALLER = fileURLToPath(
new URL("../../../../scripts/install-openshell.sh", import.meta.url),
);
const USER_SERVICE_STAGE_RESULT_PREFIX = "NEMOCLAW_E2E_GATEWAY_USER_SERVICE=";
const USER_SERVICE_STOP_RESULT_PREFIX = "NEMOCLAW_E2E_STOPPED_GATEWAY_USER_SERVICE=";

type UserServiceStageResult = "upstream" | "existing" | "staged";
type UserServiceSelection =
| "homebrew:openshell"
| "systemd:nemoclaw-openshell-gateway.service"
| "systemd:openshell-gateway.service";

export function buildOpenShellGatewayUserServiceStageScript(): string {
return [
Expand Down Expand Up @@ -123,29 +128,42 @@ export function buildOpenShellGatewayUserServiceRemovalScript(): string {
].join("\n");
}

export function buildOpenShellGatewayUserServiceRestartScript(): string {
export function buildOpenShellGatewayUserServiceStopScript(): string {
return [
"set -eu",
'if [ "$(uname -s)" = Darwin ] && command -v brew >/dev/null 2>&1 && brew list --formula openshell >/dev/null 2>&1; then',
' brew info --json=v2 openshell | grep -Eq \'"tap"[[:space:]]*:[[:space:]]*"nvidia/openshell"\' || exit 1',
" brew services restart openshell",
" exit 0",
"installer=$1",
'if [ ! -f "$installer" ] || [ -L "$installer" ]; then',
' printf "NemoClaw installer is unavailable: %s\\n" "$installer" >&2',
" exit 1",
"fi",
`if ! command -v systemctl >/dev/null 2>&1; then exit ${USER_SERVICE_UNAVAILABLE_EXIT}; fi`,
"service=openshell-gateway",
'if ! systemctl --user cat "$service" >/dev/null 2>&1; then',
' case "${XDG_CONFIG_HOME:-}" in',
' /*) config_home="$XDG_CONFIG_HOME" ;;',
' *) config_home="$HOME/.config" ;;',
'source "$installer"',
"selection=",
"if stop_active_openshell_gateway_user_service selection; then",
' case "$selection" in',
" homebrew:openshell|systemd:nemoclaw-openshell-gateway.service|systemd:openshell-gateway.service|unavailable) ;;",
" *) exit 1 ;;",
" esac",
' unit="$config_home/systemd/user/nemoclaw-openshell-gateway.service"',
` if [ ! -f "$unit" ]; then exit ${USER_SERVICE_UNAVAILABLE_EXIT}; fi`,
` grep -Fxq '${NEMOCLAW_OPENSHELL_GATEWAY_USER_SERVICE_MARKER_LINE}' "$unit" || exit ${USER_SERVICE_UNAVAILABLE_EXIT}`,
" service=nemoclaw-openshell-gateway",
` printf '%s%s\\n' '${USER_SERVICE_STOP_RESULT_PREFIX}' "$selection"`,
" exit 0",
"else",
" status=$?",
` if [ "$status" -eq 1 ]; then exit ${USER_SERVICE_UNAVAILABLE_EXIT}; fi`,
' exit "$status"',
"fi",
'systemctl --user is-enabled "$service" >/dev/null',
"systemctl --user daemon-reload",
'systemctl --user restart "$service"',
].join("\n");
}

export function buildOpenShellGatewayUserServiceRestartScript(): string {
return [
"set -eu",
"installer=$1",
"selection=$2",
'if [ ! -f "$installer" ] || [ -L "$installer" ]; then',
' printf "NemoClaw installer is unavailable: %s\\n" "$installer" >&2',
" exit 1",
"fi",
'source "$installer"',
'restart_selected_openshell_gateway_user_service "$selection"',
].join("\n");
}

Expand Down Expand Up @@ -230,6 +248,7 @@ function instanceName(instance: NemoClawInstance | string): string {
export class LifecyclePhaseFixture {
private postRebootUserServiceStage: UserServiceStageResult | undefined;
private readonly runtimeProvider: RuntimeProviderPrerequisite;
private stoppedOpenShellGatewayUserService: UserServiceSelection | null = null;

constructor(
private readonly host: HostCliClient,
Expand Down Expand Up @@ -417,6 +436,7 @@ export class LifecyclePhaseFixture {
* created one);
* - `docker start` the labeled container so the sandbox returns
* to a usable state for any teardown that expects it live;
* - restart a gateway user service when normal recovery did not;
* - remove a user service staged only for this source-checkout
* fixture after the sandbox cleanup has used it.
*/
Expand Down Expand Up @@ -596,6 +616,8 @@ export class LifecyclePhaseFixture {
timeoutMs: 30_000,
},
);
if (await this.stopOpenShellGatewayUserService()) return runtime;

await this.host.command(
"sh",
["-lc", "command -v openshell >/dev/null 2>&1 && openshell gateway stop -g nemoclaw || true"],
Expand Down Expand Up @@ -664,6 +686,49 @@ export class LifecyclePhaseFixture {
return runtime;
}

private async stopOpenShellGatewayUserService(): Promise<boolean> {
this.stoppedOpenShellGatewayUserService = null;
const result = await this.host.command(
"bash",
[
"-lc",
buildOpenShellGatewayUserServiceStopScript(),
"stop-openshell-gateway-user-service",
NEMOCLAW_INSTALLER,
],
{
artifactName: "lifecycle-gateway-user-service-stop",
env: buildAvailabilityProbeEnv(),
timeoutMs: 120_000,
},
);
if (result.exitCode === 0) {
const match = result.stdout.match(
new RegExp(
`(?:^|\\n)${USER_SERVICE_STOP_RESULT_PREFIX}` +
`(homebrew:openshell|systemd:nemoclaw-openshell-gateway\\.service|systemd:openshell-gateway\\.service|unavailable)(?:\\n|$)`,
"u",
),
);
if (!match) {
throw new Error("OpenShell gateway user service stop did not report its selection.");
}
if (match[1] === "unavailable") return false;
const selection = match[1] as UserServiceSelection;
this.stoppedOpenShellGatewayUserService = selection;
this.cleanup.add(`lifecycle.gateway-user-service-restart:${selection}`, async () => {
if (this.stoppedOpenShellGatewayUserService !== selection) return;
await this.startOpenShellGatewayUserService({ requireAvailable: true });
});
return true;
}
if (result.exitCode === USER_SERVICE_UNAVAILABLE_EXIT) return false;
throw new Error(
`OpenShell gateway user service stop failed during lifecycle qualification: ` +
`${result.stderr || result.stdout || `exit ${String(result.exitCode)}`}`,
);
}

async startGatewayRuntime(
previousRuntime: HostGatewayRuntime | null,
options: { requireUserService?: boolean; sandboxName?: string } = {},
Expand Down Expand Up @@ -703,16 +768,32 @@ export class LifecyclePhaseFixture {
private async startOpenShellGatewayUserService(options: {
requireAvailable?: boolean;
}): Promise<ShellProbeResult | null> {
if (!this.stoppedOpenShellGatewayUserService) {
if (!options.requireAvailable) return null;
throw new Error(
`OpenShell gateway user service is not available for reboot lifecycle recovery.`,
);
}
const selection = this.stoppedOpenShellGatewayUserService;
const result = await this.host.command(
"sh",
["-lc", buildOpenShellGatewayUserServiceRestartScript()],
"bash",
[
"-lc",
buildOpenShellGatewayUserServiceRestartScript(),
"restart-openshell-gateway-user-service",
NEMOCLAW_INSTALLER,
selection,
],
{
artifactName: "lifecycle-gateway-user-service-restart",
env: buildAvailabilityProbeEnv(),
timeoutMs: 120_000,
},
);
if (result.exitCode === 0) return result;
if (result.exitCode === 0) {
this.stoppedOpenShellGatewayUserService = null;
return result;
}
if (result.exitCode === USER_SERVICE_UNAVAILABLE_EXIT && !options.requireAvailable) {
return null;
}
Expand Down
Loading
Loading