feat: implement Docker/Podman socket compatibility via launchd activation#8
feat: implement Docker/Podman socket compatibility via launchd activation#8
Conversation
…ssue us#7) - Add `mocker system service` — HTTP/1.1 Docker Engine REST API server over a Unix-domain socket with launchd socket activation and inactivity timeout - Add `mocker socket install/uninstall/status` — manage launchd plist - Implement Docker Engine API: /_ping, /version, /info, /containers/json, /containers/{id}/json, start/stop/remove/create, /images/json, image pull - Add socketPath / launchAgentPlistPath / launchAgentLabel / mockerVersion to MockerConfig - Add DockerAPIServerTests (9 tests) Agent-Logs-Url: https://github.com/kaovilai/mocker/sessions/40690106-b143-4e3b-a558-3fb45ebf1b88 Co-authored-by: kaovilai <11228024+kaovilai@users.noreply.github.com>
Agent-Logs-Url: https://github.com/kaovilai/mocker/sessions/40690106-b143-4e3b-a558-3fb45ebf1b88 Co-authored-by: kaovilai <11228024+kaovilai@users.noreply.github.com>
Agent-Logs-Url: https://github.com/kaovilai/mocker/sessions/27423872-5f03-4575-b6b2-c09dce2971a1 Co-authored-by: kaovilai <11228024+kaovilai@users.noreply.github.com>
Co-authored-by: kaovilai <11228024+kaovilai@users.noreply.github.com>
Co-authored-by: kaovilai <11228024+kaovilai@users.noreply.github.com>
…n CI kill/podman Agent-Logs-Url: https://github.com/kaovilai/mocker/sessions/27423872-5f03-4575-b6b2-c09dce2971a1 Co-authored-by: kaovilai <11228024+kaovilai@users.noreply.github.com>
Agent-Logs-Url: https://github.com/kaovilai/mocker/sessions/27423872-5f03-4575-b6b2-c09dce2971a1 Co-authored-by: kaovilai <11228024+kaovilai@users.noreply.github.com>
…routes, CI tests Agent-Logs-Url: https://github.com/kaovilai/mocker/sessions/27423872-5f03-4575-b6b2-c09dce2971a1 Co-authored-by: kaovilai <11228024+kaovilai@users.noreply.github.com>
…reCaptures error Agent-Logs-Url: https://github.com/kaovilai/mocker/sessions/0b048ed2-3833-4fab-8c9c-d11adcbc2178 Co-authored-by: kaovilai <11228024+kaovilai@users.noreply.github.com>
Agent-Logs-Url: https://github.com/kaovilai/mocker/sessions/073792b3-e384-42c7-bfac-d8875ba963d5 Co-authored-by: kaovilai <11228024+kaovilai@users.noreply.github.com>
Agent-Logs-Url: https://github.com/kaovilai/mocker/sessions/93666e83-ed13-490f-bd74-a0dedc95f1cf Co-authored-by: kaovilai <11228024+kaovilai@users.noreply.github.com>
Agent-Logs-Url: https://github.com/kaovilai/mocker/sessions/798ce0e4-32bd-4d87-b576-b47d5dcf2ffe Co-authored-by: kaovilai <11228024+kaovilai@users.noreply.github.com>
Agent-Logs-Url: https://github.com/kaovilai/mocker/sessions/31299aa0-72b3-4aad-aee6-2d2330b2a6d1 Co-authored-by: kaovilai <11228024+kaovilai@users.noreply.github.com>
… README Agent-Logs-Url: https://github.com/kaovilai/mocker/sessions/48921f21-beae-470d-aec2-bba4406b777e Co-authored-by: kaovilai <11228024+kaovilai@users.noreply.github.com>
…m start Agent-Logs-Url: https://github.com/kaovilai/mocker/sessions/0a3a975a-bb19-4661-83c1-6c3aad0c4ab0 Co-authored-by: kaovilai <11228024+kaovilai@users.noreply.github.com>
… system start Agent-Logs-Url: https://github.com/kaovilai/mocker/sessions/c69bfef9-0220-467b-b27a-4f234573fd52 Co-authored-by: kaovilai <11228024+kaovilai@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR adds a Docker-compatible HTTP API server over a Unix-domain socket (including launchd socket activation on macOS), wires it into the CLI, and documents how to use it with Docker/Podman CLIs. It also extends CI to run end-to-end socket integration checks.
Changes:
- Implement a Docker/Podman-compatible REST API server over a Unix socket with basic Docker + libpod routing and container attach streaming.
- Add CLI entry points for
mocker system serviceandmocker socket (install|uninstall|status), plus config for socket + launchd paths/labels. - Expand documentation and CI with launchd templates and integration tests (curl + Docker CLI + Podman CLI).
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| Tests/MockerKitTests/DockerAPIServerTests.swift | Adds unit tests for API path stripping, response headers, and config fields. |
| Sources/MockerKit/SocketService/DockerAPIServer.swift | Implements the socket-based HTTP server, HTTP parsing/writing, accept loop, and launchd activation helper. |
| Sources/MockerKit/SocketService/DockerAPIHandlers.swift | Implements Docker + libpod route handling and response payload models. |
| Sources/MockerKit/Container/ContainerEngine.swift | Adds runWithOutputCapture used by attach/hijack streaming. |
| Sources/MockerKit/Config/MockerConfig.swift | Adds socket path + launchd plist path/label configuration. |
| Sources/Mocker/MockerCLI.swift | Registers the new socket top-level CLI command. |
| Sources/Mocker/Commands/System.swift | Adds mocker system service command to run the socket API server (launchd-activated or standalone). |
| Sources/Mocker/Commands/Socket.swift | Adds `mocker socket install |
| README.md | Documents socket service usage with Docker/Podman and a launchd template. |
| README.zh-CN.md | Chinese documentation for socket service usage and launchd template. |
| .github/workflows/ci.yml | Adds caching, installs dependencies, and runs socket integration tests; modifies push branch triggers. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Agent-Logs-Url: https://github.com/kaovilai/mocker/sessions/e2b4ddde-d636-4e15-884d-ee4b9ea8832b Co-authored-by: kaovilai <11228024+kaovilai@users.noreply.github.com>
… loop Agent-Logs-Url: https://github.com/kaovilai/mocker/sessions/8b950c4f-38f8-4e2a-8756-8c936277e602 Co-authored-by: kaovilai <11228024+kaovilai@users.noreply.github.com>
Agent-Logs-Url: https://github.com/kaovilai/mocker/sessions/edf5a179-44ce-45ba-bebb-a87e747415de Co-authored-by: kaovilai <11228024+kaovilai@users.noreply.github.com>
…tion
Emit stderr before stdout in the attach handler so that VM startup
progress lines ([0/6] Fetching image…) appear before container output
instead of after.
Add Connection: close to all non-101 HTTP responses. The server handles
one request per connection then closes the fd, but HTTP/1.1 defaults to
keep-alive. Without this header the Docker client reuses idle connections
that mocker has already closed, producing "server closed idle connection"
on the /containers/{id}/start request.
Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
Signed-off-by: Tiger Kaovilai <passawit.kaovilai@gmail.com>
…ility Documents the no-env-var symlink approach (same as Podman Desktop Docker compatibility mode) for both Docker CLI (/var/run/docker.sock) and Podman CLI (~/.local/share/containers/podman/machine/qemu/podman.sock). Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering> Signed-off-by: Tiger Kaovilai <passawit.kaovilai@gmail.com>
Replace runWithOutputCapture in the attach handler with runStreaming, which drains both pipes concurrently with availableData and calls the onOutput callback via a serial queue as chunks arrive. This eliminates the hang users saw while waiting for large image downloads to complete before any output appeared. Ordering (progress before output) is now natural: vz writes startup progress to stderr before the container command produces stdout, so frames arrive in the correct order without manual reordering. runWithOutputCapture is preserved for callers that need buffered data, and is now implemented on top of runStreaming. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering> Signed-off-by: Tiger Kaovilai <passawit.kaovilai@gmail.com>
…odman run --rm
Add GET /libpod/containers/{id}/json so podman can inspect container
state after the container exits. Handles both pending (runStore) and
engine-managed containers.
Fix DELETE /libpod/containers/{id} to return [{"Id":"..."}] JSON array
instead of empty 204. Podman unmarshals the response as []*RmReport;
the empty body caused "unexpected end of JSON input" and a visible
ERRO log when using --rm.
Also add PendingRunStore.getEntry() to expose both config and exit state
to the inspect handler.
Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
Signed-off-by: Tiger Kaovilai <passawit.kaovilai@gmail.com>
Podman unmarshals POST /libpod/containers/{id}/wait as *int32.
The previous response {"Error":{"Message":""},"StatusCode":0} caused
"cannot unmarshal object into Go value of type int32".
Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
Signed-off-by: Tiger Kaovilai <passawit.kaovilai@gmail.com>
Podman logs "Service did not provide Libpod-API-Version Header" when the header is absent. Add it to all JSON and text responses. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering> Signed-off-by: Tiger Kaovilai <passawit.kaovilai@gmail.com>
|
working great |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 11 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <plist version="1.0"> | ||
| <dict> | ||
| <key>Label</key> | ||
| <string>io.mocker.socket</string> |
There was a problem hiding this comment.
The plist template hard-codes the launchd label as io.mocker.socket even though MockerConfig.launchAgentLabel exists. Using the constant here avoids future mismatches between the installed job label and what socket uninstall/status expect.
| <string>io.mocker.socket</string> | |
| <string>\(MockerConfig.launchAgentLabel)</string> |
| // Remove socket file if present | ||
| let sock = config.socketPath | ||
| if FileManager.default.fileExists(atPath: sock) { | ||
| try? FileManager.default.removeItem(atPath: sock) | ||
| } |
There was a problem hiding this comment.
mocker socket install supports --socket-path, but socket uninstall always deletes config.socketPath (the default). If the agent was installed with a custom socket path, uninstall will leave the socket file behind. Consider either adding the same --socket-path option here, or reading the socket path from the installed plist before deleting.
| let config = MockerConfig() | ||
| let sock = config.socketPath | ||
| let plistPath = config.launchAgentPlistPath | ||
| let label = MockerConfig.launchAgentLabel | ||
|
|
||
| let plistExists = FileManager.default.fileExists(atPath: plistPath) | ||
| let sockExists = FileManager.default.fileExists(atPath: sock) | ||
|
|
||
| print("Label: \(label)") | ||
| print("Plist: \(plistPath) [\(plistExists ? "installed" : "not installed")]") | ||
| print("Socket: \(sock) [\(sockExists ? "exists" : "not present")]") | ||
| print("DOCKER_HOST: unix://\(sock)") | ||
|
|
There was a problem hiding this comment.
mocker socket status always reports config.socketPath (the default), even though socket install allows a custom --socket-path. This can lead to misleading status output when a non-default socket path is configured. Consider reading the SockPathName from the installed plist or accepting --socket-path here too.
|
|
||
| // Ensure ~/.mocker directory exists | ||
| try config.ensureDirectories() | ||
|
|
There was a problem hiding this comment.
socket install only ensures ~/.mocker exists via config.ensureDirectories(). If the user passes a custom --socket-path whose parent directory doesn't exist, launchd will fail to create/bind the socket at that path. Consider creating dirname(socketPath) (with intermediate directories) when a custom socket path is provided.
| // Ensure the selected socket directory exists, including for custom paths. | |
| let socketDirectoryURL = URL( | |
| fileURLWithPath: (sock as NSString).expandingTildeInPath | |
| ).deletingLastPathComponent() | |
| try FileManager.default.createDirectory( | |
| at: socketDirectoryURL, | |
| withIntermediateDirectories: true | |
| ) |
| let (_, exitCode) = try await runProcess( | ||
| "/bin/launchctl", | ||
| ["bootstrap", "gui/\(uid)", plistPath] | ||
| ) | ||
|
|
||
| if exitCode == 0 { | ||
| print("Mocker socket agent installed.") | ||
| print("Socket path: \(sock)") | ||
| print("Set DOCKER_HOST=unix://\(sock) to use it with Docker-compatible tools.") | ||
| } else { | ||
| // Already loaded — try to unload and reload | ||
| _ = try? await runProcess("/bin/launchctl", ["bootout", "gui/\(uid)", MockerConfig.launchAgentLabel]) | ||
| let (_, rc2) = try await runProcess( | ||
| "/bin/launchctl", | ||
| ["bootstrap", "gui/\(uid)", plistPath] | ||
| ) | ||
| if rc2 == 0 { | ||
| print("Mocker socket agent reinstalled.") | ||
| print("Socket path: \(sock)") | ||
| } else { | ||
| print("Warning: launchctl bootstrap returned exit code \(rc2).") | ||
| print("Plist written to: \(plistPath)") | ||
| print("Run manually: launchctl bootstrap gui/\(uid) \(plistPath)") | ||
| } | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Any non-zero launchctl bootstrap exit code is treated as “already loaded” and triggers a bootout/rebootstrap. That can hide real bootstrap failures (e.g. invalid plist, permissions) and make debugging harder. Consider checking the launchctl output for the specific “already bootstrapped” case, and otherwise surfacing the original error/output to the user (or throwing a ValidationError).
| let (_, exitCode) = try await runProcess( | |
| "/bin/launchctl", | |
| ["bootstrap", "gui/\(uid)", plistPath] | |
| ) | |
| if exitCode == 0 { | |
| print("Mocker socket agent installed.") | |
| print("Socket path: \(sock)") | |
| print("Set DOCKER_HOST=unix://\(sock) to use it with Docker-compatible tools.") | |
| } else { | |
| // Already loaded — try to unload and reload | |
| _ = try? await runProcess("/bin/launchctl", ["bootout", "gui/\(uid)", MockerConfig.launchAgentLabel]) | |
| let (_, rc2) = try await runProcess( | |
| "/bin/launchctl", | |
| ["bootstrap", "gui/\(uid)", plistPath] | |
| ) | |
| if rc2 == 0 { | |
| print("Mocker socket agent reinstalled.") | |
| print("Socket path: \(sock)") | |
| } else { | |
| print("Warning: launchctl bootstrap returned exit code \(rc2).") | |
| print("Plist written to: \(plistPath)") | |
| print("Run manually: launchctl bootstrap gui/\(uid) \(plistPath)") | |
| } | |
| } | |
| } | |
| let domain = "gui/\(uid)" | |
| let manualCommand = "launchctl bootstrap \(domain) \(plistPath)" | |
| let (bootstrapOutput, exitCode) = try await runProcess( | |
| "/bin/launchctl", | |
| ["bootstrap", domain, plistPath] | |
| ) | |
| if exitCode == 0 { | |
| print("Mocker socket agent installed.") | |
| print("Socket path: \(sock)") | |
| print("Set DOCKER_HOST=unix://\(sock) to use it with Docker-compatible tools.") | |
| } else if isAlreadyBootstrappedMessage(bootstrapOutput) { | |
| // Already loaded — try to unload and reload | |
| _ = try? await runProcess("/bin/launchctl", ["bootout", domain, MockerConfig.launchAgentLabel]) | |
| let (rebootstrapOutput, rc2) = try await runProcess( | |
| "/bin/launchctl", | |
| ["bootstrap", domain, plistPath] | |
| ) | |
| if rc2 == 0 { | |
| print("Mocker socket agent reinstalled.") | |
| print("Socket path: \(sock)") | |
| } else { | |
| throw bootstrapValidationError( | |
| exitCode: rc2, | |
| output: rebootstrapOutput, | |
| plistPath: plistPath, | |
| manualCommand: manualCommand | |
| ) | |
| } | |
| } else { | |
| throw bootstrapValidationError( | |
| exitCode: exitCode, | |
| output: bootstrapOutput, | |
| plistPath: plistPath, | |
| manualCommand: manualCommand | |
| ) | |
| } | |
| } | |
| private func isAlreadyBootstrappedMessage(_ output: String) -> Bool { | |
| let normalized = output.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() | |
| return normalized.contains("already bootstrapped") | |
| || normalized.contains("already loaded") | |
| || normalized.contains("service already loaded") | |
| } | |
| private func bootstrapValidationError( | |
| exitCode: Int32, | |
| output: String, | |
| plistPath: String, | |
| manualCommand: String | |
| ) -> ValidationError { | |
| let trimmedOutput = output.trimmingCharacters(in: .whitespacesAndNewlines) | |
| let details = trimmedOutput.isEmpty ? "No output from launchctl." : trimmedOutput | |
| return ValidationError( | |
| """ | |
| launchctl bootstrap failed with exit code \(exitCode). | |
| \(details) | |
| Plist written to: \(plistPath) | |
| Run manually: \(manualCommand) | |
| """ | |
| ) | |
| } |
| To have the socket service start on login and stay running in the background, | ||
| register it as a launchd user agent. Create | ||
| `~/Library/LaunchAgents/io.mocker.socket.plist`: |
There was a problem hiding this comment.
This section says the service will “start on login and stay running in the background”, but the implementation is socket-activated (RunAtLoad=false) and mocker system service can exit after an inactivity timeout. Consider rewording to describe launchd socket activation (launchd holds the socket and spawns the service on first connect) rather than a long-running background daemon.
| To have the socket service start on login and stay running in the background, | |
| register it as a launchd user agent. Create | |
| `~/Library/LaunchAgents/io.mocker.socket.plist`: | |
| To make the socket available automatically after login, register Mocker as a | |
| launchd user agent with socket activation. launchd will create and hold the | |
| socket, then start `mocker system service` the first time a client connects to | |
| it. Create `~/Library/LaunchAgents/io.mocker.socket.plist`: |
|
|
||
| #### 通过 launchd 自动启动(macOS) | ||
|
|
||
| 如需在登录时自动启动 socket 服务并在后台保持运行,可将其注册为 launchd 用户代理。 |
There was a problem hiding this comment.
This section says the socket service will “在后台保持运行”, but the implementation is launchd socket activation (RunAtLoad=false) and the service may exit after inactivity. Consider updating the wording to explain that launchd holds the socket and spawns mocker system service on connect, rather than implying a permanently running background process.
| 如需在登录时自动启动 socket 服务并在后台保持运行,可将其注册为 launchd 用户代理。 | |
| 如需在登录后通过 launchd 管理该 socket,可将其注册为 launchd 用户代理。`launchd` 会持有该 socket,并在有客户端连接时按需启动 `mocker system service`;服务进程空闲后可能退出,因此这里并不是一个永久常驻的后台进程。 |
| let data = (try? encoder.encode(body)) ?? Data("{}".utf8) | ||
| return HTTPResponse( | ||
| status: status, | ||
| statusText: statusPhrase(status), | ||
| headers: [ | ||
| "Content-Type": "application/json", | ||
| "Content-Length": "\(data.count)", | ||
| "Api-Version": DockerAPIServer.apiVersion, | ||
| "Libpod-API-Version": "5.0.0", | ||
| "Server": "mocker/\(MockerConfig.mockerVersion)", | ||
| ], | ||
| body: data | ||
| ) |
There was a problem hiding this comment.
HTTPResponse.json silently falls back to {} when encoding fails. That can mask server-side bugs and return a 200 with an invalid/empty payload, which is hard for clients to diagnose. Consider making json(...) throwing, or returning a 500 error response when encoding fails (at minimum, log/assert in debug).
| let data = (try? encoder.encode(body)) ?? Data("{}".utf8) | |
| return HTTPResponse( | |
| status: status, | |
| statusText: statusPhrase(status), | |
| headers: [ | |
| "Content-Type": "application/json", | |
| "Content-Length": "\(data.count)", | |
| "Api-Version": DockerAPIServer.apiVersion, | |
| "Libpod-API-Version": "5.0.0", | |
| "Server": "mocker/\(MockerConfig.mockerVersion)", | |
| ], | |
| body: data | |
| ) | |
| do { | |
| let data = try encoder.encode(body) | |
| return HTTPResponse( | |
| status: status, | |
| statusText: statusPhrase(status), | |
| headers: [ | |
| "Content-Type": "application/json", | |
| "Content-Length": "\(data.count)", | |
| "Api-Version": DockerAPIServer.apiVersion, | |
| "Libpod-API-Version": "5.0.0", | |
| "Server": "mocker/\(MockerConfig.mockerVersion)", | |
| ], | |
| body: data | |
| ) | |
| } catch { | |
| assertionFailure("Failed to JSON-encode HTTP response body: \(error)") | |
| return HTTPResponse.text(status: 500, body: "Internal Server Error") | |
| } |
| Then load it: | ||
|
|
||
| ```bash | ||
| launchctl load ~/Library/LaunchAgents/io.mocker.socket.plist |
There was a problem hiding this comment.
The README uses launchctl load to load the plist. On modern macOS, launchctl bootstrap gui/<uid> <plist> is the supported interface (and matches what mocker socket install does). Consider updating the command to launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/io.mocker.socket.plist (or point users to mocker socket install).
| launchctl load ~/Library/LaunchAgents/io.mocker.socket.plist | |
| launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/io.mocker.socket.plist |
| 然后加载: | ||
|
|
||
| ```bash | ||
| launchctl load ~/Library/LaunchAgents/io.mocker.socket.plist |
There was a problem hiding this comment.
这里使用了 launchctl load 来加载 plist。现代 macOS 更推荐 launchctl bootstrap gui/<uid> <plist>(也与 mocker socket install 的行为一致)。建议把示例命令改为 launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/io.mocker.socket.plist,或直接引导用户使用 mocker socket install。
| launchctl load ~/Library/LaunchAgents/io.mocker.socket.plist | |
| launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/io.mocker.socket.plist |
Replace incorrect ~/.local/share/containers/podman/machine/qemu path with the actual macOS fallback path Podman uses when CONTAINER_HOST and XDG_RUNTIME_DIR are unset: $TMPDIR/storage-run-$(id -u)/podman/ podman.sock (sourced from containers/storage homedir_unix.go). Also add the missing symlink method section to README.zh-CN.md, and remove the incorrect reboot-cleanup warning ($TMPDIR on macOS resolves to a stable per-user /var/folders/…/T/ path that persists across boots). Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering> Signed-off-by: Tiger Kaovilai <passawit.kaovilai@gmail.com>
Add a build architecture support table to both READMEs explaining: - linux/arm64: native on Apple Silicon - linux/amd64: works via Rosetta 2 hardware translation - linux/ppc64le, s390x, riscv64: layer-only builds work but RUN steps fail (Exec format error — no QEMU in Apple's build VM) Include workarounds (remote builder, GitHub Actions, Podman machine) and link to tracking issues us#10 and apple/container#1496. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering> Signed-off-by: Tiger Kaovilai <passawit.kaovilai@gmail.com>
- mocker build --platform a,b,c -t tag: builds each arch via Podman machine (QEMU for exotic arches), then assembles OCI manifest list with `podman manifest create` - Exotic-arch single builds print warning before attempt and hint on failure pointing to Podman machine or mocker multi-arch flow - Auto-detects running Podman machine; missing machine prints recovery steps including which native arches can still be built individually - README/README.zh-CN: add v0.2.2 changelog, update arch table with multi-arch column, add multi-arch build usage section Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering> Signed-off-by: Tiger Kaovilai <passawit.kaovilai@gmail.com>
Closes #7
Architecture
graph TD subgraph clients["Client Tools"] D["Docker CLI\ndocker run / ps / images"] P["Podman CLI\npodman run / ps / images"] SDK["Docker/Podman SDKs\nGo · Python · etc."] end subgraph launchd["macOS launchd — socket activation"] PLIST["io.mocker.socket.plist\n↳ mocker socket install"] SOCK["~/.mocker/mocker.sock\n(fd owned by launchd)"] PLIST -->|"bootstrap / enable"| SOCK end D -->|"DOCKER_HOST=unix://…"| SOCK P -->|"CONTAINER_HOST=unix://…"| SOCK SDK -->|"DOCKER_HOST=unix://…"| SOCK SOCK -->|"on connect: spawn"| SVC subgraph service["mocker system service ← this PR"] SVC["DockerAPIServer\nHTTP/1.1 · Connection: close\nLibpod-API-Version header"] HDL["DockerAPIHandlers\nDocker Engine API v1.47\nPodman libpod API v5.0.0"] STORE["PendingRunStore\nin-flight container state\n+ exit-code signalling"] SVC --> HDL HDL <-->|"create / wait / inspect / remove"| STORE end SVC -->|"inactivity timeout → exit\nlaunchd resumes socket"| SOCK subgraph existing["Existing mocker code"] CE["ContainerEngine\nrunStreaming · run · stop\ninspect · remove · exec"] IM["ImageManager\npull · build · list · inspect"] end HDL -->|"container lifecycle"| CE HDL -->|"image ops"| IM subgraph apple["Apple Containerization"] CLI["/usr/local/bin/container\nApple container CLI"] IS["Containerization.ImageStore\n(direct framework)"] VZ["Virtualization.framework\none Linux VM per container"] end CE -->|"exec subprocess"| CLI IM -->|"direct API"| IS CLI --> VZChecklist
Original review fixes
PendingRunStore.waitForExithang on unknown IDs +removenot notifying waiters (DockerAPIServer.swift)waitContainermissing 404 for unknown container IDs (DockerAPIHandlers.swift)runWithOutputCapturepipe buffer deadlock by draining pipes concurrently (ContainerEngine.swift)bindAndListenmagic numbers 104/103 →MemoryLayout(DockerAPIServer.swift)RunAtLoad=true+KeepAlive=truenegating timeout (README.md,README.zh-CN.md)mainbranch (ci.yml)Runtime fixes (verified end-to-end)
Connection: closeheader — fixes "server closed idle connection" ondocker run --rm(DockerAPIServer.swift)runWithOutputCapturewithrunStreamingin attach handler — eliminates hang during large image downloads; progress and output now appear in real time (ContainerEngine.swift,DockerAPIHandlers.swift)GET /libpod/containers/{id}/json—podman run --rminspect after exit (DockerAPIHandlers.swift)DELETE /libpod/containers/{id}— return[{"Id":"…"}]JSON array; was returning empty 204, causing podmanERRO: unexpected end of JSON inputon--rmcleanup (DockerAPIHandlers.swift)POST /libpod/containers/{id}/wait— return plainint32exit code; was returning{"Error":…,"StatusCode":0}object, causing podman unmarshal error (DockerAPIHandlers.swift)Libpod-API-Version: 5.0.0header — suppressesWARN: Service did not provide Libpod-API-Version Header(DockerAPIServer.swift)Acceptance criteria (issue #7)
mocker system servicestarts, accepts launchd-inherited socket fd, serves Docker Engine REST APImocker socket installinstalls launchd plist and activates socketDOCKER_HOST=unix://~/.mocker/mocker.sock docker psworksCONTAINER_HOST=unix://~/.mocker/mocker.sock podman run --rm alpine cat /etc/os-releaseworkspodman --remote --url unix://~/.mocker/mocker.sock ps— not yet explicitly testedlaunchdholds the socket)mocker system serviceexits after inactivity timeoutNote
Responses generated with Claude