Skip to content

Commit 13b3b52

Browse files
committed
Implement NativeProcess
NativeProcess is a ContainerProcess that runs outside the containerized environment. It's equally managed like the other ManagedProcess, but only runs outside sandbox.
1 parent 5ab9c95 commit 13b3b52

3 files changed

Lines changed: 258 additions & 8 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2026 Apple Inc. and the container project authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
public struct CreateProcessOptions: Sendable, Codable {
18+
/// The process is created outside containerized env.
19+
public var native: Bool
20+
21+
enum CodingKeys: String, CodingKey {
22+
case native
23+
}
24+
25+
public init(native: Bool) {
26+
self.native = native
27+
}
28+
29+
public init(from decoder: Decoder) throws {
30+
let container = try decoder.container(keyedBy: CodingKeys.self)
31+
32+
native = try container.decodeIfPresent(Bool.self, forKey: .native) ?? false
33+
}
34+
}

vminitd/Sources/VminitdCore/ManagedContainer.swift

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -171,14 +171,24 @@ extension ManagedContainer {
171171
id: id,
172172
process: process
173173
)
174-
let process = try ManagedProcess(
175-
id: id,
176-
stdio: stdio,
177-
bundle: self.bundle,
178-
owningPid: self.initProcess.pid,
179-
log: self.log
180-
)
181-
self.execs[id] = process
174+
let exec: ContainerProcess
175+
if native {
176+
exec = try NativeProcess(
177+
id: id,
178+
stdio: stdio,
179+
process: process,
180+
log: self.log
181+
)
182+
} else {
183+
exec = try ManagedProcess(
184+
id: id,
185+
stdio: stdio,
186+
bundle: self.bundle,
187+
owningPid: self.initProcess.pid,
188+
log: self.log
189+
)
190+
}
191+
self.execs[id] = exec
182192
}
183193

184194
func start(execID: String) async throws -> Int32 {
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2026 Apple Inc. and the Containerization project authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
import ContainerizationError
18+
import ContainerizationOCI
19+
import ContainerizationOS
20+
import Foundation
21+
import Logging
22+
import Synchronization
23+
import SystemPackage
24+
25+
final class NativeProcess: ContainerProcess, Sendable {
26+
private struct State {
27+
init(io: ManagedProcess.IO) {
28+
self.io = io
29+
}
30+
31+
var waiters: [CheckedContinuation<ContainerExitStatus, Never>] = []
32+
var exitStatus: ContainerExitStatus? = nil
33+
var pid: Int32?
34+
let io: ManagedProcess.IO
35+
}
36+
37+
let id: String
38+
39+
private let log: Logger
40+
private let command: Command
41+
private let state: Mutex<State>
42+
43+
var pid: Int32? {
44+
self.state.withLock {
45+
$0.pid
46+
}
47+
}
48+
49+
init(
50+
id: String,
51+
stdio: HostStdio,
52+
process: ContainerizationOCI.Process,
53+
log: Logger
54+
) throws {
55+
self.id = id
56+
var log = log
57+
log[metadataKey: "id"] = "\(id)"
58+
self.log = log
59+
60+
guard !process.args.isEmpty else {
61+
throw ContainerizationError(.invalidArgument, message: "process args cannot be empty")
62+
}
63+
64+
let executableArg = process.args[0]
65+
guard executableArg.hasPrefix("/") else {
66+
throw ContainerizationError(.invalidArgument, message: "executable path must be absolute path")
67+
}
68+
69+
let executable = FilePath(executableArg)
70+
guard FileManager.default.fileExists(atPath: executable.string) else {
71+
throw ContainerizationError(.invalidArgument, message: "failed to find target executable \(executableArg)")
72+
}
73+
74+
var command = Command(
75+
executable.string,
76+
arguments: Array(process.args.dropFirst()),
77+
environment: process.env,
78+
directory: process.cwd
79+
)
80+
81+
guard !stdio.terminal else {
82+
throw ContainerizationError(.invalidArgument, message: "native process doesn't support terminal")
83+
}
84+
85+
command.attrs = .init(setsid: false)
86+
let io = StandardIO(
87+
stdio: stdio,
88+
log: log
89+
)
90+
91+
log.info("starting I/O")
92+
93+
// Setup IO early. We expect the host to be listening already.
94+
try io.start(process: &command)
95+
96+
self.command = command
97+
self.state = Mutex(State(io: io))
98+
}
99+
100+
func start() async throws -> Int32 {
101+
do {
102+
return try self.state.withLock {
103+
log.info(
104+
"starting native process",
105+
metadata: ["id": "\(id)"]
106+
)
107+
108+
try command.start()
109+
try $0.io.closeAfterExec()
110+
111+
let pid = command.pid
112+
$0.pid = pid
113+
114+
log.info(
115+
"started native process",
116+
metadata: [
117+
"pid": "\(pid)",
118+
"id": "\(id)",
119+
]
120+
)
121+
122+
return pid
123+
}
124+
} catch {
125+
throw ContainerizationError(
126+
.internalError,
127+
message: "native process failed to start: \(error)"
128+
)
129+
}
130+
}
131+
132+
func setExit(_ status: Int32) {
133+
self.state.withLock { state in
134+
self.log.info(
135+
"native process exit",
136+
metadata: [
137+
"status": "\(status)"
138+
]
139+
)
140+
141+
let exitStatus = ContainerExitStatus(exitCode: status, exitedAt: Date.now)
142+
state.exitStatus = exitStatus
143+
144+
do {
145+
try state.io.close()
146+
} catch {
147+
self.log.error("failed to close I/O for process: \(error)")
148+
}
149+
150+
for waiter in state.waiters {
151+
waiter.resume(returning: exitStatus)
152+
}
153+
154+
self.log.debug("\(state.waiters.count) native process waiters signaled")
155+
state.waiters.removeAll()
156+
}
157+
}
158+
159+
func wait() async -> ContainerExitStatus {
160+
await withCheckedContinuation { cont in
161+
self.state.withLock {
162+
if let status = $0.exitStatus {
163+
cont.resume(returning: status)
164+
return
165+
}
166+
$0.waiters.append(cont)
167+
}
168+
}
169+
}
170+
171+
func kill(_ signal: Int32) async throws {
172+
try self.state.withLock {
173+
guard let pid = $0.pid else {
174+
throw ContainerizationError(.invalidState, message: "process PID is required")
175+
}
176+
177+
guard $0.exitStatus == nil else {
178+
return
179+
}
180+
181+
self.log.info("sending signal \(signal) to native process \(pid)")
182+
guard Foundation.kill(pid, signal) == 0 else {
183+
throw POSIXError.fromErrno()
184+
}
185+
}
186+
}
187+
188+
func resize(size: Terminal.Size) throws {
189+
try self.state.withLock {
190+
guard $0.exitStatus == nil else {
191+
return
192+
}
193+
try $0.io.resize(size: size)
194+
}
195+
}
196+
197+
func closeStdin() throws {
198+
let io = self.state.withLock { $0.io }
199+
try io.closeStdin()
200+
}
201+
202+
func delete() async throws {
203+
// Nothing to be done
204+
}
205+
206+
}

0 commit comments

Comments
 (0)