Skip to content

feat: implement Docker/Podman socket compatibility via launchd activation#8

Open
kaovilai wants to merge 28 commits intous:mainfrom
kaovilai:copilot/implement-mocker-enhancement-7
Open

feat: implement Docker/Podman socket compatibility via launchd activation#8
kaovilai wants to merge 28 commits intous:mainfrom
kaovilai:copilot/implement-mocker-enhancement-7

Conversation

@kaovilai
Copy link
Copy Markdown

@kaovilai kaovilai commented May 1, 2026

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 --> VZ
Loading

Checklist

Original review fixes

  • Fix PendingRunStore.waitForExit hang on unknown IDs + remove not notifying waiters (DockerAPIServer.swift)
  • Fix waitContainer missing 404 for unknown container IDs (DockerAPIHandlers.swift)
  • Fix runWithOutputCapture pipe buffer deadlock by draining pipes concurrently (ContainerEngine.swift)
  • Fix bindAndListen magic numbers 104/103 → MemoryLayout (DockerAPIServer.swift)
  • Fix launchd template RunAtLoad=true+KeepAlive=true negating timeout (README.md, README.zh-CN.md)
  • Fix CI push trigger to only include main branch (ci.yml)

Runtime fixes (verified end-to-end)

  • Add Connection: close header — fixes "server closed idle connection" on docker run --rm (DockerAPIServer.swift)
  • Replace runWithOutputCapture with runStreaming in attach handler — eliminates hang during large image downloads; progress and output now appear in real time (ContainerEngine.swift, DockerAPIHandlers.swift)
  • Add GET /libpod/containers/{id}/jsonpodman run --rm inspect after exit (DockerAPIHandlers.swift)
  • Fix DELETE /libpod/containers/{id} — return [{"Id":"…"}] JSON array; was returning empty 204, causing podman ERRO: unexpected end of JSON input on --rm cleanup (DockerAPIHandlers.swift)
  • Fix POST /libpod/containers/{id}/wait — return plain int32 exit code; was returning {"Error":…,"StatusCode":0} object, causing podman unmarshal error (DockerAPIHandlers.swift)
  • Add Libpod-API-Version: 5.0.0 header — suppresses WARN: Service did not provide Libpod-API-Version Header (DockerAPIServer.swift)

