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
20 changes: 0 additions & 20 deletions Sources/ContainerClient/ContainerEvents.swift

This file was deleted.

157 changes: 112 additions & 45 deletions Sources/ContainerClient/Core/ClientContainer.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,10 +25,6 @@ import TerminalProgress
public struct ClientContainer: Sendable, Codable {
static let serviceIdentifier = "com.apple.container.apiserver"

private var sandboxClient: SandboxClient {
SandboxClient(id: configuration.id, runtime: configuration.runtimeHandler)
}

/// Identifier of the container.
public var id: String {
configuration.id
Expand DownExpand Up@@ -58,14 +54,10 @@ public struct ClientContainer: Sendable, Codable {
self.status = snapshot.status
self.networks = snapshot.networks
}

public var initProcess: ClientProcess {
ClientProcessImpl(containerId: self.id, client: self.sandboxClient)
}
}

extension ClientContainer {
private static func newClient() -> XPCClient {
private static func newXPCClient() -> XPCClient {
XPCClient(service: serviceIdentifier)
}

Expand All@@ -84,8 +76,8 @@ extension ClientContainer {
kernel: Kernel
) async throws -> ClientContainer {
do {
let client = Self.newClient()
let request = XPCMessage(route: .createContainer)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerCreate)

let data = try JSONEncoder().encode(configuration)
let kdata = try JSONEncoder().encode(kernel)
Expand All@@ -107,8 +99,8 @@ extension ClientContainer {

public static func list() async throws -> [ClientContainer] {
do {
let client = Self.newClient()
let request = XPCMessage(route: .listContainer)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerList)

let response = try await xpcSend(
client: client,
Expand DownExpand Up@@ -145,16 +137,72 @@ extension ClientContainer {

extension ClientContainer {
public func bootstrap(stdio: [FileHandle?]) async throws -> ClientProcess {
let client = self.sandboxClient
try await client.bootstrap(stdio: stdio)
return ClientProcessImpl(containerId: self.id, client: self.sandboxClient)
let request = XPCMessage(route: .containerBootstrap)
let client = Self.newXPCClient()

for (i, h) in stdio.enumerated() {
let key: XPCKeys = try {
switch i {
case 0: .stdin
case 1: .stdout
case 2: .stderr
default:
throw ContainerizationError(.invalidArgument, message: "invalid fd \(i)")
}
}()

if let h {
request.set(key: key, value: h)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This code repeats 4 times. Consider extracting it into helper methods in XPCMessage:

 static func stdioKey(for index: Int) throws -> XPCKeys {
switch index {
case 0: return .stdin
case 1: return .stdout
case 2: return .stderr
default:
throw ContainerizationError(.invalidArgument, message: "invalid fd \(index)")
}
}
static func setStdioHandles(on request: XPCMessage, stdio: [FileHandle?]) throws {
for (index, handle) in stdio.enumerated() {
if let handle {
request.set(key: stdioKey(for: index), value: handle)
}
}
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

John had commented the same, I'm going to do that (and some other cleanups) in a followup


do {
request.set(key: .id, value: self.id)
try await client.send(request)
return ClientProcessImpl(containerId: self.id, xpcClient: client)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to bootstrap container",
cause: error
)
}
}

public func kill(_ signal: Int32) async throws {
do {
let request = XPCMessage(route: .containerKill)
request.set(key: .id, value: self.id)
request.set(key: .processIdentifier, value: self.id)
request.set(key: .signal, value: Int64(signal))

let client = Self.newXPCClient()
try await client.send(request)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to kill container",
cause: error
)
}
}

/// Stop the container and all processes currently executing inside.
public func stop(opts: ContainerStopOptions = ContainerStopOptions.default) async throws {
do {
let client = self.sandboxClient
try await client.stop(options: opts)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerStop)
let data = try JSONEncoder().encode(opts)
request.set(key: .id, value: self.id)
request.set(key: .stopOptions, value: data)

// Stop is somewhat more prone to hanging than other commands given it
// has quite a bit of `wait()`'s down the chain to make sure the container actually
// exited. To combat a potential hang, lets timeout if we don't return in a small
// time period after the actual stop timeout sent via .stopOptions (the time
// until we send SIGKILL after SIGTERM if the container still hasn't exited).
let responseTimeout = Duration(.seconds(Int64(opts.timeoutInSeconds + 3)))
Comment thread
dcantah marked this conversation as resolved.
try await client.send(request, responseTimeout: responseTimeout)
} catch {
throw ContainerizationError(
.internalError,
Expand All@@ -167,8 +215,8 @@ extension ClientContainer {
/// Delete the container along with any resources.
public func delete(force: Bool = false) async throws {
do {
let client = XPCClient(service: Self.serviceIdentifier)
let request = XPCMessage(route: .deleteContainer)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerDelete)
request.set(key: .id, value: self.id)
request.set(key: .forceDelete, value: force)
try await client.send(request)
Expand All@@ -180,46 +228,53 @@ extension ClientContainer {
)
}
}
}

extension ClientContainer {
/// Execute a new process inside a running container.
/// Create a new process inside a running container. The process is in a
/// created state and must still be started.
public func createProcess(
id: String,
configuration: ProcessConfiguration,
stdio: [FileHandle?]
) async throws -> ClientProcess {
do {
let client = self.sandboxClient
try await client.createProcess(id, config: configuration, stdio: stdio)
return ClientProcessImpl(containerId: self.id, processId: id, client: client)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to exec in container",
cause: error
)
}
}
let request = XPCMessage(route: .containerCreateProcess)
request.set(key: .id, value: self.id)
request.set(key: .processIdentifier, value: id)

/// Send or "kill" a signal to the initial process of the container.
/// Kill does not wait for the process to exit, it only delivers the signal.
public func kill(_ signal: Int32) async throws {
do {
let client = self.sandboxClient
try await client.kill(self.id, signal: Int64(signal))
let data = try JSONEncoder().encode(configuration)
request.set(key: .processConfig, value: data)

for (i, h) in stdio.enumerated() {
Comment thread
dcantah marked this conversation as resolved.
let key: XPCKeys = try {
switch i {
case 0: .stdin
case 1: .stdout
case 2: .stderr
default:
throw ContainerizationError(.invalidArgument, message: "invalid fd \(i)")
}
}()

if let h {
request.set(key: key, value: h)
}
}

let client = Self.newXPCClient()
try await client.send(request)
return ClientProcessImpl(containerId: self.id, processId: id, xpcClient: client)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to kill container \(self.id)",
message: "failed to create process in container",
cause: error
)
}
}

public func logs() async throws -> [FileHandle] {
do {
let client = XPCClient(service: Self.serviceIdentifier)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerLogs)
request.set(key: .id, value: self.id)

Expand All@@ -242,15 +297,27 @@ extension ClientContainer {
}

public func dial(_ port: UInt32) async throws -> FileHandle {
let request = XPCMessage(route: .containerDial)
request.set(key: .id, value: self.id)
request.set(key: .port, value: UInt64(port))

let client = Self.newXPCClient()
let response: XPCMessage
do {
let client = self.sandboxClient
return try await client.dial(port)
response = try await client.send(request)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to dial \(port) in container \(self.id)",
message: "failed to dial port \(port) on container",
cause: error
)
}
guard let fh = response.fileHandle(key: .fd) else {
throw ContainerizationError(
.internalError,
message: "failed to get fd for vsock port \(port)"
)
}
return fh
}
}
91 changes: 39 additions & 52 deletions Sources/ContainerClient/Core/ClientProcess.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,7 @@ public protocol ClientProcess: Sendable {
func start() async throws
/// Send a terminal resize request to the process `id`.
func resize(_ size: Terminal.Size) async throws
/// Send or "kill" a signal to the process `id`.
/// Send a signal to the process `id`.
/// Kill does not wait for the process to exit, it only delivers the signal.
func kill(_ signal: Int32) async throws
/// Wait for the process `id` to complete and return its exit code.
Expand All@@ -45,79 +45,66 @@ public protocol ClientProcess: Sendable {

struct ClientProcessImpl: ClientProcess, Sendable {
static let serviceIdentifier = "com.apple.container.apiserver"

/// ID of the process.
public var id: String {
Comment thread
dcantah marked this conversation as resolved.
processId ?? containerId
}

/// Identifier of the container.
public let containerId: String

private let client: SandboxClient

/// Identifier of a process. That is running inside of a container.
/// This field is nil if the process this objects refers to is the
/// init process of the container.
public let processId: String?

public var id: String {
processId ?? containerId
}
private let xpcClient: XPCClient

init(containerId: String, processId: String? = nil, client: SandboxClient) {
init(containerId: String, processId: String? = nil, xpcClient: XPCClient) {
self.containerId = containerId
self.processId = processId
self.client = client
self.xpcClient = xpcClient
}

/// Start the container and return the initial process.
/// Start the process.
public func start() async throws {
do {
let client = self.client
try await client.startProcess(self.id)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to start container",
cause: error
)
}
let request = XPCMessage(route: .containerStartProcess)
request.set(key: .id, value: containerId)
request.set(key: .processIdentifier, value: id)

try await xpcClient.send(request)
}

/// Send a signal to the process.
public func kill(_ signal: Int32) async throws {
do {

let client = self.client
try await client.kill(self.id, signal: Int64(signal))
} catch {
throw ContainerizationError(
.internalError,
message: "failed to kill process",
cause: error
)
}
}
let request = XPCMessage(route: .containerKill)
request.set(key: .id, value: containerId)
request.set(key: .processIdentifier, value: id)
request.set(key: .signal, value: Int64(signal))

public func resize(_ size: ContainerizationOS.Terminal.Size) async throws {
do {
try await xpcClient.send(request)
}

let client = self.client
try await client.resize(self.id, size: size)
/// Resize the processes PTY if it has one.
public func resize(_ size: Terminal.Size) async throws {
let request = XPCMessage(route: .containerResize)
request.set(key: .id, value: containerId)
request.set(key: .processIdentifier, value: id)
request.set(key: .width, value: UInt64(size.width))
request.set(key: .height, value: UInt64(size.height))

} catch {
throw ContainerizationError(
.internalError,
message: "failed to resize process",
cause: error
)
}
try await xpcClient.send(request)
}

/// Wait for the process to exit.
public func wait() async throws -> Int32 {
do {
let client = self.client
return try await client.wait(self.id)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to wait on process",
cause: error
)
}
let request = XPCMessage(route: .containerWait)
request.set(key: .id, value: containerId)
request.set(key: .processIdentifier, value: id)

let response = try await xpcClient.send(request)
let code = response.int64(key: .exitCode)
return Int32(code)
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
20 changes: 0 additions & 20 deletions Sources/ContainerClient/ContainerEvents.swift

This file was deleted.

157 changes: 112 additions & 45 deletions Sources/ContainerClient/Core/ClientContainer.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,10 +25,6 @@ import TerminalProgress
public struct ClientContainer: Sendable, Codable {
static let serviceIdentifier = "com.apple.container.apiserver"

private var sandboxClient: SandboxClient {
SandboxClient(id: configuration.id, runtime: configuration.runtimeHandler)
}

/// Identifier of the container.
public var id: String {
configuration.id
Expand DownExpand Up@@ -58,14 +54,10 @@ public struct ClientContainer: Sendable, Codable {
self.status = snapshot.status
self.networks = snapshot.networks
}

public var initProcess: ClientProcess {
ClientProcessImpl(containerId: self.id, client: self.sandboxClient)
}
}

extension ClientContainer {
private static func newClient() -> XPCClient {
private static func newXPCClient() -> XPCClient {
XPCClient(service: serviceIdentifier)
}

Expand All@@ -84,8 +76,8 @@ extension ClientContainer {
kernel: Kernel
) async throws -> ClientContainer {
do {
let client = Self.newClient()
let request = XPCMessage(route: .createContainer)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerCreate)

let data = try JSONEncoder().encode(configuration)
let kdata = try JSONEncoder().encode(kernel)
Expand All@@ -107,8 +99,8 @@ extension ClientContainer {

public static func list() async throws -> [ClientContainer] {
do {
let client = Self.newClient()
let request = XPCMessage(route: .listContainer)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerList)

let response = try await xpcSend(
client: client,
Expand DownExpand Up@@ -145,16 +137,72 @@ extension ClientContainer {

extension ClientContainer {
public func bootstrap(stdio: [FileHandle?]) async throws -> ClientProcess {
let client = self.sandboxClient
try await client.bootstrap(stdio: stdio)
return ClientProcessImpl(containerId: self.id, client: self.sandboxClient)
let request = XPCMessage(route: .containerBootstrap)
let client = Self.newXPCClient()

for (i, h) in stdio.enumerated() {
let key: XPCKeys = try {
switch i {
case 0: .stdin
case 1: .stdout
case 2: .stderr
default:
throw ContainerizationError(.invalidArgument, message: "invalid fd \(i)")
}
}()

if let h {
request.set(key: key, value: h)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This code repeats 4 times. Consider extracting it into helper methods in XPCMessage:

 static func stdioKey(for index: Int) throws -> XPCKeys {
switch index {
case 0: return .stdin
case 1: return .stdout
case 2: return .stderr
default:
throw ContainerizationError(.invalidArgument, message: "invalid fd \(index)")
}
}
static func setStdioHandles(on request: XPCMessage, stdio: [FileHandle?]) throws {
for (index, handle) in stdio.enumerated() {
if let handle {
request.set(key: stdioKey(for: index), value: handle)
}
}
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

John had commented the same, I'm going to do that (and some other cleanups) in a followup


do {
request.set(key: .id, value: self.id)
try await client.send(request)
return ClientProcessImpl(containerId: self.id, xpcClient: client)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to bootstrap container",
cause: error
)
}
}

public func kill(_ signal: Int32) async throws {
do {
let request = XPCMessage(route: .containerKill)
request.set(key: .id, value: self.id)
request.set(key: .processIdentifier, value: self.id)
request.set(key: .signal, value: Int64(signal))

let client = Self.newXPCClient()
try await client.send(request)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to kill container",
cause: error
)
}
}

/// Stop the container and all processes currently executing inside.
public func stop(opts: ContainerStopOptions = ContainerStopOptions.default) async throws {
do {
let client = self.sandboxClient
try await client.stop(options: opts)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerStop)
let data = try JSONEncoder().encode(opts)
request.set(key: .id, value: self.id)
request.set(key: .stopOptions, value: data)

// Stop is somewhat more prone to hanging than other commands given it
// has quite a bit of `wait()`'s down the chain to make sure the container actually
// exited. To combat a potential hang, lets timeout if we don't return in a small
// time period after the actual stop timeout sent via .stopOptions (the time
// until we send SIGKILL after SIGTERM if the container still hasn't exited).
let responseTimeout = Duration(.seconds(Int64(opts.timeoutInSeconds + 3)))
Comment thread
dcantah marked this conversation as resolved.
try await client.send(request, responseTimeout: responseTimeout)
} catch {
throw ContainerizationError(
.internalError,
Expand All@@ -167,8 +215,8 @@ extension ClientContainer {
/// Delete the container along with any resources.
public func delete(force: Bool = false) async throws {
do {
let client = XPCClient(service: Self.serviceIdentifier)
let request = XPCMessage(route: .deleteContainer)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerDelete)
request.set(key: .id, value: self.id)
request.set(key: .forceDelete, value: force)
try await client.send(request)
Expand All@@ -180,46 +228,53 @@ extension ClientContainer {
)
}
}
}

extension ClientContainer {
/// Execute a new process inside a running container.
/// Create a new process inside a running container. The process is in a
/// created state and must still be started.
public func createProcess(
id: String,
configuration: ProcessConfiguration,
stdio: [FileHandle?]
) async throws -> ClientProcess {
do {
let client = self.sandboxClient
try await client.createProcess(id, config: configuration, stdio: stdio)
return ClientProcessImpl(containerId: self.id, processId: id, client: client)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to exec in container",
cause: error
)
}
}
let request = XPCMessage(route: .containerCreateProcess)
request.set(key: .id, value: self.id)
request.set(key: .processIdentifier, value: id)

/// Send or "kill" a signal to the initial process of the container.
/// Kill does not wait for the process to exit, it only delivers the signal.
public func kill(_ signal: Int32) async throws {
do {
let client = self.sandboxClient
try await client.kill(self.id, signal: Int64(signal))
let data = try JSONEncoder().encode(configuration)
request.set(key: .processConfig, value: data)

for (i, h) in stdio.enumerated() {
Comment thread
dcantah marked this conversation as resolved.
let key: XPCKeys = try {
switch i {
case 0: .stdin
case 1: .stdout
case 2: .stderr
default:
throw ContainerizationError(.invalidArgument, message: "invalid fd \(i)")
}
}()

if let h {
request.set(key: key, value: h)
}
}

let client = Self.newXPCClient()
try await client.send(request)
return ClientProcessImpl(containerId: self.id, processId: id, xpcClient: client)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to kill container \(self.id)",
message: "failed to create process in container",
cause: error
)
}
}

public func logs() async throws -> [FileHandle] {
do {
let client = XPCClient(service: Self.serviceIdentifier)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerLogs)
request.set(key: .id, value: self.id)

Expand All@@ -242,15 +297,27 @@ extension ClientContainer {
}

public func dial(_ port: UInt32) async throws -> FileHandle {
let request = XPCMessage(route: .containerDial)
request.set(key: .id, value: self.id)
request.set(key: .port, value: UInt64(port))

let client = Self.newXPCClient()
let response: XPCMessage
do {
let client = self.sandboxClient
return try await client.dial(port)
response = try await client.send(request)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to dial \(port) in container \(self.id)",
message: "failed to dial port \(port) on container",
cause: error
)
}
guard let fh = response.fileHandle(key: .fd) else {
throw ContainerizationError(
.internalError,
message: "failed to get fd for vsock port \(port)"
)
}
return fh
}
}
91 changes: 39 additions & 52 deletions Sources/ContainerClient/Core/ClientProcess.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,7 @@ public protocol ClientProcess: Sendable {
func start() async throws
/// Send a terminal resize request to the process `id`.
func resize(_ size: Terminal.Size) async throws
/// Send or "kill" a signal to the process `id`.
/// Send a signal to the process `id`.
/// Kill does not wait for the process to exit, it only delivers the signal.
func kill(_ signal: Int32) async throws
/// Wait for the process `id` to complete and return its exit code.
Expand All@@ -45,79 +45,66 @@ public protocol ClientProcess: Sendable {

struct ClientProcessImpl: ClientProcess, Sendable {
static let serviceIdentifier = "com.apple.container.apiserver"

/// ID of the process.
public var id: String {
Comment thread
dcantah marked this conversation as resolved.
processId ?? containerId
}

/// Identifier of the container.
public let containerId: String

private let client: SandboxClient

/// Identifier of a process. That is running inside of a container.
/// This field is nil if the process this objects refers to is the
/// init process of the container.
public let processId: String?

public var id: String {
processId ?? containerId
}
private let xpcClient: XPCClient

init(containerId: String, processId: String? = nil, client: SandboxClient) {
init(containerId: String, processId: String? = nil, xpcClient: XPCClient) {
self.containerId = containerId
self.processId = processId
self.client = client
self.xpcClient = xpcClient
}

/// Start the container and return the initial process.
/// Start the process.
public func start() async throws {
do {
let client = self.client
try await client.startProcess(self.id)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to start container",
cause: error
)
}
let request = XPCMessage(route: .containerStartProcess)
request.set(key: .id, value: containerId)
request.set(key: .processIdentifier, value: id)

try await xpcClient.send(request)
}

/// Send a signal to the process.
public func kill(_ signal: Int32) async throws {
do {

let client = self.client
try await client.kill(self.id, signal: Int64(signal))
} catch {
throw ContainerizationError(
.internalError,
message: "failed to kill process",
cause: error
)
}
}
let request = XPCMessage(route: .containerKill)
request.set(key: .id, value: containerId)
request.set(key: .processIdentifier, value: id)
request.set(key: .signal, value: Int64(signal))

public func resize(_ size: ContainerizationOS.Terminal.Size) async throws {
do {
try await xpcClient.send(request)
}

let client = self.client
try await client.resize(self.id, size: size)
/// Resize the processes PTY if it has one.
public func resize(_ size: Terminal.Size) async throws {
let request = XPCMessage(route: .containerResize)
request.set(key: .id, value: containerId)
request.set(key: .processIdentifier, value: id)
request.set(key: .width, value: UInt64(size.width))
request.set(key: .height, value: UInt64(size.height))

} catch {
throw ContainerizationError(
.internalError,
message: "failed to resize process",
cause: error
)
}
try await xpcClient.send(request)
}

/// Wait for the process to exit.
public func wait() async throws -> Int32 {
do {
let client = self.client
return try await client.wait(self.id)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to wait on process",
cause: error
)
}
let request = XPCMessage(route: .containerWait)
request.set(key: .id, value: containerId)
request.set(key: .processIdentifier, value: id)

let response = try await xpcClient.send(request)
let code = response.int64(key: .exitCode)
return Int32(code)
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
20 changes: 0 additions & 20 deletions Sources/ContainerClient/ContainerEvents.swift

This file was deleted.

157 changes: 112 additions & 45 deletions Sources/ContainerClient/Core/ClientContainer.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,10 +25,6 @@ import TerminalProgress
public struct ClientContainer: Sendable, Codable {
static let serviceIdentifier = "com.apple.container.apiserver"

private var sandboxClient: SandboxClient {
SandboxClient(id: configuration.id, runtime: configuration.runtimeHandler)
}

/// Identifier of the container.
public var id: String {
configuration.id
Expand DownExpand Up@@ -58,14 +54,10 @@ public struct ClientContainer: Sendable, Codable {
self.status = snapshot.status
self.networks = snapshot.networks
}

public var initProcess: ClientProcess {
ClientProcessImpl(containerId: self.id, client: self.sandboxClient)
}
}

extension ClientContainer {
private static func newClient() -> XPCClient {
private static func newXPCClient() -> XPCClient {
XPCClient(service: serviceIdentifier)
}

Expand All@@ -84,8 +76,8 @@ extension ClientContainer {
kernel: Kernel
) async throws -> ClientContainer {
do {
let client = Self.newClient()
let request = XPCMessage(route: .createContainer)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerCreate)

let data = try JSONEncoder().encode(configuration)
let kdata = try JSONEncoder().encode(kernel)
Expand All@@ -107,8 +99,8 @@ extension ClientContainer {

public static func list() async throws -> [ClientContainer] {
do {
let client = Self.newClient()
let request = XPCMessage(route: .listContainer)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerList)

let response = try await xpcSend(
client: client,
Expand DownExpand Up@@ -145,16 +137,72 @@ extension ClientContainer {

extension ClientContainer {
public func bootstrap(stdio: [FileHandle?]) async throws -> ClientProcess {
let client = self.sandboxClient
try await client.bootstrap(stdio: stdio)
return ClientProcessImpl(containerId: self.id, client: self.sandboxClient)
let request = XPCMessage(route: .containerBootstrap)
let client = Self.newXPCClient()

for (i, h) in stdio.enumerated() {
let key: XPCKeys = try {
switch i {
case 0: .stdin
case 1: .stdout
case 2: .stderr
default:
throw ContainerizationError(.invalidArgument, message: "invalid fd \(i)")
}
}()

if let h {
request.set(key: key, value: h)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This code repeats 4 times. Consider extracting it into helper methods in XPCMessage:

 static func stdioKey(for index: Int) throws -> XPCKeys {
switch index {
case 0: return .stdin
case 1: return .stdout
case 2: return .stderr
default:
throw ContainerizationError(.invalidArgument, message: "invalid fd \(index)")
}
}
static func setStdioHandles(on request: XPCMessage, stdio: [FileHandle?]) throws {
for (index, handle) in stdio.enumerated() {
if let handle {
request.set(key: stdioKey(for: index), value: handle)
}
}
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

John had commented the same, I'm going to do that (and some other cleanups) in a followup


do {
request.set(key: .id, value: self.id)
try await client.send(request)
return ClientProcessImpl(containerId: self.id, xpcClient: client)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to bootstrap container",
cause: error
)
}
}

public func kill(_ signal: Int32) async throws {
do {
let request = XPCMessage(route: .containerKill)
request.set(key: .id, value: self.id)
request.set(key: .processIdentifier, value: self.id)
request.set(key: .signal, value: Int64(signal))

let client = Self.newXPCClient()
try await client.send(request)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to kill container",
cause: error
)
}
}

/// Stop the container and all processes currently executing inside.
public func stop(opts: ContainerStopOptions = ContainerStopOptions.default) async throws {
do {
let client = self.sandboxClient
try await client.stop(options: opts)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerStop)
let data = try JSONEncoder().encode(opts)
request.set(key: .id, value: self.id)
request.set(key: .stopOptions, value: data)

// Stop is somewhat more prone to hanging than other commands given it
// has quite a bit of `wait()`'s down the chain to make sure the container actually
// exited. To combat a potential hang, lets timeout if we don't return in a small
// time period after the actual stop timeout sent via .stopOptions (the time
// until we send SIGKILL after SIGTERM if the container still hasn't exited).
let responseTimeout = Duration(.seconds(Int64(opts.timeoutInSeconds + 3)))
Comment thread
dcantah marked this conversation as resolved.
try await client.send(request, responseTimeout: responseTimeout)
} catch {
throw ContainerizationError(
.internalError,
Expand All@@ -167,8 +215,8 @@ extension ClientContainer {
/// Delete the container along with any resources.
public func delete(force: Bool = false) async throws {
do {
let client = XPCClient(service: Self.serviceIdentifier)
let request = XPCMessage(route: .deleteContainer)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerDelete)
request.set(key: .id, value: self.id)
request.set(key: .forceDelete, value: force)
try await client.send(request)
Expand All@@ -180,46 +228,53 @@ extension ClientContainer {
)
}
}
}

extension ClientContainer {
/// Execute a new process inside a running container.
/// Create a new process inside a running container. The process is in a
/// created state and must still be started.
public func createProcess(
id: String,
configuration: ProcessConfiguration,
stdio: [FileHandle?]
) async throws -> ClientProcess {
do {
let client = self.sandboxClient
try await client.createProcess(id, config: configuration, stdio: stdio)
return ClientProcessImpl(containerId: self.id, processId: id, client: client)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to exec in container",
cause: error
)
}
}
let request = XPCMessage(route: .containerCreateProcess)
request.set(key: .id, value: self.id)
request.set(key: .processIdentifier, value: id)

/// Send or "kill" a signal to the initial process of the container.
/// Kill does not wait for the process to exit, it only delivers the signal.
public func kill(_ signal: Int32) async throws {
do {
let client = self.sandboxClient
try await client.kill(self.id, signal: Int64(signal))
let data = try JSONEncoder().encode(configuration)
request.set(key: .processConfig, value: data)

for (i, h) in stdio.enumerated() {
Comment thread
dcantah marked this conversation as resolved.
let key: XPCKeys = try {
switch i {
case 0: .stdin
case 1: .stdout
case 2: .stderr
default:
throw ContainerizationError(.invalidArgument, message: "invalid fd \(i)")
}
}()

if let h {
request.set(key: key, value: h)
}
}

let client = Self.newXPCClient()
try await client.send(request)
return ClientProcessImpl(containerId: self.id, processId: id, xpcClient: client)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to kill container \(self.id)",
message: "failed to create process in container",
cause: error
)
}
}

public func logs() async throws -> [FileHandle] {
do {
let client = XPCClient(service: Self.serviceIdentifier)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerLogs)
request.set(key: .id, value: self.id)

Expand All@@ -242,15 +297,27 @@ extension ClientContainer {
}

public func dial(_ port: UInt32) async throws -> FileHandle {
let request = XPCMessage(route: .containerDial)
request.set(key: .id, value: self.id)
request.set(key: .port, value: UInt64(port))

let client = Self.newXPCClient()
let response: XPCMessage
do {
let client = self.sandboxClient
return try await client.dial(port)
response = try await client.send(request)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to dial \(port) in container \(self.id)",
message: "failed to dial port \(port) on container",
cause: error
)
}
guard let fh = response.fileHandle(key: .fd) else {
throw ContainerizationError(
.internalError,
message: "failed to get fd for vsock port \(port)"
)
}
return fh
}
}
91 changes: 39 additions & 52 deletions Sources/ContainerClient/Core/ClientProcess.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,7 @@ public protocol ClientProcess: Sendable {
func start() async throws
/// Send a terminal resize request to the process `id`.
func resize(_ size: Terminal.Size) async throws
/// Send or "kill" a signal to the process `id`.
/// Send a signal to the process `id`.
/// Kill does not wait for the process to exit, it only delivers the signal.
func kill(_ signal: Int32) async throws
/// Wait for the process `id` to complete and return its exit code.
Expand All@@ -45,79 +45,66 @@ public protocol ClientProcess: Sendable {

struct ClientProcessImpl: ClientProcess, Sendable {
static let serviceIdentifier = "com.apple.container.apiserver"

/// ID of the process.
public var id: String {
Comment thread
dcantah marked this conversation as resolved.
processId ?? containerId
}

/// Identifier of the container.
public let containerId: String

private let client: SandboxClient

/// Identifier of a process. That is running inside of a container.
/// This field is nil if the process this objects refers to is the
/// init process of the container.
public let processId: String?

public var id: String {
processId ?? containerId
}
private let xpcClient: XPCClient

init(containerId: String, processId: String? = nil, client: SandboxClient) {
init(containerId: String, processId: String? = nil, xpcClient: XPCClient) {
self.containerId = containerId
self.processId = processId
self.client = client
self.xpcClient = xpcClient
}

/// Start the container and return the initial process.
/// Start the process.
public func start() async throws {
do {
let client = self.client
try await client.startProcess(self.id)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to start container",
cause: error
)
}
let request = XPCMessage(route: .containerStartProcess)
request.set(key: .id, value: containerId)
request.set(key: .processIdentifier, value: id)

try await xpcClient.send(request)
}

/// Send a signal to the process.
public func kill(_ signal: Int32) async throws {
do {

let client = self.client
try await client.kill(self.id, signal: Int64(signal))
} catch {
throw ContainerizationError(
.internalError,
message: "failed to kill process",
cause: error
)
}
}
let request = XPCMessage(route: .containerKill)
request.set(key: .id, value: containerId)
request.set(key: .processIdentifier, value: id)
request.set(key: .signal, value: Int64(signal))

public func resize(_ size: ContainerizationOS.Terminal.Size) async throws {
do {
try await xpcClient.send(request)
}

let client = self.client
try await client.resize(self.id, size: size)
/// Resize the processes PTY if it has one.
public func resize(_ size: Terminal.Size) async throws {
let request = XPCMessage(route: .containerResize)
request.set(key: .id, value: containerId)
request.set(key: .processIdentifier, value: id)
request.set(key: .width, value: UInt64(size.width))
request.set(key: .height, value: UInt64(size.height))

} catch {
throw ContainerizationError(
.internalError,
message: "failed to resize process",
cause: error
)
}
try await xpcClient.send(request)
}

/// Wait for the process to exit.
public func wait() async throws -> Int32 {
do {
let client = self.client
return try await client.wait(self.id)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to wait on process",
cause: error
)
}
let request = XPCMessage(route: .containerWait)
request.set(key: .id, value: containerId)
request.set(key: .processIdentifier, value: id)

let response = try await xpcClient.send(request)
let code = response.int64(key: .exitCode)
return Int32(code)
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
20 changes: 0 additions & 20 deletions Sources/ContainerClient/ContainerEvents.swift

This file was deleted.

157 changes: 112 additions & 45 deletions Sources/ContainerClient/Core/ClientContainer.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,10 +25,6 @@ import TerminalProgress
public struct ClientContainer: Sendable, Codable {
static let serviceIdentifier = "com.apple.container.apiserver"

private var sandboxClient: SandboxClient {
SandboxClient(id: configuration.id, runtime: configuration.runtimeHandler)
}

/// Identifier of the container.
public var id: String {
configuration.id
Expand DownExpand Up@@ -58,14 +54,10 @@ public struct ClientContainer: Sendable, Codable {
self.status = snapshot.status
self.networks = snapshot.networks
}

public var initProcess: ClientProcess {
ClientProcessImpl(containerId: self.id, client: self.sandboxClient)
}
}

extension ClientContainer {
private static func newClient() -> XPCClient {
private static func newXPCClient() -> XPCClient {
XPCClient(service: serviceIdentifier)
}

Expand All@@ -84,8 +76,8 @@ extension ClientContainer {
kernel: Kernel
) async throws -> ClientContainer {
do {
let client = Self.newClient()
let request = XPCMessage(route: .createContainer)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerCreate)

let data = try JSONEncoder().encode(configuration)
let kdata = try JSONEncoder().encode(kernel)
Expand All@@ -107,8 +99,8 @@ extension ClientContainer {

public static func list() async throws -> [ClientContainer] {
do {
let client = Self.newClient()
let request = XPCMessage(route: .listContainer)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerList)

let response = try await xpcSend(
client: client,
Expand DownExpand Up@@ -145,16 +137,72 @@ extension ClientContainer {

extension ClientContainer {
public func bootstrap(stdio: [FileHandle?]) async throws -> ClientProcess {
let client = self.sandboxClient
try await client.bootstrap(stdio: stdio)
return ClientProcessImpl(containerId: self.id, client: self.sandboxClient)
let request = XPCMessage(route: .containerBootstrap)
let client = Self.newXPCClient()

for (i, h) in stdio.enumerated() {
let key: XPCKeys = try {
switch i {
case 0: .stdin
case 1: .stdout
case 2: .stderr
default:
throw ContainerizationError(.invalidArgument, message: "invalid fd \(i)")
}
}()

if let h {
request.set(key: key, value: h)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This code repeats 4 times. Consider extracting it into helper methods in XPCMessage:

 static func stdioKey(for index: Int) throws -> XPCKeys {
switch index {
case 0: return .stdin
case 1: return .stdout
case 2: return .stderr
default:
throw ContainerizationError(.invalidArgument, message: "invalid fd \(index)")
}
}
static func setStdioHandles(on request: XPCMessage, stdio: [FileHandle?]) throws {
for (index, handle) in stdio.enumerated() {
if let handle {
request.set(key: stdioKey(for: index), value: handle)
}
}
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

John had commented the same, I'm going to do that (and some other cleanups) in a followup


do {
request.set(key: .id, value: self.id)
try await client.send(request)
return ClientProcessImpl(containerId: self.id, xpcClient: client)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to bootstrap container",
cause: error
)
}
}

public func kill(_ signal: Int32) async throws {
do {
let request = XPCMessage(route: .containerKill)
request.set(key: .id, value: self.id)
request.set(key: .processIdentifier, value: self.id)
request.set(key: .signal, value: Int64(signal))

let client = Self.newXPCClient()
try await client.send(request)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to kill container",
cause: error
)
}
}

/// Stop the container and all processes currently executing inside.
public func stop(opts: ContainerStopOptions = ContainerStopOptions.default) async throws {
do {
let client = self.sandboxClient
try await client.stop(options: opts)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerStop)
let data = try JSONEncoder().encode(opts)
request.set(key: .id, value: self.id)
request.set(key: .stopOptions, value: data)

// Stop is somewhat more prone to hanging than other commands given it
// has quite a bit of `wait()`'s down the chain to make sure the container actually
// exited. To combat a potential hang, lets timeout if we don't return in a small
// time period after the actual stop timeout sent via .stopOptions (the time
// until we send SIGKILL after SIGTERM if the container still hasn't exited).
let responseTimeout = Duration(.seconds(Int64(opts.timeoutInSeconds + 3)))
Comment thread
dcantah marked this conversation as resolved.
try await client.send(request, responseTimeout: responseTimeout)
} catch {
throw ContainerizationError(
.internalError,
Expand All@@ -167,8 +215,8 @@ extension ClientContainer {
/// Delete the container along with any resources.
public func delete(force: Bool = false) async throws {
do {
let client = XPCClient(service: Self.serviceIdentifier)
let request = XPCMessage(route: .deleteContainer)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerDelete)
request.set(key: .id, value: self.id)
request.set(key: .forceDelete, value: force)
try await client.send(request)
Expand All@@ -180,46 +228,53 @@ extension ClientContainer {
)
}
}
}

extension ClientContainer {
/// Execute a new process inside a running container.
/// Create a new process inside a running container. The process is in a
/// created state and must still be started.
public func createProcess(
id: String,
configuration: ProcessConfiguration,
stdio: [FileHandle?]
) async throws -> ClientProcess {
do {
let client = self.sandboxClient
try await client.createProcess(id, config: configuration, stdio: stdio)
return ClientProcessImpl(containerId: self.id, processId: id, client: client)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to exec in container",
cause: error
)
}
}
let request = XPCMessage(route: .containerCreateProcess)
request.set(key: .id, value: self.id)
request.set(key: .processIdentifier, value: id)

/// Send or "kill" a signal to the initial process of the container.
/// Kill does not wait for the process to exit, it only delivers the signal.
public func kill(_ signal: Int32) async throws {
do {
let client = self.sandboxClient
try await client.kill(self.id, signal: Int64(signal))
let data = try JSONEncoder().encode(configuration)
request.set(key: .processConfig, value: data)

for (i, h) in stdio.enumerated() {
Comment thread
dcantah marked this conversation as resolved.
let key: XPCKeys = try {
switch i {
case 0: .stdin
case 1: .stdout
case 2: .stderr
default:
throw ContainerizationError(.invalidArgument, message: "invalid fd \(i)")
}
}()

if let h {
request.set(key: key, value: h)
}
}

let client = Self.newXPCClient()
try await client.send(request)
return ClientProcessImpl(containerId: self.id, processId: id, xpcClient: client)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to kill container \(self.id)",
message: "failed to create process in container",
cause: error
)
}
}

public func logs() async throws -> [FileHandle] {
do {
let client = XPCClient(service: Self.serviceIdentifier)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerLogs)
request.set(key: .id, value: self.id)

Expand All@@ -242,15 +297,27 @@ extension ClientContainer {
}

public func dial(_ port: UInt32) async throws -> FileHandle {
let request = XPCMessage(route: .containerDial)
request.set(key: .id, value: self.id)
request.set(key: .port, value: UInt64(port))

let client = Self.newXPCClient()
let response: XPCMessage
do {
let client = self.sandboxClient
return try await client.dial(port)
response = try await client.send(request)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to dial \(port) in container \(self.id)",
message: "failed to dial port \(port) on container",
cause: error
)
}
guard let fh = response.fileHandle(key: .fd) else {
throw ContainerizationError(
.internalError,
message: "failed to get fd for vsock port \(port)"
)
}
return fh
}
}
91 changes: 39 additions & 52 deletions Sources/ContainerClient/Core/ClientProcess.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,7 @@ public protocol ClientProcess: Sendable {
func start() async throws
/// Send a terminal resize request to the process `id`.
func resize(_ size: Terminal.Size) async throws
/// Send or "kill" a signal to the process `id`.
/// Send a signal to the process `id`.
/// Kill does not wait for the process to exit, it only delivers the signal.
func kill(_ signal: Int32) async throws
/// Wait for the process `id` to complete and return its exit code.
Expand All@@ -45,79 +45,66 @@ public protocol ClientProcess: Sendable {

struct ClientProcessImpl: ClientProcess, Sendable {
static let serviceIdentifier = "com.apple.container.apiserver"

/// ID of the process.
public var id: String {
Comment thread
dcantah marked this conversation as resolved.
processId ?? containerId
}

/// Identifier of the container.
public let containerId: String

private let client: SandboxClient

/// Identifier of a process. That is running inside of a container.
/// This field is nil if the process this objects refers to is the
/// init process of the container.
public let processId: String?

public var id: String {
processId ?? containerId
}
private let xpcClient: XPCClient

init(containerId: String, processId: String? = nil, client: SandboxClient) {
init(containerId: String, processId: String? = nil, xpcClient: XPCClient) {
self.containerId = containerId
self.processId = processId
self.client = client
self.xpcClient = xpcClient
}

/// Start the container and return the initial process.
/// Start the process.
public func start() async throws {
do {
let client = self.client
try await client.startProcess(self.id)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to start container",
cause: error
)
}
let request = XPCMessage(route: .containerStartProcess)
request.set(key: .id, value: containerId)
request.set(key: .processIdentifier, value: id)

try await xpcClient.send(request)
}

/// Send a signal to the process.
public func kill(_ signal: Int32) async throws {
do {

let client = self.client
try await client.kill(self.id, signal: Int64(signal))
} catch {
throw ContainerizationError(
.internalError,
message: "failed to kill process",
cause: error
)
}
}
let request = XPCMessage(route: .containerKill)
request.set(key: .id, value: containerId)
request.set(key: .processIdentifier, value: id)
request.set(key: .signal, value: Int64(signal))

public func resize(_ size: ContainerizationOS.Terminal.Size) async throws {
do {
try await xpcClient.send(request)
}

let client = self.client
try await client.resize(self.id, size: size)
/// Resize the processes PTY if it has one.
public func resize(_ size: Terminal.Size) async throws {
let request = XPCMessage(route: .containerResize)
request.set(key: .id, value: containerId)
request.set(key: .processIdentifier, value: id)
request.set(key: .width, value: UInt64(size.width))
request.set(key: .height, value: UInt64(size.height))

} catch {
throw ContainerizationError(
.internalError,
message: "failed to resize process",
cause: error
)
}
try await xpcClient.send(request)
}

/// Wait for the process to exit.
public func wait() async throws -> Int32 {
do {
let client = self.client
return try await client.wait(self.id)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to wait on process",
cause: error
)
}
let request = XPCMessage(route: .containerWait)
request.set(key: .id, value: containerId)
request.set(key: .processIdentifier, value: id)

let response = try await xpcClient.send(request)
let code = response.int64(key: .exitCode)
return Int32(code)
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
20 changes: 0 additions & 20 deletions Sources/ContainerClient/ContainerEvents.swift

This file was deleted.

157 changes: 112 additions & 45 deletions Sources/ContainerClient/Core/ClientContainer.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,10 +25,6 @@ import TerminalProgress
public struct ClientContainer: Sendable, Codable {
static let serviceIdentifier = "com.apple.container.apiserver"

private var sandboxClient: SandboxClient {
SandboxClient(id: configuration.id, runtime: configuration.runtimeHandler)
}

/// Identifier of the container.
public var id: String {
configuration.id
Expand DownExpand Up@@ -58,14 +54,10 @@ public struct ClientContainer: Sendable, Codable {
self.status = snapshot.status
self.networks = snapshot.networks
}

public var initProcess: ClientProcess {
ClientProcessImpl(containerId: self.id, client: self.sandboxClient)
}
}

extension ClientContainer {
private static func newClient() -> XPCClient {
private static func newXPCClient() -> XPCClient {
XPCClient(service: serviceIdentifier)
}

Expand All@@ -84,8 +76,8 @@ extension ClientContainer {
kernel: Kernel
) async throws -> ClientContainer {
do {
let client = Self.newClient()
let request = XPCMessage(route: .createContainer)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerCreate)

let data = try JSONEncoder().encode(configuration)
let kdata = try JSONEncoder().encode(kernel)
Expand All@@ -107,8 +99,8 @@ extension ClientContainer {

public static func list() async throws -> [ClientContainer] {
do {
let client = Self.newClient()
let request = XPCMessage(route: .listContainer)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerList)

let response = try await xpcSend(
client: client,
Expand DownExpand Up@@ -145,16 +137,72 @@ extension ClientContainer {

extension ClientContainer {
public func bootstrap(stdio: [FileHandle?]) async throws -> ClientProcess {
let client = self.sandboxClient
try await client.bootstrap(stdio: stdio)
return ClientProcessImpl(containerId: self.id, client: self.sandboxClient)
let request = XPCMessage(route: .containerBootstrap)
let client = Self.newXPCClient()

for (i, h) in stdio.enumerated() {
let key: XPCKeys = try {
switch i {
case 0: .stdin
case 1: .stdout
case 2: .stderr
default:
throw ContainerizationError(.invalidArgument, message: "invalid fd \(i)")
}
}()

if let h {
request.set(key: key, value: h)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This code repeats 4 times. Consider extracting it into helper methods in XPCMessage:

 static func stdioKey(for index: Int) throws -> XPCKeys {
switch index {
case 0: return .stdin
case 1: return .stdout
case 2: return .stderr
default:
throw ContainerizationError(.invalidArgument, message: "invalid fd \(index)")
}
}
static func setStdioHandles(on request: XPCMessage, stdio: [FileHandle?]) throws {
for (index, handle) in stdio.enumerated() {
if let handle {
request.set(key: stdioKey(for: index), value: handle)
}
}
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

John had commented the same, I'm going to do that (and some other cleanups) in a followup


do {
request.set(key: .id, value: self.id)
try await client.send(request)
return ClientProcessImpl(containerId: self.id, xpcClient: client)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to bootstrap container",
cause: error
)
}
}

public func kill(_ signal: Int32) async throws {
do {
let request = XPCMessage(route: .containerKill)
request.set(key: .id, value: self.id)
request.set(key: .processIdentifier, value: self.id)
request.set(key: .signal, value: Int64(signal))

let client = Self.newXPCClient()
try await client.send(request)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to kill container",
cause: error
)
}
}

/// Stop the container and all processes currently executing inside.
public func stop(opts: ContainerStopOptions = ContainerStopOptions.default) async throws {
do {
let client = self.sandboxClient
try await client.stop(options: opts)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerStop)
let data = try JSONEncoder().encode(opts)
request.set(key: .id, value: self.id)
request.set(key: .stopOptions, value: data)

// Stop is somewhat more prone to hanging than other commands given it
// has quite a bit of `wait()`'s down the chain to make sure the container actually
// exited. To combat a potential hang, lets timeout if we don't return in a small
// time period after the actual stop timeout sent via .stopOptions (the time
// until we send SIGKILL after SIGTERM if the container still hasn't exited).
let responseTimeout = Duration(.seconds(Int64(opts.timeoutInSeconds + 3)))
Comment thread
dcantah marked this conversation as resolved.
try await client.send(request, responseTimeout: responseTimeout)
} catch {
throw ContainerizationError(
.internalError,
Expand All@@ -167,8 +215,8 @@ extension ClientContainer {
/// Delete the container along with any resources.
public func delete(force: Bool = false) async throws {
do {
let client = XPCClient(service: Self.serviceIdentifier)
let request = XPCMessage(route: .deleteContainer)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerDelete)
request.set(key: .id, value: self.id)
request.set(key: .forceDelete, value: force)
try await client.send(request)
Expand All@@ -180,46 +228,53 @@ extension ClientContainer {
)
}
}
}

extension ClientContainer {
/// Execute a new process inside a running container.
/// Create a new process inside a running container. The process is in a
/// created state and must still be started.
public func createProcess(
id: String,
configuration: ProcessConfiguration,
stdio: [FileHandle?]
) async throws -> ClientProcess {
do {
let client = self.sandboxClient
try await client.createProcess(id, config: configuration, stdio: stdio)
return ClientProcessImpl(containerId: self.id, processId: id, client: client)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to exec in container",
cause: error
)
}
}
let request = XPCMessage(route: .containerCreateProcess)
request.set(key: .id, value: self.id)
request.set(key: .processIdentifier, value: id)

/// Send or "kill" a signal to the initial process of the container.
/// Kill does not wait for the process to exit, it only delivers the signal.
public func kill(_ signal: Int32) async throws {
do {
let client = self.sandboxClient
try await client.kill(self.id, signal: Int64(signal))
let data = try JSONEncoder().encode(configuration)
request.set(key: .processConfig, value: data)

for (i, h) in stdio.enumerated() {
Comment thread
dcantah marked this conversation as resolved.
let key: XPCKeys = try {
switch i {
case 0: .stdin
case 1: .stdout
case 2: .stderr
default:
throw ContainerizationError(.invalidArgument, message: "invalid fd \(i)")
}
}()

if let h {
request.set(key: key, value: h)
}
}

let client = Self.newXPCClient()
try await client.send(request)
return ClientProcessImpl(containerId: self.id, processId: id, xpcClient: client)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to kill container \(self.id)",
message: "failed to create process in container",
cause: error
)
}
}

public func logs() async throws -> [FileHandle] {
do {
let client = XPCClient(service: Self.serviceIdentifier)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerLogs)
request.set(key: .id, value: self.id)

Expand All@@ -242,15 +297,27 @@ extension ClientContainer {
}

public func dial(_ port: UInt32) async throws -> FileHandle {
let request = XPCMessage(route: .containerDial)
request.set(key: .id, value: self.id)
request.set(key: .port, value: UInt64(port))

let client = Self.newXPCClient()
let response: XPCMessage
do {
let client = self.sandboxClient
return try await client.dial(port)
response = try await client.send(request)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to dial \(port) in container \(self.id)",
message: "failed to dial port \(port) on container",
cause: error
)
}
guard let fh = response.fileHandle(key: .fd) else {
throw ContainerizationError(
.internalError,
message: "failed to get fd for vsock port \(port)"
)
}
return fh
}
}
91 changes: 39 additions & 52 deletions Sources/ContainerClient/Core/ClientProcess.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,7 @@ public protocol ClientProcess: Sendable {
func start() async throws
/// Send a terminal resize request to the process `id`.
func resize(_ size: Terminal.Size) async throws
/// Send or "kill" a signal to the process `id`.
/// Send a signal to the process `id`.
/// Kill does not wait for the process to exit, it only delivers the signal.
func kill(_ signal: Int32) async throws
/// Wait for the process `id` to complete and return its exit code.
Expand All@@ -45,79 +45,66 @@ public protocol ClientProcess: Sendable {

struct ClientProcessImpl: ClientProcess, Sendable {
static let serviceIdentifier = "com.apple.container.apiserver"

/// ID of the process.
public var id: String {
Comment thread
dcantah marked this conversation as resolved.
processId ?? containerId
}

/// Identifier of the container.
public let containerId: String

private let client: SandboxClient

/// Identifier of a process. That is running inside of a container.
/// This field is nil if the process this objects refers to is the
/// init process of the container.
public let processId: String?

public var id: String {
processId ?? containerId
}
private let xpcClient: XPCClient

init(containerId: String, processId: String? = nil, client: SandboxClient) {
init(containerId: String, processId: String? = nil, xpcClient: XPCClient) {
self.containerId = containerId
self.processId = processId
self.client = client
self.xpcClient = xpcClient
}

/// Start the container and return the initial process.
/// Start the process.
public func start() async throws {
do {
let client = self.client
try await client.startProcess(self.id)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to start container",
cause: error
)
}
let request = XPCMessage(route: .containerStartProcess)
request.set(key: .id, value: containerId)
request.set(key: .processIdentifier, value: id)

try await xpcClient.send(request)
}

/// Send a signal to the process.
public func kill(_ signal: Int32) async throws {
do {

let client = self.client
try await client.kill(self.id, signal: Int64(signal))
} catch {
throw ContainerizationError(
.internalError,
message: "failed to kill process",
cause: error
)
}
}
let request = XPCMessage(route: .containerKill)
request.set(key: .id, value: containerId)
request.set(key: .processIdentifier, value: id)
request.set(key: .signal, value: Int64(signal))

public func resize(_ size: ContainerizationOS.Terminal.Size) async throws {
do {
try await xpcClient.send(request)
}

let client = self.client
try await client.resize(self.id, size: size)
/// Resize the processes PTY if it has one.
public func resize(_ size: Terminal.Size) async throws {
let request = XPCMessage(route: .containerResize)
request.set(key: .id, value: containerId)
request.set(key: .processIdentifier, value: id)
request.set(key: .width, value: UInt64(size.width))
request.set(key: .height, value: UInt64(size.height))

} catch {
throw ContainerizationError(
.internalError,
message: "failed to resize process",
cause: error
)
}
try await xpcClient.send(request)
}

/// Wait for the process to exit.
public func wait() async throws -> Int32 {
do {
let client = self.client
return try await client.wait(self.id)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to wait on process",
cause: error
)
}
let request = XPCMessage(route: .containerWait)
request.set(key: .id, value: containerId)
request.set(key: .processIdentifier, value: id)

let response = try await xpcClient.send(request)
let code = response.int64(key: .exitCode)
return Int32(code)
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
20 changes: 0 additions & 20 deletions Sources/ContainerClient/ContainerEvents.swift

This file was deleted.

157 changes: 112 additions & 45 deletions Sources/ContainerClient/Core/ClientContainer.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,10 +25,6 @@ import TerminalProgress
public struct ClientContainer: Sendable, Codable {
static let serviceIdentifier = "com.apple.container.apiserver"

private var sandboxClient: SandboxClient {
SandboxClient(id: configuration.id, runtime: configuration.runtimeHandler)
}

/// Identifier of the container.
public var id: String {
configuration.id
Expand DownExpand Up@@ -58,14 +54,10 @@ public struct ClientContainer: Sendable, Codable {
self.status = snapshot.status
self.networks = snapshot.networks
}

public var initProcess: ClientProcess {
ClientProcessImpl(containerId: self.id, client: self.sandboxClient)
}
}

extension ClientContainer {
private static func newClient() -> XPCClient {
private static func newXPCClient() -> XPCClient {
XPCClient(service: serviceIdentifier)
}

Expand All@@ -84,8 +76,8 @@ extension ClientContainer {
kernel: Kernel
) async throws -> ClientContainer {
do {
let client = Self.newClient()
let request = XPCMessage(route: .createContainer)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerCreate)

let data = try JSONEncoder().encode(configuration)
let kdata = try JSONEncoder().encode(kernel)
Expand All@@ -107,8 +99,8 @@ extension ClientContainer {

public static func list() async throws -> [ClientContainer] {
do {
let client = Self.newClient()
let request = XPCMessage(route: .listContainer)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerList)

let response = try await xpcSend(
client: client,
Expand DownExpand Up@@ -145,16 +137,72 @@ extension ClientContainer {

extension ClientContainer {
public func bootstrap(stdio: [FileHandle?]) async throws -> ClientProcess {
let client = self.sandboxClient
try await client.bootstrap(stdio: stdio)
return ClientProcessImpl(containerId: self.id, client: self.sandboxClient)
let request = XPCMessage(route: .containerBootstrap)
let client = Self.newXPCClient()

for (i, h) in stdio.enumerated() {
let key: XPCKeys = try {
switch i {
case 0: .stdin
case 1: .stdout
case 2: .stderr
default:
throw ContainerizationError(.invalidArgument, message: "invalid fd \(i)")
}
}()

if let h {
request.set(key: key, value: h)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This code repeats 4 times. Consider extracting it into helper methods in XPCMessage:

 static func stdioKey(for index: Int) throws -> XPCKeys {
switch index {
case 0: return .stdin
case 1: return .stdout
case 2: return .stderr
default:
throw ContainerizationError(.invalidArgument, message: "invalid fd \(index)")
}
}
static func setStdioHandles(on request: XPCMessage, stdio: [FileHandle?]) throws {
for (index, handle) in stdio.enumerated() {
if let handle {
request.set(key: stdioKey(for: index), value: handle)
}
}
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

John had commented the same, I'm going to do that (and some other cleanups) in a followup


do {
request.set(key: .id, value: self.id)
try await client.send(request)
return ClientProcessImpl(containerId: self.id, xpcClient: client)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to bootstrap container",
cause: error
)
}
}

public func kill(_ signal: Int32) async throws {
do {
let request = XPCMessage(route: .containerKill)
request.set(key: .id, value: self.id)
request.set(key: .processIdentifier, value: self.id)
request.set(key: .signal, value: Int64(signal))

let client = Self.newXPCClient()
try await client.send(request)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to kill container",
cause: error
)
}
}

/// Stop the container and all processes currently executing inside.
public func stop(opts: ContainerStopOptions = ContainerStopOptions.default) async throws {
do {
let client = self.sandboxClient
try await client.stop(options: opts)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerStop)
let data = try JSONEncoder().encode(opts)
request.set(key: .id, value: self.id)
request.set(key: .stopOptions, value: data)

// Stop is somewhat more prone to hanging than other commands given it
// has quite a bit of `wait()`'s down the chain to make sure the container actually
// exited. To combat a potential hang, lets timeout if we don't return in a small
// time period after the actual stop timeout sent via .stopOptions (the time
// until we send SIGKILL after SIGTERM if the container still hasn't exited).
let responseTimeout = Duration(.seconds(Int64(opts.timeoutInSeconds + 3)))
Comment thread
dcantah marked this conversation as resolved.
try await client.send(request, responseTimeout: responseTimeout)
} catch {
throw ContainerizationError(
.internalError,
Expand All@@ -167,8 +215,8 @@ extension ClientContainer {
/// Delete the container along with any resources.
public func delete(force: Bool = false) async throws {
do {
let client = XPCClient(service: Self.serviceIdentifier)
let request = XPCMessage(route: .deleteContainer)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerDelete)
request.set(key: .id, value: self.id)
request.set(key: .forceDelete, value: force)
try await client.send(request)
Expand All@@ -180,46 +228,53 @@ extension ClientContainer {
)
}
}
}

extension ClientContainer {
/// Execute a new process inside a running container.
/// Create a new process inside a running container. The process is in a
/// created state and must still be started.
public func createProcess(
id: String,
configuration: ProcessConfiguration,
stdio: [FileHandle?]
) async throws -> ClientProcess {
do {
let client = self.sandboxClient
try await client.createProcess(id, config: configuration, stdio: stdio)
return ClientProcessImpl(containerId: self.id, processId: id, client: client)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to exec in container",
cause: error
)
}
}
let request = XPCMessage(route: .containerCreateProcess)
request.set(key: .id, value: self.id)
request.set(key: .processIdentifier, value: id)

/// Send or "kill" a signal to the initial process of the container.
/// Kill does not wait for the process to exit, it only delivers the signal.
public func kill(_ signal: Int32) async throws {
do {
let client = self.sandboxClient
try await client.kill(self.id, signal: Int64(signal))
let data = try JSONEncoder().encode(configuration)
request.set(key: .processConfig, value: data)

for (i, h) in stdio.enumerated() {
Comment thread
dcantah marked this conversation as resolved.
let key: XPCKeys = try {
switch i {
case 0: .stdin
case 1: .stdout
case 2: .stderr
default:
throw ContainerizationError(.invalidArgument, message: "invalid fd \(i)")
}
}()

if let h {
request.set(key: key, value: h)
}
}

let client = Self.newXPCClient()
try await client.send(request)
return ClientProcessImpl(containerId: self.id, processId: id, xpcClient: client)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to kill container \(self.id)",
message: "failed to create process in container",
cause: error
)
}
}

public func logs() async throws -> [FileHandle] {
do {
let client = XPCClient(service: Self.serviceIdentifier)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerLogs)
request.set(key: .id, value: self.id)

Expand All@@ -242,15 +297,27 @@ extension ClientContainer {
}

public func dial(_ port: UInt32) async throws -> FileHandle {
let request = XPCMessage(route: .containerDial)
request.set(key: .id, value: self.id)
request.set(key: .port, value: UInt64(port))

let client = Self.newXPCClient()
let response: XPCMessage
do {
let client = self.sandboxClient
return try await client.dial(port)
response = try await client.send(request)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to dial \(port) in container \(self.id)",
message: "failed to dial port \(port) on container",
cause: error
)
}
guard let fh = response.fileHandle(key: .fd) else {
throw ContainerizationError(
.internalError,
message: "failed to get fd for vsock port \(port)"
)
}
return fh
}
}
91 changes: 39 additions & 52 deletions Sources/ContainerClient/Core/ClientProcess.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,7 @@ public protocol ClientProcess: Sendable {
func start() async throws
/// Send a terminal resize request to the process `id`.
func resize(_ size: Terminal.Size) async throws
/// Send or "kill" a signal to the process `id`.
/// Send a signal to the process `id`.
/// Kill does not wait for the process to exit, it only delivers the signal.
func kill(_ signal: Int32) async throws
/// Wait for the process `id` to complete and return its exit code.
Expand All@@ -45,79 +45,66 @@ public protocol ClientProcess: Sendable {

struct ClientProcessImpl: ClientProcess, Sendable {
static let serviceIdentifier = "com.apple.container.apiserver"

/// ID of the process.
public var id: String {
Comment thread
dcantah marked this conversation as resolved.
processId ?? containerId
}

/// Identifier of the container.
public let containerId: String

private let client: SandboxClient

/// Identifier of a process. That is running inside of a container.
/// This field is nil if the process this objects refers to is the
/// init process of the container.
public let processId: String?

public var id: String {
processId ?? containerId
}
private let xpcClient: XPCClient

init(containerId: String, processId: String? = nil, client: SandboxClient) {
init(containerId: String, processId: String? = nil, xpcClient: XPCClient) {
self.containerId = containerId
self.processId = processId
self.client = client
self.xpcClient = xpcClient
}

/// Start the container and return the initial process.
/// Start the process.
public func start() async throws {
do {
let client = self.client
try await client.startProcess(self.id)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to start container",
cause: error
)
}
let request = XPCMessage(route: .containerStartProcess)
request.set(key: .id, value: containerId)
request.set(key: .processIdentifier, value: id)

try await xpcClient.send(request)
}

/// Send a signal to the process.
public func kill(_ signal: Int32) async throws {
do {

let client = self.client
try await client.kill(self.id, signal: Int64(signal))
} catch {
throw ContainerizationError(
.internalError,
message: "failed to kill process",
cause: error
)
}
}
let request = XPCMessage(route: .containerKill)
request.set(key: .id, value: containerId)
request.set(key: .processIdentifier, value: id)
request.set(key: .signal, value: Int64(signal))

public func resize(_ size: ContainerizationOS.Terminal.Size) async throws {
do {
try await xpcClient.send(request)
}

let client = self.client
try await client.resize(self.id, size: size)
/// Resize the processes PTY if it has one.
public func resize(_ size: Terminal.Size) async throws {
let request = XPCMessage(route: .containerResize)
request.set(key: .id, value: containerId)
request.set(key: .processIdentifier, value: id)
request.set(key: .width, value: UInt64(size.width))
request.set(key: .height, value: UInt64(size.height))

} catch {
throw ContainerizationError(
.internalError,
message: "failed to resize process",
cause: error
)
}
try await xpcClient.send(request)
}

/// Wait for the process to exit.
public func wait() async throws -> Int32 {
do {
let client = self.client
return try await client.wait(self.id)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to wait on process",
cause: error
)
}
let request = XPCMessage(route: .containerWait)
request.set(key: .id, value: containerId)
request.set(key: .processIdentifier, value: id)

let response = try await xpcClient.send(request)
let code = response.int64(key: .exitCode)
return Int32(code)
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
20 changes: 0 additions & 20 deletions Sources/ContainerClient/ContainerEvents.swift

This file was deleted.

157 changes: 112 additions & 45 deletions Sources/ContainerClient/Core/ClientContainer.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,10 +25,6 @@ import TerminalProgress
public struct ClientContainer: Sendable, Codable {
static let serviceIdentifier = "com.apple.container.apiserver"

private var sandboxClient: SandboxClient {
SandboxClient(id: configuration.id, runtime: configuration.runtimeHandler)
}

/// Identifier of the container.
public var id: String {
configuration.id
Expand DownExpand Up@@ -58,14 +54,10 @@ public struct ClientContainer: Sendable, Codable {
self.status = snapshot.status
self.networks = snapshot.networks
}

public var initProcess: ClientProcess {
ClientProcessImpl(containerId: self.id, client: self.sandboxClient)
}
}

extension ClientContainer {
private static func newClient() -> XPCClient {
private static func newXPCClient() -> XPCClient {
XPCClient(service: serviceIdentifier)
}

Expand All@@ -84,8 +76,8 @@ extension ClientContainer {
kernel: Kernel
) async throws -> ClientContainer {
do {
let client = Self.newClient()
let request = XPCMessage(route: .createContainer)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerCreate)

let data = try JSONEncoder().encode(configuration)
let kdata = try JSONEncoder().encode(kernel)
Expand All@@ -107,8 +99,8 @@ extension ClientContainer {

public static func list() async throws -> [ClientContainer] {
do {
let client = Self.newClient()
let request = XPCMessage(route: .listContainer)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerList)

let response = try await xpcSend(
client: client,
Expand DownExpand Up@@ -145,16 +137,72 @@ extension ClientContainer {

extension ClientContainer {
public func bootstrap(stdio: [FileHandle?]) async throws -> ClientProcess {
let client = self.sandboxClient
try await client.bootstrap(stdio: stdio)
return ClientProcessImpl(containerId: self.id, client: self.sandboxClient)
let request = XPCMessage(route: .containerBootstrap)
let client = Self.newXPCClient()

for (i, h) in stdio.enumerated() {
let key: XPCKeys = try {
switch i {
case 0: .stdin
case 1: .stdout
case 2: .stderr
default:
throw ContainerizationError(.invalidArgument, message: "invalid fd \(i)")
}
}()

if let h {
request.set(key: key, value: h)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This code repeats 4 times. Consider extracting it into helper methods in XPCMessage:

 static func stdioKey(for index: Int) throws -> XPCKeys {
switch index {
case 0: return .stdin
case 1: return .stdout
case 2: return .stderr
default:
throw ContainerizationError(.invalidArgument, message: "invalid fd \(index)")
}
}
static func setStdioHandles(on request: XPCMessage, stdio: [FileHandle?]) throws {
for (index, handle) in stdio.enumerated() {
if let handle {
request.set(key: stdioKey(for: index), value: handle)
}
}
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

John had commented the same, I'm going to do that (and some other cleanups) in a followup


do {
request.set(key: .id, value: self.id)
try await client.send(request)
return ClientProcessImpl(containerId: self.id, xpcClient: client)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to bootstrap container",
cause: error
)
}
}

public func kill(_ signal: Int32) async throws {
do {
let request = XPCMessage(route: .containerKill)
request.set(key: .id, value: self.id)
request.set(key: .processIdentifier, value: self.id)
request.set(key: .signal, value: Int64(signal))

let client = Self.newXPCClient()
try await client.send(request)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to kill container",
cause: error
)
}
}

/// Stop the container and all processes currently executing inside.
public func stop(opts: ContainerStopOptions = ContainerStopOptions.default) async throws {
do {
let client = self.sandboxClient
try await client.stop(options: opts)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerStop)
let data = try JSONEncoder().encode(opts)
request.set(key: .id, value: self.id)
request.set(key: .stopOptions, value: data)

// Stop is somewhat more prone to hanging than other commands given it
// has quite a bit of `wait()`'s down the chain to make sure the container actually
// exited. To combat a potential hang, lets timeout if we don't return in a small
// time period after the actual stop timeout sent via .stopOptions (the time
// until we send SIGKILL after SIGTERM if the container still hasn't exited).
let responseTimeout = Duration(.seconds(Int64(opts.timeoutInSeconds + 3)))
Comment thread
dcantah marked this conversation as resolved.
try await client.send(request, responseTimeout: responseTimeout)
} catch {
throw ContainerizationError(
.internalError,
Expand All@@ -167,8 +215,8 @@ extension ClientContainer {
/// Delete the container along with any resources.
public func delete(force: Bool = false) async throws {
do {
let client = XPCClient(service: Self.serviceIdentifier)
let request = XPCMessage(route: .deleteContainer)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerDelete)
request.set(key: .id, value: self.id)
request.set(key: .forceDelete, value: force)
try await client.send(request)
Expand All@@ -180,46 +228,53 @@ extension ClientContainer {
)
}
}
}

extension ClientContainer {
/// Execute a new process inside a running container.
/// Create a new process inside a running container. The process is in a
/// created state and must still be started.
public func createProcess(
id: String,
configuration: ProcessConfiguration,
stdio: [FileHandle?]
) async throws -> ClientProcess {
do {
let client = self.sandboxClient
try await client.createProcess(id, config: configuration, stdio: stdio)
return ClientProcessImpl(containerId: self.id, processId: id, client: client)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to exec in container",
cause: error
)
}
}
let request = XPCMessage(route: .containerCreateProcess)
request.set(key: .id, value: self.id)
request.set(key: .processIdentifier, value: id)

/// Send or "kill" a signal to the initial process of the container.
/// Kill does not wait for the process to exit, it only delivers the signal.
public func kill(_ signal: Int32) async throws {
do {
let client = self.sandboxClient
try await client.kill(self.id, signal: Int64(signal))
let data = try JSONEncoder().encode(configuration)
request.set(key: .processConfig, value: data)

for (i, h) in stdio.enumerated() {
Comment thread
dcantah marked this conversation as resolved.
let key: XPCKeys = try {
switch i {
case 0: .stdin
case 1: .stdout
case 2: .stderr
default:
throw ContainerizationError(.invalidArgument, message: "invalid fd \(i)")
}
}()

if let h {
request.set(key: key, value: h)
}
}

let client = Self.newXPCClient()
try await client.send(request)
return ClientProcessImpl(containerId: self.id, processId: id, xpcClient: client)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to kill container \(self.id)",
message: "failed to create process in container",
cause: error
)
}
}

public func logs() async throws -> [FileHandle] {
do {
let client = XPCClient(service: Self.serviceIdentifier)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerLogs)
request.set(key: .id, value: self.id)

Expand All@@ -242,15 +297,27 @@ extension ClientContainer {
}

public func dial(_ port: UInt32) async throws -> FileHandle {
let request = XPCMessage(route: .containerDial)
request.set(key: .id, value: self.id)
request.set(key: .port, value: UInt64(port))

let client = Self.newXPCClient()
let response: XPCMessage
do {
let client = self.sandboxClient
return try await client.dial(port)
response = try await client.send(request)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to dial \(port) in container \(self.id)",
message: "failed to dial port \(port) on container",
cause: error
)
}
guard let fh = response.fileHandle(key: .fd) else {
throw ContainerizationError(
.internalError,
message: "failed to get fd for vsock port \(port)"
)
}
return fh
}
}
91 changes: 39 additions & 52 deletions Sources/ContainerClient/Core/ClientProcess.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,7 @@ public protocol ClientProcess: Sendable {
func start() async throws
/// Send a terminal resize request to the process `id`.
func resize(_ size: Terminal.Size) async throws
/// Send or "kill" a signal to the process `id`.
/// Send a signal to the process `id`.
/// Kill does not wait for the process to exit, it only delivers the signal.
func kill(_ signal: Int32) async throws
/// Wait for the process `id` to complete and return its exit code.
Expand All@@ -45,79 +45,66 @@ public protocol ClientProcess: Sendable {

struct ClientProcessImpl: ClientProcess, Sendable {
static let serviceIdentifier = "com.apple.container.apiserver"

/// ID of the process.
public var id: String {
Comment thread
dcantah marked this conversation as resolved.
processId ?? containerId
}

/// Identifier of the container.
public let containerId: String

private let client: SandboxClient

/// Identifier of a process. That is running inside of a container.
/// This field is nil if the process this objects refers to is the
/// init process of the container.
public let processId: String?

public var id: String {
processId ?? containerId
}
private let xpcClient: XPCClient

init(containerId: String, processId: String? = nil, client: SandboxClient) {
init(containerId: String, processId: String? = nil, xpcClient: XPCClient) {
self.containerId = containerId
self.processId = processId
self.client = client
self.xpcClient = xpcClient
}

/// Start the container and return the initial process.
/// Start the process.
public func start() async throws {
do {
let client = self.client
try await client.startProcess(self.id)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to start container",
cause: error
)
}
let request = XPCMessage(route: .containerStartProcess)
request.set(key: .id, value: containerId)
request.set(key: .processIdentifier, value: id)

try await xpcClient.send(request)
}

/// Send a signal to the process.
public func kill(_ signal: Int32) async throws {
do {

let client = self.client
try await client.kill(self.id, signal: Int64(signal))
} catch {
throw ContainerizationError(
.internalError,
message: "failed to kill process",
cause: error
)
}
}
let request = XPCMessage(route: .containerKill)
request.set(key: .id, value: containerId)
request.set(key: .processIdentifier, value: id)
request.set(key: .signal, value: Int64(signal))

public func resize(_ size: ContainerizationOS.Terminal.Size) async throws {
do {
try await xpcClient.send(request)
}

let client = self.client
try await client.resize(self.id, size: size)
/// Resize the processes PTY if it has one.
public func resize(_ size: Terminal.Size) async throws {
let request = XPCMessage(route: .containerResize)
request.set(key: .id, value: containerId)
request.set(key: .processIdentifier, value: id)
request.set(key: .width, value: UInt64(size.width))
request.set(key: .height, value: UInt64(size.height))

} catch {
throw ContainerizationError(
.internalError,
message: "failed to resize process",
cause: error
)
}
try await xpcClient.send(request)
}

/// Wait for the process to exit.
public func wait() async throws -> Int32 {
do {
let client = self.client
return try await client.wait(self.id)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to wait on process",
cause: error
)
}
let request = XPCMessage(route: .containerWait)
request.set(key: .id, value: containerId)
request.set(key: .processIdentifier, value: id)

let response = try await xpcClient.send(request)
let code = response.int64(key: .exitCode)
return Int32(code)
}
}
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
20 changes: 0 additions & 20 deletions Sources/ContainerClient/ContainerEvents.swift

This file was deleted.

157 changes: 112 additions & 45 deletions Sources/ContainerClient/Core/ClientContainer.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,10 +25,6 @@ import TerminalProgress
public struct ClientContainer: Sendable, Codable {
static let serviceIdentifier = "com.apple.container.apiserver"

private var sandboxClient: SandboxClient {
SandboxClient(id: configuration.id, runtime: configuration.runtimeHandler)
}

/// Identifier of the container.
public var id: String {
configuration.id
Expand DownExpand Up@@ -58,14 +54,10 @@ public struct ClientContainer: Sendable, Codable {
self.status = snapshot.status
self.networks = snapshot.networks
}

public var initProcess: ClientProcess {
ClientProcessImpl(containerId: self.id, client: self.sandboxClient)
}
}

extension ClientContainer {
private static func newClient() -> XPCClient {
private static func newXPCClient() -> XPCClient {
XPCClient(service: serviceIdentifier)
}

Expand All@@ -84,8 +76,8 @@ extension ClientContainer {
kernel: Kernel
) async throws -> ClientContainer {
do {
let client = Self.newClient()
let request = XPCMessage(route: .createContainer)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerCreate)

let data = try JSONEncoder().encode(configuration)
let kdata = try JSONEncoder().encode(kernel)
Expand All@@ -107,8 +99,8 @@ extension ClientContainer {

public static func list() async throws -> [ClientContainer] {
do {
let client = Self.newClient()
let request = XPCMessage(route: .listContainer)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerList)

let response = try await xpcSend(
client: client,
Expand DownExpand Up@@ -145,16 +137,72 @@ extension ClientContainer {

extension ClientContainer {
public func bootstrap(stdio: [FileHandle?]) async throws -> ClientProcess {
let client = self.sandboxClient
try await client.bootstrap(stdio: stdio)
return ClientProcessImpl(containerId: self.id, client: self.sandboxClient)
let request = XPCMessage(route: .containerBootstrap)
let client = Self.newXPCClient()

for (i, h) in stdio.enumerated() {
let key: XPCKeys = try {
switch i {
case 0: .stdin
case 1: .stdout
case 2: .stderr
default:
throw ContainerizationError(.invalidArgument, message: "invalid fd \(i)")
}
}()

if let h {
request.set(key: key, value: h)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This code repeats 4 times. Consider extracting it into helper methods in XPCMessage:

 static func stdioKey(for index: Int) throws -> XPCKeys {
switch index {
case 0: return .stdin
case 1: return .stdout
case 2: return .stderr
default:
throw ContainerizationError(.invalidArgument, message: "invalid fd \(index)")
}
}
static func setStdioHandles(on request: XPCMessage, stdio: [FileHandle?]) throws {
for (index, handle) in stdio.enumerated() {
if let handle {
request.set(key: stdioKey(for: index), value: handle)
}
}
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

John had commented the same, I'm going to do that (and some other cleanups) in a followup


do {
request.set(key: .id, value: self.id)
try await client.send(request)
return ClientProcessImpl(containerId: self.id, xpcClient: client)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to bootstrap container",
cause: error
)
}
}

public func kill(_ signal: Int32) async throws {
do {
let request = XPCMessage(route: .containerKill)
request.set(key: .id, value: self.id)
request.set(key: .processIdentifier, value: self.id)
request.set(key: .signal, value: Int64(signal))

let client = Self.newXPCClient()
try await client.send(request)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to kill container",
cause: error
)
}
}

/// Stop the container and all processes currently executing inside.
public func stop(opts: ContainerStopOptions = ContainerStopOptions.default) async throws {
do {
let client = self.sandboxClient
try await client.stop(options: opts)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerStop)
let data = try JSONEncoder().encode(opts)
request.set(key: .id, value: self.id)
request.set(key: .stopOptions, value: data)

// Stop is somewhat more prone to hanging than other commands given it
// has quite a bit of `wait()`'s down the chain to make sure the container actually
// exited. To combat a potential hang, lets timeout if we don't return in a small
// time period after the actual stop timeout sent via .stopOptions (the time
// until we send SIGKILL after SIGTERM if the container still hasn't exited).
let responseTimeout = Duration(.seconds(Int64(opts.timeoutInSeconds + 3)))
Comment thread
dcantah marked this conversation as resolved.
try await client.send(request, responseTimeout: responseTimeout)
} catch {
throw ContainerizationError(
.internalError,
Expand All@@ -167,8 +215,8 @@ extension ClientContainer {
/// Delete the container along with any resources.
public func delete(force: Bool = false) async throws {
do {
let client = XPCClient(service: Self.serviceIdentifier)
let request = XPCMessage(route: .deleteContainer)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerDelete)
request.set(key: .id, value: self.id)
request.set(key: .forceDelete, value: force)
try await client.send(request)
Expand All@@ -180,46 +228,53 @@ extension ClientContainer {
)
}
}
}

extension ClientContainer {
/// Execute a new process inside a running container.
/// Create a new process inside a running container. The process is in a
/// created state and must still be started.
public func createProcess(
id: String,
configuration: ProcessConfiguration,
stdio: [FileHandle?]
) async throws -> ClientProcess {
do {
let client = self.sandboxClient
try await client.createProcess(id, config: configuration, stdio: stdio)
return ClientProcessImpl(containerId: self.id, processId: id, client: client)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to exec in container",
cause: error
)
}
}
let request = XPCMessage(route: .containerCreateProcess)
request.set(key: .id, value: self.id)
request.set(key: .processIdentifier, value: id)

/// Send or "kill" a signal to the initial process of the container.
/// Kill does not wait for the process to exit, it only delivers the signal.
public func kill(_ signal: Int32) async throws {
do {
let client = self.sandboxClient
try await client.kill(self.id, signal: Int64(signal))
let data = try JSONEncoder().encode(configuration)
request.set(key: .processConfig, value: data)

for (i, h) in stdio.enumerated() {
Comment thread
dcantah marked this conversation as resolved.
let key: XPCKeys = try {
switch i {
case 0: .stdin
case 1: .stdout
case 2: .stderr
default:
throw ContainerizationError(.invalidArgument, message: "invalid fd \(i)")
}
}()

if let h {
request.set(key: key, value: h)
}
}

let client = Self.newXPCClient()
try await client.send(request)
return ClientProcessImpl(containerId: self.id, processId: id, xpcClient: client)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to kill container \(self.id)",
message: "failed to create process in container",
cause: error
)
}
}

public func logs() async throws -> [FileHandle] {
do {
let client = XPCClient(service: Self.serviceIdentifier)
let client = Self.newXPCClient()
let request = XPCMessage(route: .containerLogs)
request.set(key: .id, value: self.id)

Expand All@@ -242,15 +297,27 @@ extension ClientContainer {
}

public func dial(_ port: UInt32) async throws -> FileHandle {
let request = XPCMessage(route: .containerDial)
request.set(key: .id, value: self.id)
request.set(key: .port, value: UInt64(port))

let client = Self.newXPCClient()
let response: XPCMessage
do {
let client = self.sandboxClient
return try await client.dial(port)
response = try await client.send(request)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to dial \(port) in container \(self.id)",
message: "failed to dial port \(port) on container",
cause: error
)
}
guard let fh = response.fileHandle(key: .fd) else {
throw ContainerizationError(
.internalError,
message: "failed to get fd for vsock port \(port)"
)
}
return fh
}
}
91 changes: 39 additions & 52 deletions Sources/ContainerClient/Core/ClientProcess.swift
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,7 @@ public protocol ClientProcess: Sendable {
func start() async throws
/// Send a terminal resize request to the process `id`.
func resize(_ size: Terminal.Size) async throws
/// Send or "kill" a signal to the process `id`.
/// Send a signal to the process `id`.
/// Kill does not wait for the process to exit, it only delivers the signal.
func kill(_ signal: Int32) async throws
/// Wait for the process `id` to complete and return its exit code.
Expand All@@ -45,79 +45,66 @@ public protocol ClientProcess: Sendable {

struct ClientProcessImpl: ClientProcess, Sendable {
static let serviceIdentifier = "com.apple.container.apiserver"

/// ID of the process.
public var id: String {
Comment thread
dcantah marked this conversation as resolved.
processId ?? containerId
}

/// Identifier of the container.
public let containerId: String

private let client: SandboxClient

/// Identifier of a process. That is running inside of a container.
/// This field is nil if the process this objects refers to is the
/// init process of the container.
public let processId: String?

public var id: String {
processId ?? containerId
}
private let xpcClient: XPCClient

init(containerId: String, processId: String? = nil, client: SandboxClient) {
init(containerId: String, processId: String? = nil, xpcClient: XPCClient) {
self.containerId = containerId
self.processId = processId
self.client = client
self.xpcClient = xpcClient
}

/// Start the container and return the initial process.
/// Start the process.
public func start() async throws {
do {
let client = self.client
try await client.startProcess(self.id)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to start container",
cause: error
)
}
let request = XPCMessage(route: .containerStartProcess)
request.set(key: .id, value: containerId)
request.set(key: .processIdentifier, value: id)

try await xpcClient.send(request)
}

/// Send a signal to the process.
public func kill(_ signal: Int32) async throws {
do {

let client = self.client
try await client.kill(self.id, signal: Int64(signal))
} catch {
throw ContainerizationError(
.internalError,
message: "failed to kill process",
cause: error
)
}
}
let request = XPCMessage(route: .containerKill)
request.set(key: .id, value: containerId)
request.set(key: .processIdentifier, value: id)
request.set(key: .signal, value: Int64(signal))

public func resize(_ size: ContainerizationOS.Terminal.Size) async throws {
do {
try await xpcClient.send(request)
}

let client = self.client
try await client.resize(self.id, size: size)
/// Resize the processes PTY if it has one.
public func resize(_ size: Terminal.Size) async throws {
let request = XPCMessage(route: .containerResize)
request.set(key: .id, value: containerId)
request.set(key: .processIdentifier, value: id)
request.set(key: .width, value: UInt64(size.width))
request.set(key: .height, value: UInt64(size.height))

} catch {
throw ContainerizationError(
.internalError,
message: "failed to resize process",
cause: error
)
}
try await xpcClient.send(request)
}

/// Wait for the process to exit.
public func wait() async throws -> Int32 {
do {
let client = self.client
return try await client.wait(self.id)
} catch {
throw ContainerizationError(
.internalError,
message: "failed to wait on process",
cause: error
)
}
let request = XPCMessage(route: .containerWait)
request.set(key: .id, value: containerId)
request.set(key: .processIdentifier, value: id)

let response = try await xpcClient.send(request)
let code = response.int64(key: .exitCode)
return Int32(code)
}
}
Loading