Skip to content

Commit

Permalink
Show stdout/stderr from Wasm in terminal (#472)
Browse files Browse the repository at this point in the history
  • Loading branch information
omochi authored May 25, 2024
1 parent 3cb3877 commit 6cc68d3
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 44 deletions.
121 changes: 82 additions & 39 deletions Sources/CartonKit/Server/Server.swift
Original file line number Diff line number Diff line change
Expand Up @@ -238,9 +238,14 @@ public actor Server {
let environment =
head.headers["User-Agent"].compactMap(DestinationEnvironment.init).first
?? .other
let handler = await ServerWebSocketHandler(
let handler = ServerWebSocketHandler(
configuration: ServerWebSocketHandler.Configuration(
onText: self.createWebSocketTextHandler(in: environment, terminal: self.configuration.terminal)
onText: { [weak self] (text) in
self?.webSocketTextHandler(text: text, environment: environment)
},
onBinary: { [weak self] (data) in
self?.webSocketBinaryHandler(data: data)
}
)
)
await self.add(connection: Connection(channel: channel))
Expand Down Expand Up @@ -329,50 +334,88 @@ public actor Server {
}

extension Server {
/// Returns a handler that responds to WebSocket messages coming from the browser.
func createWebSocketTextHandler(
in environment: DestinationEnvironment,
terminal: InteractiveWriter
) -> @Sendable (String) -> Void {
{ [weak self] text in
guard let self = self else { return }
guard
let data = text.data(using: .utf8),
let event = try? self.decoder.decode(Event.self, from: data)
else {
return
}
/// Respond to WebSocket messages coming from the browser.
nonisolated func webSocketTextHandler(
text: String,
environment: DestinationEnvironment
) {
guard
let data = text.data(using: .utf8),
let event = try? self.decoder.decode(Event.self, from: data)
else {
return
}

switch event {
case let .stackTrace(rawStackTrace):
if let stackTrace = rawStackTrace.parsedStackTrace(in: environment) {
terminal.write("\nAn error occurred, here's a stack trace for it:\n", inColor: .red)
stackTrace.forEach { item in
terminal.write(" \(item.symbol)", inColor: .cyan)
terminal.write(" at \(item.location ?? "<unknown>")\n", inColor: .gray)
}
} else {
terminal.write("\nAn error occurred, here's the raw stack trace for it:\n", inColor: .red)
terminal.write(
" Please create an issue or PR to the Carton repository\n"
+ " with your browser name and this raw stack trace so\n"
+ " we can add support for it: https://github.com/swiftwasm/carton\n", inColor: .gray
)
terminal.write(rawStackTrace + "\n")
let terminal = self.configuration.terminal

switch event {
case let .stackTrace(rawStackTrace):
if let stackTrace = rawStackTrace.parsedStackTrace(in: environment) {
terminal.write("\nAn error occurred, here's a stack trace for it:\n", inColor: .red)
stackTrace.forEach { item in
terminal.write(" \(item.symbol)", inColor: .cyan)
terminal.write(" at \(item.location ?? "<unknown>")\n", inColor: .gray)
}
} else {
terminal.write("\nAn error occurred, here's the raw stack trace for it:\n", inColor: .red)
terminal.write(
" Please create an issue or PR to the Carton repository\n"
+ " with your browser name and this raw stack trace so\n"
+ " we can add support for it: https://github.com/swiftwasm/carton\n", inColor: .gray
)
terminal.write(rawStackTrace + "\n")
}

case let .testRunOutput(output):
TestsParser().parse(output, terminal)

case .testPassed:
Task { await self.stopTest(hadError: false) }

case let .errorReport(output):
terminal.write("\nAn error occurred:\n", inColor: .red)
terminal.write(output + "\n")

case let .testRunOutput(output):
TestsParser().parse(output, terminal)
Task { await self.stopTest(hadError: true) }
}
}

private static func decodeLines(data: Data) -> [String] {
let text = String(decoding: data, as: UTF8.self)
return text.components(separatedBy: .newlines)
}

nonisolated func webSocketBinaryHandler(data: Data) {
let terminal = self.configuration.terminal

if data.count < 2 {
return
}

var kind: UInt16 = 0
_ = withUnsafeMutableBytes(of: &kind) { (buffer) in
data.copyBytes(to: buffer, from: 0..<2)
}
kind = UInt16(littleEndian: kind)

case .testPassed:
Task { await self.stopTest(hadError: false) }
switch kind {
case 1001:
// stdout
let chunk = data.subdata(in: 2..<data.count)
if chunk.isEmpty { return }

case let .errorReport(output):
terminal.write("\nAn error occurred:\n", inColor: .red)
terminal.write(output + "\n")
for line in Self.decodeLines(data: chunk) {
terminal.write("stdout: " + line + "\n")
}
case 1002:
// stderr
let chunk = data.subdata(in: 2..<data.count)
if chunk.isEmpty { return }

Task { await self.stopTest(hadError: true) }
for line in Self.decodeLines(data: chunk) {
terminal.write("stderr: " + line + "\n", inColor: .red)
}
default: break
}
}
}
Expand Down
10 changes: 8 additions & 2 deletions Sources/CartonKit/Server/ServerWebSocketHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import Foundation
import NIO
import NIOWebSocket

Expand All @@ -20,7 +21,8 @@ final class ServerWebSocketHandler: ChannelInboundHandler {
typealias OutboundOut = WebSocketFrame

struct Configuration {
let onText: @Sendable (String) -> Void
var onText: @Sendable (String) -> Void
var onBinary: @Sendable (Data) -> Void
}

private var awaitingClose: Bool = false
Expand All @@ -43,7 +45,11 @@ final class ServerWebSocketHandler: ChannelInboundHandler {
var data = frame.unmaskedData
let text = data.readString(length: data.readableBytes) ?? ""
self.configuration.onText(text)
case .binary, .continuation, .pong:
case .binary:
let nioData = frame.unmaskedData
let data = Data(nioData.readableBytesView)
self.configuration.onBinary(data)
case .continuation, .pong:
// We ignore these frames.
break
default:
Expand Down
2 changes: 1 addition & 1 deletion Sources/CartonKit/Server/StaticArchive.swift

Large diffs are not rendered by default.

17 changes: 16 additions & 1 deletion Tests/Fixtures/DevServerTestApp/Sources/app/main.swift
Original file line number Diff line number Diff line change
@@ -1 +1,16 @@
print("hello dev server")
#if os(WASI)
import WASILibc
typealias FILEPointer = OpaquePointer
#else
import Darwin
typealias FILEPointer = UnsafeMutablePointer<FILE>
#endif

func fputs(_ string: String, file: FILEPointer) {
_ = string.withCString { (cstr) in
fputs(cstr, file)
}
}

fputs("hello stdout\n", file: stdout)
fputs("hello stderr\n", file: stderr)
20 changes: 20 additions & 0 deletions entrypoint/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,29 @@ const startWasiTask = async () => {

const wasmRunner = WasmRunner(
{
onStdout(chunk) {
const kindBuffer = new ArrayBuffer(2);
new DataView(kindBuffer).setUint16(0, 1001, true);

const buffer = new Uint8Array(2 + chunk.length);
buffer.set(new Uint8Array(kindBuffer), 0);
buffer.set(chunk, 2);

socket.send(buffer);
},
onStdoutLine(line) {
console.log(line);
},
onStderr(chunk) {
const kindBuffer = new ArrayBuffer(2);
new DataView(kindBuffer).setUint16(0, 1002, true);

const buffer = new Uint8Array(2 + chunk.length);
buffer.set(new Uint8Array(kindBuffer), 0);
buffer.set(chunk, 2);

socket.send(buffer);
},
onStderrLine(line) {
console.error(line);
}
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 6cc68d3

Please sign in to comment.