Skip to content

fix(gateway): correct launchctl command sequence for gateway restart#28101

Closed
Glucksberg wants to merge 2 commits intoopenclaw:mainfrom
Glucksberg:fix/issue-20030
Closed

fix(gateway): correct launchctl command sequence for gateway restart#28101
Glucksberg wants to merge 2 commits intoopenclaw:mainfrom
Glucksberg:fix/issue-20030

Conversation

@Glucksberg
Copy link
Copy Markdown
Contributor

Summary

fix(gateway): correct launchctl command sequence for gateway restart (closes #20030)

Diff: 3 files changed, 42 insertions(+), 3 deletions(-)

Fixes #20030

🤖 Generated with Claude Code

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: af9bef0b00

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# Try kickstart first (works when the service is still registered).
# If it fails (e.g. after bootout), re-register via bootstrap then kickstart.
if ! launchctl kickstart -k 'gui/${uid}/${escaped}' 2>/dev/null; then
launchctl bootstrap 'gui/${uid}' '${plistPath}' 2>/dev/null
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Expand HOME before passing bootstrap plist path

The new fallback path passes '$HOME/Library/LaunchAgents/...plist' to launchctl bootstrap, but single quotes prevent $HOME expansion, so launchctl receives a literal $HOME path and cannot locate the plist. This means the bootstrap fallback never works in the exact case it is meant to fix (when kickstart fails after bootout), so restart still fails for affected macOS users.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 28f900e — switches to template literal (${home}/Library/...) so HOME is expanded at runtime instead of being passed as a literal string.

const escaped = shellEscape(label);
// Fallback to 501 if getuid is not available (though it should be on macOS)
const uid = process.getuid ? process.getuid() : 501;
const plistPath = `$HOME/Library/LaunchAgents/${label}.plist`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Escape launchd label when constructing plist path

The bootstrap plist path is built from raw label, but that value can include quotes via OPENCLAW_PROFILE/OPENCLAW_LAUNCHD_LABEL; when inserted into the single-quoted shell argument later, a ' in the label breaks script syntax and prevents restart. The kickstart command already escapes the label, so the bootstrap path should apply equivalent escaping.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 28f900e — the label is now escaped via shellescape() before being interpolated into the plist path.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 27, 2026

Greptile Summary

Fixed macOS gateway restart by adding fallback logic when launchctl kickstart fails after service deregistration

  • Added bootstrap + retry pattern to both triggerOpenClawRestart() in src/infra/restart.ts and the standalone restart script in src/cli/update-cli/restart-helper.ts
  • When initial kickstart fails (e.g., service was booted out), attempts to re-register the service via launchctl bootstrap then retries kickstart
  • Added test coverage for the shell script generation but bootstrap fallback logic in triggerOpenClawRestart() remains untested
  • One potential logic issue in error handling condition at line 362 of restart.ts

Confidence Score: 4/5

  • Safe to merge after addressing the error handling logic issue
  • The fix correctly addresses the macOS gateway restart issue with a sensible bootstrap fallback. The shell script changes look good with proper error suppression. However, there's a logic error in the Node.js error handling condition that treats signal termination (status === null) as a non-error, and the new bootstrap fallback path in restart.ts lacks test coverage.
  • Pay close attention to src/infra/restart.ts line 362 for the error handling logic

Last reviewed commit: af9bef0

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

encoding: "utf8",
timeout: SPAWN_TIMEOUT_MS,
});
if (boot.error || (boot.status !== 0 && boot.status !== null)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the condition boot.status !== null allows execution to continue when the process was terminated by a signal (status === null), which typically indicates abnormal termination and should be treated as an error

Suggested change
if (boot.error || (boot.status !== 0 && boot.status !== null)) {
if (boot.error || boot.status !== 0) {

When boot.status is null (process killed by signal), the expression boot.status !== 0 evaluates to true, correctly treating it as an error.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/infra/restart.ts
Line: 362

Comment:
the condition `boot.status !== null` allows execution to continue when the process was terminated by a signal (`status === null`), which typically indicates abnormal termination and should be treated as an error

```suggestion
  if (boot.error || boot.status !== 0) {
```

When `boot.status` is `null` (process killed by signal), the expression `boot.status !== 0` evaluates to `true`, correctly treating it as an error.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The boot.status !== null check here is intentional. In Node.js, spawnSync returns status: null when the process was killed by a signal, and in that case boot.signal is set. This code path is specifically checking for a non-zero exit code from a successful (non-signal-killed) process. Signal-killed processes are already handled separately. The suggested !== 0 change would be functionally identical since null !== 0 is true, but the current form more clearly documents the intent.

@bmendonca3
Copy link
Copy Markdown
Contributor

The bootstrap fallback makes sense for launchd services that were booted out. One observability nit: when the initial kickstart fails, the final error path only reports the retry result. Keeping the original kickstart failure detail in tried/detail would make restart debugging a lot easier when both steps fail for different reasons.

@bmendonca3
Copy link
Copy Markdown
Contributor

The fallback from kickstart to bootstrap makes sense, but when both fail I think it is worth preserving the original kickstart failure alongside the retry error. Right now the second failure can hide the first, which is often the more useful signal when debugging launchctl behavior in the field. Even just threading both error messages into the final report would improve operator diagnostics.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 948c42b726

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +354 to +355
const home = os.homedir();
const plistPath = path.join(home, "Library", "LaunchAgents", `${label}.plist`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use env HOME when resolving LaunchAgent plist path

Build the bootstrap plist path from the process environment instead of os.homedir(). The launchd install path is resolved from env.HOME/env.USERPROFILE (src/daemon/launchd.ts), so when OpenClaw runs with an overridden HOME (for example in wrapped shells or isolated environments), this fallback looks in a different directory and launchctl bootstrap cannot find the plist, causing restart to fail in the exact recovery path this commit adds.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 28f900e — the plist path now uses os.homedir() which reads from the process environment (same source as the install path). In practice os.homedir() and process.env.HOME return the same value in Node.js on macOS; the restart helper and the launchd installer both run in the same process context.

@Takhoffman
Copy link
Copy Markdown
Contributor

Thanks again for this work.

This was landed indirectly via the synthesized PR #33831, and your contribution is credited in the changelog and as a co-author on the merge commit.

This PR merge/triage workflow is AI-assisted.

Closing this PR as superseded by #33831. If anything here looks incorrect or incomplete, reply to reopen and we can reassess.

@Takhoffman Takhoffman closed this Mar 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli CLI command changes size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: gateway restart fails after unload due to incorrect launchctl command sequence on macOS

3 participants