CLI: Implement exec -d (#852)

Surprised we didn't have this already..
This commit is contained in:
Danny Canter
2025-11-07 12:46:00 -08:00
committed by GitHub
parent c88cd2ef4f
commit 14f1df59bf
4 changed files with 96 additions and 11 deletions
@@ -34,6 +34,9 @@ extension Application {
@OptionGroup
var global: Flags.Global
@Flag(name: .shortAndLong, help: "Run the process and detach from it")
var detach = false
@Argument(help: "Container ID")
var containerId: String
@@ -71,11 +74,24 @@ extension Application {
config.supplementalGroups.append(contentsOf: additionalGroups)
do {
let io = try ProcessIO.create(tty: tty, interactive: stdin, detach: false)
let io = try ProcessIO.create(tty: tty, interactive: stdin, detach: self.detach)
defer {
try? io.close()
}
let process = try await container.createProcess(
id: UUID().uuidString.lowercased(),
configuration: config,
stdio: io.stdio
)
if self.detach {
try await process.start()
try io.closeAfterStart()
print(containerId)
return
}
if !self.processFlags.tty {
var handler = SignalThreshold(threshold: 3, signals: [SIGINT, SIGTERM])
handler.start {
@@ -84,12 +100,6 @@ extension Application {
}
}
let process = try await container.createProcess(
id: UUID().uuidString.lowercased(),
configuration: config,
stdio: io.stdio
)
exitCode = try await io.handleProcess(process: process, log: log)
} catch {
if error is ContainerizationError {
@@ -39,4 +39,72 @@ class TestCLIExecCommand: CLITest {
return
}
}
@Test func testExecDetach() throws {
do {
let name = getTestName()
try doCreate(name: name)
defer {
try? doStop(name: name)
}
try doStart(name: name)
// Run a long-running process in detached mode
let output = try doExec(name: name, cmd: ["sh", "-c", "touch /tmp/detach_test_marker"], detach: true)
let containerIdOutput = output.trimmingCharacters(in: .whitespacesAndNewlines)
try #require(containerIdOutput == name, "exec --detach should print the container ID")
// Verify the detached process is running by checking if we can still exec commands
var lsActual = try doExec(name: name, cmd: ["ls", "/"])
lsActual = lsActual.trimmingCharacters(in: .whitespacesAndNewlines)
try #require(lsActual.contains("tmp"), "container should still be running and accepting exec commands")
// Retry loop to check if the marker file was created by the detached process
var markerFound = false
for _ in 0..<3 {
let (_, _, status) = try run(arguments: [
"exec",
name,
"test", "-f", "/tmp/detach_test_marker",
])
if status == 0 {
markerFound = true
break
}
sleep(1)
}
try #require(markerFound, "marker file should be created by detached process within 3 seconds")
try doStop(name: name)
} catch {
Issue.record("failed to exec with detach in container \(error)")
return
}
}
@Test func testExecDetachProcessRunning() throws {
do {
let name = getTestName()
try doCreate(name: name)
defer {
try? doStop(name: name)
}
try doStart(name: name)
// Run a long-running process in detached mode
let output = try doExec(name: name, cmd: ["sleep", "10"], detach: true)
let containerIdOutput = output.trimmingCharacters(in: .whitespacesAndNewlines)
try #require(containerIdOutput == name, "exec --detach should print the container ID")
// Immediately check if the process is running using ps
var psOutput = try doExec(name: name, cmd: ["ps", "aux"])
psOutput = psOutput.trimmingCharacters(in: .whitespacesAndNewlines)
try #require(psOutput.contains("sleep 10"), "detached process 'sleep 10' should be visible in ps output")
try doStop(name: name)
} catch {
Issue.record("failed to verify detached process is running \(error)")
return
}
}
}
+6 -3
View File
@@ -226,11 +226,14 @@ class CLITest {
}
}
func doExec(name: String, cmd: [String]) throws -> String {
func doExec(name: String, cmd: [String], detach: Bool = false) throws -> String {
var execArgs = [
"exec",
name,
"exec"
]
if detach {
execArgs.append("-d")
}
execArgs.append(name)
execArgs.append(contentsOf: cmd)
let (resp, error, status) = try run(arguments: execArgs)
if status != 0 {
+5 -1
View File
@@ -313,7 +313,7 @@ Executes a command inside a running container. It uses the same process flags as
**Usage**
```bash
container exec [--env <env> ...] [--env-file <env-file> ...] [--gid <gid>] [--interactive] [--tty] [--user <user>] [--uid <uid>] [--workdir <dir>] [--debug] <container-id> <arguments> ...
container exec [--detach] [--env <env> ...] [--env-file <env-file> ...] [--gid <gid>] [--interactive] [--tty] [--user <user>] [--uid <uid>] [--workdir <dir>] [--debug] <container-id> <arguments> ...
```
**Arguments**
@@ -321,6 +321,10 @@ container exec [--env <env> ...] [--env-file <env-file> ...] [--gid <gid>] [--in
* `<container-id>`: Container ID
* `<arguments>`: New process arguments
**Options**
* `-d, --detach`: Run the process and detach from it
**Process Options**
* `-e, --env <env>`: Set environment variables (format: key=value)