Skip to content

Add --wait to code-server's own CLI - #8117

Open
b84330808 wants to merge 2 commits into
coder:mainfrom
b84330808:cli-wait-flag
Open

b84330808 wants to merge 2 commits into
coder:mainfrom
b84330808:cli-wait-flag

Conversation

@b84330808

Copy link
Copy Markdown

Fixes #8089

As suggested in the issue, this adds --wait to code-server's own script, so code-server --wait --reuse-window works as GIT_EDITOR even when that script is the one on PATH instead of remote-cli. It also works from a shell outside the integrated terminal, through the session socket.

It does what VS Code's remote-cli does: create a marker file, send it as waitMarkerFilePath with the open request, and return once Code deletes it after the files are closed. Like --reuse-window, --wait only opens in an existing instance and errors if there is none. -w is already --welcome-text, so there's no short flag.

Added unit tests for the wait loop (with a fake socket standing in for Code's) and for --wait not falling back to starting a server. I haven't run it against a full build.

Using `code-server --wait --reuse-window` as GIT_EDITOR fails with
"Unknown option --wait" whenever code-server's own script is the one on
PATH rather than the remote-cli script VS Code adds to the integrated
terminal (coder#8089). It also cannot be used as an editor from a shell
outside the integrated terminal.

With --wait, code-server now creates a marker file, passes it to Code
with the open request, and returns once Code deletes it, which Code
does when the opened files are closed. This is what VS Code's remote-cli
does. Like --reuse-window, --wait always opens in an existing instance.
@b84330808
b84330808 requested a review from a team as a code owner November 7, 2026 16:58

@code-asher code-asher left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thank you for the PR!

Comment thread src/node/main.ts Outdated
)
vscode.on("error", (error: unknown) => {
logger.error("got error from Code", field("error", error))
failed = true

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think in the error case we should actually throw the error instead of logging it, so the wait command does not exit with 0 despite the request having failed.

With other commands it would not matter as much if we exit with 0 despite failing to make the request, but I think it will be more important with --wait otherwise git might think the edit was done even though it failed.

I think it could look something like this (untested):

try {
  await new Promise((resolve, reject) => {
    const vscode = http.request(
      {
        path: "/",
        method: "POST",
        socketPath,
      },
      (response) => {
        response.on("data", (message) => {
          logger.debug("got message from Code", field("message", message.toString()))
        })
        response.on("end", resolve)
      },
    )
    vscode.on("error", reject)
    vscode.write(JSON.stringify(pipeArgs))
    vscode.end()
  })
} catch (error) {
  // Try to clean up the marker, but do not let removal errors override the original error.
  if (marker) {
    try {
      await fs.rm(marker)
    } catch (error) {
      if (!isNodeJSErrnoException(error) || error.code !== "ENOENT") {
        logger.error("failed to clean up wait marker", error)
      }
    }
  }
  throw error
}

// Code deletes the marker file once the opened files are closed.
while (marker && await isFile(marker)) {
  await newPromise((resolve) => setTimeout(resolve, 1000))
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done, it now rejects on request errors and rethrows after trying to remove the marker, so --wait exits non-zero. I followed your sketch, so the other commands now wait for the response and fail on errors too.

Comment thread src/node/main.ts Outdated
logger.error("--wait requires at least one file")
process.exit(1)
}
pipeArgs.waitMarkerFilePath = path.join(os.tmpdir(), `code-server-wait-${crypto.randomBytes(8).toString("hex")}`)

@code-asher code-asher Nov 10, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should use tmpdir from constants instead of os.tmpdir() (it is basically the same but uses a code-server sub-directory).

Also maybe we could use generateUuid() from common/util.

So something like marker = path.join(tmpdir, generateUuid(16))

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done: path.join(tmpdir, generateUuid(16)). I also mkdir tmpdir first, since nothing else creates it in the CLI process before this runs.

Comment thread test/unit/node/main.test.ts Outdated
expect((await fs.stat(marker!)).isFile()).toBe(true)

// Still waiting while the file is open.
await new Promise((resolve) => setTimeout(resolve, 1500))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This would add an extra constant 1.5 seconds to the test run time, instead could we perhaps use Jest's timer mocks? jest.useFakeTimers() and jest.runAllTimers() should do the trick I think.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Switched to fake timers. The test runs the poll timer as soon as it's scheduled, so it takes about 25 ms now.


// Code deletes the marker once the file is closed.
await fs.rm(marker!)
await opening

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Similarly here, after removing we would do jest.runAllTimers() again to make it resolve instantly.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done, the removal is followed by running the pending timer.

await fs.rm(pipeArgs.waitMarkerFilePath, { force: true })
}
server.close()
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

There is no need to clean up the file because everything in the tmpdir gets deleted as part of the test cleanup afterward.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Removed the cleanup.

Reject on request errors instead of logging them, so --wait does not exit 0 when Code never got the request, and clean up the marker in that case. Put the marker in code-server's tmpdir with generateUuid(16), and use fake timers in the test.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cannot open files in integrated terminal

2 participants