[Request]: Surface lastExitCode on ContainerSnapshot
- Dominant language
- Swift
- Stars
- 49.9k
- Forks
- 1.8k
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 22
Description
> _Suggested triage: type=`Feature`, label=`enhancement`. (External contributor — cannot self-apply.)_
### Feature or enhancement request details
#### Summary
Add an optional `lastExitCode: Int32?` field to `ContainerSnapshot`, populated from the existing `ExitMonitor` callback when a container transitions to `.stopped`.
A draft PR with the implementation is open at #1503.
#### Motivation
External orchestrators that drive the API server (the canonical use case is a Compose-spec orchestrator implementing `depends_on: condition: service_completed_successfully`) need to distinguish between *clean* exit (exit code 0) and *failed* exit (non-zero exit code) of a one-shot container.
Today `ContainerSnapshot` exposes `RuntimeStatus.stopped`, but the underlying exit code is not surfaced. The orchestrator can observe \"the container stopped\" but not \"the container stopped successfully\". This forces consumers to treat `.stopped` as a success, silently misinterpreting non-zero exits.
The same data already exists in the daemon's path: `ContainersService.handleContainerExit(id:code:context:)` receives `code: ExitStatus?` from the existing `ExitMonitor` callback wiring. The proposed change just stamps that value onto the snapshot.
#### Proposed change
```swift
// Sources/ContainerResource/Container/ContainerSnapshot.swift
public var lastExitCode: Int32?
public init(
configuration: ContainerConfiguration,
status: RuntimeStatus,
networks: [Attachment],
startedDate: Date? = nil,
lastExitCode: Int32? = nil
) { ... }
```
```swift
// Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift
// In handleContainerExit, terminal-state block:
state.snapshot.status = .stopped
state.snapshot.lastExitCode = code?.exitCode // NEW
state.snapshot.networks = []
```
#### Wire compatibility
`ContainerSnapshot` is marshaled as `Codable` JSON over XPC. Adding an optional field is forward-compatible:
- Older clients reading from a newer server: ignore the new key.
- Newer clients reading from an older server: decode `lastExitCode` as `nil` (the documented \"never exited or exit not captured\" case).
#### Scope
Intentionally minimal — *in-memory only*. The exit code lives in the in-memory `ContainerState` snapshot for the duration of API server uptime. A daemon restart resets all snapshots to `.stopped` without exit codes (existing behavior). Bundle persistence for survival across daemon restarts (e.g. an `exit_status.json`) is a deliberate out-of-scope follow-up.
#### Use case context
This is a straightforward upstream-ing of a small change we have been carrying in a downstream fork to power Compose-style orchestration on top of `apple/container` ([container-compose](https://github.com/full-chaos/container-compose)). We would much prefer to consume the field directly from `apple/container`'s `ContainerSnapshot` rather than maintain a fork. We have existing internal coverage of this change against a real `apple/container` daemon; happy to share notes.
### Code of Conduct
I agree to follow this project's Code of Conduct.
Contributor guide
Research direction
Start with Sources/ContainerResource/Container/ContainerSnapshot.swift and Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift, then trace handleContainerExit and the existing ExitMonitor callback. Done means the optional exit code is exposed on stopped snapshots while preserving nil for uncaptured exits and Codable compatibility; coordinate with draft PR #1503.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- swift
- Domain
- api, backend
- Issue type
- Feature
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 30/100