Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
24 changes: 17 additions & 7 deletions Sources/ContainerCommands/Container/ContainerExec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
68 changes: 68 additions & 0 deletions Tests/CLITests/Subcommands/Containers/TestCLIExec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
9 changes: 6 additions & 3 deletions Tests/CLITests/Utilities/CLITest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 5 additions & 1 deletion docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,14 +313,18 @@ 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**

* `<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)
Expand Down