Skip to content

Commit 9c239aa

Browse files
authored
Add support for reading env from named pipes (#974)
This is a fix for [issue#956](#956) `FileManager.default.contents(atPath:)` returns `nil` for named pipes (FIFOs) and process substitutions like `/dev/fd/XX` because: 1. It expects regular files with a known size 2. Named pipes are stream-based and block until data arrives ## Solution Use `FileHandle(forReadingFrom:)` instead, which: - Properly handles blocking I/O - Works with named pipes, process substitutions, and regular files (mentioned in the [doc](https://developer.apple.com/documentation/foundation/filehandle)) Co-authored-by: Bortniak Volodymyr <Bortnyak@users.noreply.github.com>
1 parent 3c3a83c commit 9c239aa

File tree

3 files changed

+111
-10
lines changed

3 files changed

+111
-10
lines changed

Sources/ContainerClient/Parser.swift

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -110,17 +110,19 @@ public struct Parser {
110110
// This is a somewhat faithful Go->Swift port of Moby's envfile
111111
// parsing in the cli:
112112
// https://github.com/docker/cli/blob/f5a7a3c72eb35fc5ba9c4d65a2a0e2e1bd216bf2/pkg/kvfile/kvfile.go#L81
113-
guard FileManager.default.fileExists(atPath: path) else {
114-
throw ContainerizationError(
115-
.notFound,
116-
message: "envfile at \(path) not found"
117-
)
118-
}
119113

120-
guard let data = FileManager.default.contents(atPath: path) else {
114+
let data: Data
115+
do {
116+
// Use FileHandle to support named pipes (FIFOs) and process substitutions
117+
// like --env-file <(echo "KEY=value")
118+
let fileHandle = try FileHandle(forReadingFrom: URL(fileURLWithPath: path))
119+
defer { try? fileHandle.close() }
120+
data = try fileHandle.readToEnd() ?? Data()
121+
} catch {
121122
throw ContainerizationError(
122123
.invalidArgument,
123-
message: "failed to read envfile at \(path)"
124+
message: "failed to read envfile at \(path)",
125+
cause: error
124126
)
125127
}
126128

Tests/CLITests/Subcommands/Run/TestCLIRunCommand.swift

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,68 @@ class TestCLIRunCommand: CLITest {
577577
}
578578
}
579579

580+
@Test func testRunCommandEnvFileFromNamedPipe() throws {
581+
do {
582+
let name = getTestName()
583+
let pipePath = FileManager.default.temporaryDirectory.appendingPathComponent("envfile-pipe\(UUID().uuidString)")
584+
585+
// create pipe
586+
let result = mkfifo(pipePath.path(), 0o600)
587+
guard result == 0 else {
588+
Issue.record("failed to create named pipe: \(String(cString: strerror(errno)))")
589+
return
590+
}
591+
592+
defer {
593+
try? FileManager.default.removeItem(at: pipePath)
594+
}
595+
596+
let content = """
597+
FOO=bar
598+
BAR=baz
599+
"""
600+
601+
let group = DispatchGroup()
602+
603+
group.enter()
604+
DispatchQueue.global().async {
605+
do {
606+
let handle = try FileHandle(forWritingTo: pipePath)
607+
try handle.write(contentsOf: Data(content.utf8))
608+
try handle.close()
609+
} catch {
610+
Issue.record(error)
611+
return
612+
}
613+
614+
group.leave()
615+
}
616+
617+
try doLongRun(name: name, args: ["--env-file", pipePath.path()])
618+
defer {
619+
try? doStop(name: name)
620+
}
621+
622+
group.wait()
623+
624+
let inspectResult = try inspectContainer(name)
625+
let expected = [
626+
"FOO=bar",
627+
"BAR=baz",
628+
]
629+
630+
for item in expected {
631+
#expect(
632+
inspectResult.configuration.initProcess.environment.contains(item),
633+
"expected environment variable \(item) not found"
634+
)
635+
}
636+
try doStop(name: name)
637+
} catch {
638+
Issue.record(error)
639+
}
640+
}
641+
580642
func getDefaultDomain() throws -> String? {
581643
let (_, output, err, status) = try run(arguments: ["system", "property", "get", "dns.domain"])
582644
try #require(status == 0, "default DNS domain retrieval returned status \(status): \(err)")

Tests/ContainerClientTests/ParserTest.swift

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -493,10 +493,12 @@ struct ParserTest {
493493
#expect {
494494
_ = try Parser.envFile(path: "/nonexistent/foo_bar_baz")
495495
} throws: { error in
496-
guard let error = error as? ContainerizationError else {
496+
guard let error = error as? ContainerizationError,
497+
let cause = error.cause
498+
else {
497499
return false
498500
}
499-
return error.description.contains("not found")
501+
return String(describing: cause).contains("No such file or directory")
500502
}
501503
}
502504

@@ -579,6 +581,41 @@ struct ParserTest {
579581
}
580582
}
581583

584+
@Test
585+
func testParseEnvFileFromNamedPipe() throws {
586+
let pipePath = FileManager.default.temporaryDirectory
587+
.appendingPathComponent("envfile-pipe-\(UUID().uuidString)")
588+
589+
// Create a named pipe (FIFO)
590+
let result = mkfifo(pipePath.path, 0o600)
591+
guard result == 0 else {
592+
throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EPERM)
593+
}
594+
defer { try? FileManager.default.removeItem(at: pipePath) }
595+
596+
let group = DispatchGroup()
597+
598+
group.enter()
599+
DispatchQueue.global().async {
600+
do {
601+
let handle = try FileHandle(forWritingTo: pipePath)
602+
try handle.write(contentsOf: "SECRET_KEY=value123\n".data(using: .utf8)!)
603+
try handle.close()
604+
} catch {
605+
Issue.record(error)
606+
}
607+
group.leave()
608+
}
609+
610+
// Read from pipe (blocks until writer connects)
611+
let lines = try Parser.envFile(path: pipePath.path)
612+
613+
// Wait for write to complete
614+
group.wait()
615+
616+
#expect(lines == ["SECRET_KEY=value123"])
617+
}
618+
582619
// MARK: Network Parser Tests
583620

584621
@Test

0 commit comments

Comments
 (0)