Acceptance criteria (issue #7)

  • mocker system service starts, accepts launchd-inherited socket fd, serves Docker Engine REST API
  • mocker socket install installs launchd plist and activates socket
  • DOCKER_HOST=unix://~/.mocker/mocker.sock docker ps works
  • CONTAINER_HOST=unix://~/.mocker/mocker.sock podman run --rm alpine cat /etc/os-release works
  • podman --remote --url unix://~/.mocker/mocker.sock ps — not yet explicitly tested
  • Docker SDK clients (Go, Python) — not yet tested
  • Zero additional processes when idle (launchd holds the socket)
  • mocker system service exits after inactivity timeout

Note

Responses generated with Claude

Copilot AI and others added 16 commits May 1, 2026 07:53
…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>
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>
…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>
… 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>
Copilot AI review requested due to automatic review settings May 1, 2026 13:51
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 service and mocker 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.

Comment thread Sources/MockerKit/SocketService/DockerAPIHandlers.swift
Comment thread Sources/MockerKit/Container/ContainerEngine.swift Outdated
Comment thread Sources/MockerKit/SocketService/DockerAPIServer.swift Outdated
Comment thread README.md
Comment thread README.zh-CN.md
Comment thread .github/workflows/ci.yml Outdated
Comment thread .github/workflows/ci.yml
Comment thread Sources/MockerKit/SocketService/DockerAPIServer.swift Outdated
Copilot AI and others added 9 commits May 1, 2026 14:06
…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>
@kaovilai
Copy link
Copy Markdown
Author

kaovilai commented May 1, 2026

working great

❯ uname && podman machine list && podman version && docker version && podman run --rm alpine cat /etc/os-release && docker run --rm alpine cat /etc/os-release
Darwin
NAME        VM TYPE     CREATED     LAST UP     CPUS        MEMORY      DISK SIZE
Client:        Podman Engine
Version:       5.8.1
API Version:   5.8.1
Go Version:    go1.26.1
Git Commit:    c6077f645788743258a1a749f8005b4fb3cbe533
Built:         Wed Mar 11 15:55:05 2026
Build Origin:  pkginstaller
OS/Arch:       darwin/arm64

Server:       Podman Engine
Version:      0.2.0
API Version:  5.0.0
Go Version:   
Built:        Sun Dec 31 19:03:58 0000
OS/Arch:      linux/arm64
Client: Docker Engine - Community
 Version:           29.2.0
 API version:       1.47 (downgraded from 1.53)
 Go version:        go1.25.6
 Git commit:        0b9d1985db
 Built:             Mon Jan 26 19:20:51 2026
 OS/Arch:           darwin/arm64
 Context:           default

Server: Mocker Engine
 Engine:
  Version:          0.2.0
  API version:      1.47 (minimum version 1.24)
  Go version:       
  Git commit:       
  Built:            
  OS/Arch:          linux/arm64
  Experimental:     
[0/6] [0s]
[1/6] Fetching image [0s]
[2/6] Unpacking image [0s]
[3/6] Fetching kernel [0s]
[4/6] Fetching init image [0s]
[5/6] Unpacking init image [0s]
[6/6] Starting container [0s]
[6/6] Starting container [0s]
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.23.4
PRETTY_NAME="Alpine Linux v3.23"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://gitlab.alpinelinux.org/alpine/aports/-/issues"
[0/6] [0s]
[1/6] Fetching image [0s]
[2/6] Unpacking image [0s]
[3/6] Fetching kernel [0s]
[4/6] Fetching init image [0s]
[5/6] Unpacking init image [0s]
[6/6] Starting container [0s]
[6/6] Starting container [0s]
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.23.4
PRETTY_NAME="Alpine Linux v3.23"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://gitlab.alpinelinux.org/alpine/aports/-/issues"

@kaovilai kaovilai requested a review from Copilot May 1, 2026 16:04
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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>
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
<string>io.mocker.socket</string>
<string>\(MockerConfig.launchAgentLabel)</string>

Copilot uses AI. Check for mistakes.
Comment on lines +144 to +148
// Remove socket file if present
let sock = config.socketPath
if FileManager.default.fileExists(atPath: sock) {
try? FileManager.default.removeItem(atPath: sock)
}
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +167 to +179
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)")

Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.

// Ensure ~/.mocker directory exists
try config.ensureDirectories()

Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
// 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
)

Copilot uses AI. Check for mistakes.
Comment on lines +48 to +74
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)")
}
}
}

Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

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).

Suggested change
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)
"""
)
}

Copilot uses AI. Check for mistakes.
Comment thread README.md
Comment on lines +369 to +371
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`:
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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`:

Copilot uses AI. Check for mistakes.
Comment thread README.zh-CN.md

#### 通过 launchd 自动启动(macOS)

如需在登录时自动启动 socket 服务并在后台保持运行,可将其注册为 launchd 用户代理。
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
如需在登录时自动启动 socket 服务并在后台保持运行,可将其注册为 launchd 用户代理。
如需在登录后通过 launchd 管理该 socket,可将其注册为 launchd 用户代理`launchd` 会持有该 socket,并在有客户端连接时按需启动 `mocker system service`;服务进程空闲后可能退出,因此这里并不是一个永久常驻的后台进程

Copilot uses AI. Check for mistakes.
Comment on lines +76 to +88
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
)
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

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).

Suggested change
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")
}

Copilot uses AI. Check for mistakes.
Comment thread README.md
Then load it:

```bash
launchctl load ~/Library/LaunchAgents/io.mocker.socket.plist
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

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).

Suggested change
launchctl load ~/Library/LaunchAgents/io.mocker.socket.plist
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/io.mocker.socket.plist

Copilot uses AI. Check for mistakes.
Comment thread README.zh-CN.md
然后加载:

```bash
launchctl load ~/Library/LaunchAgents/io.mocker.socket.plist
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

这里使用了 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

Suggested change
launchctl load ~/Library/LaunchAgents/io.mocker.socket.plist
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/io.mocker.socket.plist

Copilot uses AI. Check for mistakes.
kaovilai and others added 3 commits May 1, 2026 12:49
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>
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.

Feature request: Docker/Podman-compatible socket via launchd socket activation (mocker system service)

3 participants