Skip to content

Commit e35c134

Browse files
authored
tart exec: handle input redirection of regular files (#1106)
1 parent 0debec1 commit e35c134

File tree

1 file changed

+52
-9
lines changed

1 file changed

+52
-9
lines changed

Sources/tart/Commands/Exec.swift

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -100,28 +100,61 @@ struct Exec: AsyncParsableCommand {
100100
try await withThrowingTaskGroup { group in
101101
// Stream host's standard input if interactive mode is enabled
102102
if interactive {
103-
let stdinStream = AsyncStream<Data> { continuation in
103+
let stdinStream = AsyncThrowingStream<Data, Error> { continuation in
104104
let handle = FileHandle.standardInput
105105

106-
handle.readabilityHandler = { handle in
107-
let data = handle.availableData
108-
109-
continuation.yield(data)
110-
111-
if data.isEmpty {
112-
continuation.finish()
106+
if isRegularFile(handle.fileDescriptor) {
107+
// Standard input can be a regular file when input redirection (<) is used,
108+
// in which case the handle won't receive any new readability events, so we
109+
// just read the file normally here in chunks and consider done with it
110+
//
111+
// Ideally this is best handled by using non-blocking I/O, but Swift's
112+
// standard library only offers inefficient bytes[1] property and SwiftNIO's
113+
// NIOFileSystem doesn't seem to support opening raw file descriptors.
114+
//
115+
// [1]: https://developer.apple.com/documentation/foundation/filehandle/bytes
116+
while true {
117+
do {
118+
let data = try handle.read(upToCount: 64 * 1024)
119+
if let data = data {
120+
continuation.yield(data)
121+
} else {
122+
continuation.finish()
123+
break
124+
}
125+
} catch (let error) {
126+
continuation.finish(throwing: error)
127+
break
128+
}
129+
}
130+
} else {
131+
handle.readabilityHandler = { handle in
132+
let data = handle.availableData
133+
134+
if data.isEmpty {
135+
continuation.finish()
136+
} else {
137+
continuation.yield(data)
138+
}
113139
}
114140
}
115141
}
116142

117143
group.addTask {
118-
for await stdinData in stdinStream {
144+
for try await stdinData in stdinStream {
119145
try await execCall.requestStream.send(.with {
120146
$0.type = .standardInput(.with {
121147
$0.data = stdinData
122148
})
123149
})
124150
}
151+
152+
// Signal EOF as we're done reading standard input
153+
try await execCall.requestStream.send(.with {
154+
$0.type = .standardInput(.with {
155+
$0.data = Data()
156+
})
157+
})
125158
}
126159
}
127160

@@ -178,3 +211,13 @@ struct Exec: AsyncParsableCommand {
178211
}
179212
}
180213
}
214+
215+
private func isRegularFile(_ fileDescriptor: Int32) -> Bool {
216+
var stat = stat()
217+
218+
if fstat(fileDescriptor, &stat) != 0 {
219+
return false
220+
}
221+
222+
return (stat.st_mode & S_IFMT) == S_IFREG
223+
}

0 commit comments

Comments
 (0)