Skip to content

Commit 5051545

Browse files
authored
plugin: handle chunks of output correctly (#77)
Motivation ---------- `containertool` produces its normal output in single writes of less than PIPE_BUF bytes, which should be delivered atomically through the pipe to the plugin: > Reading or writing pipe data is atomic if the size of data written is not greater than PIPE_BUF. This means that the data transfer seems to be an instantaneous unit, in that nothing else in the system can observe a state in which it is partially complete. Atomic I/O may not begin right away (it may need to wait for buffer space or for data), but once it does begin it finishes immediately. https://www.gnu.org/software/libc/manual/html_node/Pipe-Atomicity.html Progress messages written in this way are displayed correctly. Unfortunately error output from `swift-argument-parser` is delivered in smaller chunks which are often broken across several lines by the plugin: ``` % swift package --allow-network-connections all build-container-image Building for debugging... [1/1] Write swift-version-2380AA06D3543E1B.txt Build of product 'containertool' complete! (1.74s) Building for debugging... [0/3] Write swift-version-2380AA06D3543E1B.txt Build of product 'hello-world' complete! (2.04s) [ContainerImageBuilder] itory <repository>' Help: --repository < [ContainerImageBuilder] repository> Repository path [ContainerImageBuilder] Usage: containertoo [ContainerImageBuilder] l [<optio [ContainerImageBuilder] ns>] --re [ContainerImageBuilder] posito [ContainerImageBuilder] ry <rep error: Missing expected argument '--repos [ContainerImageBuilder] ository [ContainerImageBuilder] > <ex [ContainerImageBuilder] ecutable> Se [ContainerImageBuilder] e 'containertool --help' for more inf [ContainerImageBuilder] ormation [ContainerImageBuilder] . ``` At one point the plugin reassembled output from the pipe and re-split it on newlines, but the code triggered strict concurrency warnings with the Swift 6 language mode so it was removed in favour of `PIPE_BUF` write atomicity. This commit restores the pipe output reassembly. Modifications ------------- * Plugin reassembles output from `containertool` and splits it on newlines * All output after an error is now printed using `Diagnostics.error`. Otherwise error and progress output would be interleaved. Result ------ Error output from `containertool` will be presented correctly by the plugin. Test Plan --------- Existing tests continue to pass. A new integration test checks that error output is not broken into multiple lines.
1 parent b0a23d0 commit 5051545

File tree

3 files changed

+109
-7
lines changed

3 files changed

+109
-7
lines changed

.github/workflows/integration_tests.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,25 @@ jobs:
3434
REGISTRY_PORT: 5000
3535
run: |
3636
swift test
37+
38+
plugin-streaming-output-test:
39+
name: Plugin streaming output test
40+
runs-on: ubuntu-latest
41+
services:
42+
registry:
43+
image: registry:2
44+
ports:
45+
- 5000:5000
46+
steps:
47+
- name: Checkout repository
48+
uses: actions/checkout@v4
49+
with:
50+
persist-credentials: false
51+
52+
- name: Mark the workspace as safe
53+
# https://github.com/actions/checkout/issues/766
54+
run: git config --global --add safe.directory ${GITHUB_WORKSPACE}
55+
56+
- name: Check plugin streaming output is reassembled and printed properly
57+
run: |
58+
scripts/test-plugin-output-streaming.sh

Plugins/ContainerImageBuilder/main.swift

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,16 +99,48 @@ extension PluginError: CustomStringConvertible {
9999

100100
await withThrowingTaskGroup(of: Void.self) { group in
101101
group.addTask {
102+
enum LoggingState {
103+
// Normal output is logged at 'progress' level.
104+
case progress
105+
106+
// If an error is detected, all output from that point onwards is logged at 'error' level, which is always printed.
107+
// Errors are reported even without the --verbose flag and cause the build to return a nonzero exit code.
108+
case error
109+
110+
func log(_ msg: String) {
111+
let trimmed = msg.trimmingCharacters(in: .newlines)
112+
switch self {
113+
case .progress: Diagnostics.progress(trimmed)
114+
case .error: Diagnostics.error(trimmed)
115+
}
116+
}
117+
}
118+
119+
var buf = ""
120+
var logger = LoggingState.progress
121+
102122
for try await line in err.lines {
103-
let errorLabel = "Error: " // SwiftArgumentParser adds this prefix to all errors which bubble up
104-
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
123+
buf.append(line)
105124

106-
if trimmed.starts(with: errorLabel) {
107-
// Errors are reported even without the --verbose flag and cause the build to fail.
108-
Diagnostics.error(String(trimmed.dropFirst(errorLabel.count)))
109-
} else {
110-
Diagnostics.progress(trimmed)
125+
guard let (before, after) = buf.splitOn(first: "\n") else {
126+
continue
127+
}
128+
129+
var msg = before
130+
buf = String(after)
131+
132+
let errorLabel = "Error: " // SwiftArgumentParser adds this prefix to all errors which bubble up
133+
if msg.starts(with: errorLabel) {
134+
logger = .error
135+
msg.trimPrefix(errorLabel)
111136
}
137+
138+
logger.log(String(msg))
139+
}
140+
141+
// Print any leftover output in the buffer, in case the child exited without sending a final newline.
142+
if !buf.isEmpty {
143+
logger.log(buf)
112144
}
113145
}
114146

@@ -118,3 +150,13 @@ extension PluginError: CustomStringConvertible {
118150
}
119151
}
120152
}
153+
154+
extension Collection where Element: Equatable {
155+
func splitOn(first element: Element) -> (before: SubSequence, after: SubSequence)? {
156+
guard let idx = self.firstIndex(of: element) else {
157+
return nil
158+
}
159+
160+
return (self[..<idx], self[idx...].dropFirst())
161+
}
162+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/usr/bin/env bash
2+
3+
# Test that error output streamed from containertool is printed correctly by the plugin.
4+
5+
set -exo pipefail
6+
7+
log() { printf -- "** %s\n" "$*" >&2; }
8+
error() { printf -- "** ERROR: %s\n" "$*" >&2; }
9+
fatal() { error "$@"; exit 1; }
10+
11+
# Work in a temporary directory, deleted after the test finishes
12+
PKGPATH=$(mktemp -d)
13+
cleanup() {
14+
log "Deleting temporary package $PKGPATH"
15+
rm -rf "$PKGPATH"
16+
}
17+
trap cleanup EXIT
18+
19+
swift package --package-path "$PKGPATH" init --type executable --name hello
20+
cat >> "$PKGPATH/Package.swift" <<EOF
21+
package.dependencies += [
22+
.package(path: "$PWD"),
23+
]
24+
EOF
25+
26+
# Run the plugin, forgetting a mandatory argument. Verify that the output is not corrupted.
27+
# The `swift package` command will return a nonzero exit code. This is expected, so disable pipefail.
28+
set +o pipefail
29+
swift package --package-path "$PKGPATH" --allow-network-connections all build-container-image 2>&1 | tee "$PKGPATH/output"
30+
set -o pipefail
31+
32+
grep -F -x -e "error: Missing expected argument '--repository <repository>'" \
33+
-e "error: Help: --repository <repository> Repository path" \
34+
-e "error: Usage: containertool [<options>] --repository <repository> <executable>" \
35+
-e "error: See 'containertool --help' for more information." "$PKGPATH/output"
36+
37+
echo Plugin error output: PASSED
38+

0 commit comments

Comments
 (0)