File size: 4,845 Bytes
f778c12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import { GATEWAY_SERVER_CAPS } from "../../../packages/gateway-protocol/src/index.js";
import { theme } from "../../../packages/terminal-core/src/theme.js";
import { refreshLegacySystemdServiceMetadata } from "../../daemon/systemd.js";
import { callGatewayCli } from "../../gateway/call.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { resolveGatewayServiceMutationError } from "../../infra/gateway-supervision.js";
import type { SafeGatewayRestartRequestResult } from "../../infra/restart-coordinator.js";
import type { GatewayRestartIntent } from "../../infra/restart-intent.js";
import { defaultRuntime, writeRuntimeJson } from "../../runtime.js";
import { parseDurationMs } from "../parse-duration.js";
import { appendGatewayLifecycleAudit } from "./lifecycle-audit.js";
import type { DaemonLifecycleOptions } from "./types.js";

const SAFE_RESTART_METADATA_REFRESH_TIMEOUT_MS = 5_000;

function formatSafeRestartWarnings(result: SafeGatewayRestartRequestResult): string[] | undefined {
  return result.preflight.blockers.length === 0 ? undefined : [result.preflight.summary];
}

export function resolveGatewayRestartIntentOptions(
  opts: DaemonLifecycleOptions,
): GatewayRestartIntent | undefined {
  if (opts.force && opts.wait !== undefined) {
    throw new Error("--force cannot be combined with --wait");
  }
  if (opts.force) {
    return { force: true };
  }
  return opts.wait === undefined ? undefined : { waitMs: parseDurationMs(opts.wait) };
}

/** Request an OpenClaw-aware restart through the running Gateway. */
type SafeRestartTarget = { pid: number; ownerId: string; port: number };

export async function runSafeGatewayRestart(
  opts: DaemonLifecycleOptions,
  target?: SafeRestartTarget,
): Promise<boolean> {
  if (opts.force) {
    throw new Error("--safe cannot be combined with --force; omit --safe to force restart now");
  }
  if (opts.wait !== undefined) {
    throw new Error("--safe cannot be combined with --wait; safe restart uses gateway deferral");
  }
  const skipDeferral = opts.skipDeferral === true;
  const params: {
    reason: string;
    safe?: true;
    skipDeferral?: true;
    target?: SafeRestartTarget;
  } = { reason: "gateway.restart.safe" };
  if (target) {
    params.safe = true;
    params.target = {
      pid: target.pid,
      ownerId: target.ownerId,
      port: target.port,
    };
  }
  if (skipDeferral) {
    params.skipDeferral = true;
  }
  if (process.platform === "linux") {
    const reportRefreshError = (error: unknown) => {
      defaultRuntime.error(
        theme.warn(
          `Warning: legacy systemd metadata was not refreshed: ${formatErrorMessage(error)}`,
        ),
      );
    };
    const mutationError = resolveGatewayServiceMutationError(
      "refresh legacy systemd service metadata",
      process.env,
    );
    if (mutationError) {
      reportRefreshError(mutationError);
    } else {
      // Definition maintenance is best effort. Keep a wedged systemd manager from
      // suppressing the separately bounded Gateway restart request below.
      await refreshLegacySystemdServiceMetadata(
        process.env,
        SAFE_RESTART_METADATA_REFRESH_TIMEOUT_MS,
      ).catch(reportRefreshError);
    }
  }
  const result = await callGatewayCli<SafeGatewayRestartRequestResult>({
    method: "gateway.restart.request",
    params,
    ...(target
      ? {
          ignoreEnvUrlOverride: true,
          localPortOverride: target.port,
          requiredCapabilities: [GATEWAY_SERVER_CAPS.GATEWAY_RESTART_TARGET_SAFE],
        }
      : {}),
    timeoutMs: 10_000,
  });
  if (target && result.restart.pid !== target.pid) {
    throw new Error("invalid safe restart acknowledgement");
  }
  appendGatewayLifecycleAudit({
    action: "restart",
    source: "safe-rpc",
    mode: result.status,
    pid: result.restart.pid,
  });
  const message =
    result.status === "coalesced"
      ? "safe restart request joined an existing pending gateway restart"
      : result.status === "deferred"
        ? "safe restart requested; gateway will restart after active work drains " +
          "(bounded wait; may force after the timeout expires)"
        : skipDeferral
          ? "safe restart requested; gateway bypassing active-work deferral; " +
            "shutdown may still wait for pending replies to drain"
          : "safe restart requested; gateway will restart momentarily";
  const payload = {
    ok: true,
    result: result.status,
    message,
    preflight: result.preflight,
    restart: result.restart,
    warnings: formatSafeRestartWarnings(result),
  };
  if (opts.json) {
    writeRuntimeJson(defaultRuntime, payload);
  } else {
    defaultRuntime.log(message);
    if (result.preflight.blockers.length > 0) {
      defaultRuntime.log(theme.warn(result.preflight.summary));
    }
  }
  return true;
}