Show stdout/stderr from Wasm in terminal (#472)

This commit is contained in:
omochimetaru 2024-05-25 09:06:24 +09:00 committed by GitHub
parent 3cb3877d98
commit 6cc68d3ba8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 132 additions and 48 deletions

View File

@ -238,9 +238,14 @@ public actor Server {
let environment = let environment =
head.headers["User-Agent"].compactMap(DestinationEnvironment.init).first head.headers["User-Agent"].compactMap(DestinationEnvironment.init).first
?? .other ?? .other
let handler = await ServerWebSocketHandler( let handler = ServerWebSocketHandler(
configuration: ServerWebSocketHandler.Configuration( 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)) await self.add(connection: Connection(channel: channel))
@ -329,50 +334,88 @@ public actor Server {
} }
extension Server { extension Server {
/// Returns a handler that responds to WebSocket messages coming from the browser. /// Respond to WebSocket messages coming from the browser.
func createWebSocketTextHandler( nonisolated func webSocketTextHandler(
in environment: DestinationEnvironment, text: String,
terminal: InteractiveWriter environment: DestinationEnvironment
) -> @Sendable (String) -> Void { ) {
{ [weak self] text in guard
guard let self = self else { return } let data = text.data(using: .utf8),
guard let event = try? self.decoder.decode(Event.self, from: data)
let data = text.data(using: .utf8), else {
let event = try? self.decoder.decode(Event.self, from: data) return
else { }
return
}
switch event { let terminal = self.configuration.terminal
case let .stackTrace(rawStackTrace):
if let stackTrace = rawStackTrace.parsedStackTrace(in: environment) { switch event {
terminal.write("\nAn error occurred, here's a stack trace for it:\n", inColor: .red) case let .stackTrace(rawStackTrace):
stackTrace.forEach { item in if let stackTrace = rawStackTrace.parsedStackTrace(in: environment) {
terminal.write(" \(item.symbol)", inColor: .cyan) terminal.write("\nAn error occurred, here's a stack trace for it:\n", inColor: .red)
terminal.write(" at \(item.location ?? "<unknown>")\n", inColor: .gray) stackTrace.forEach { item in
} terminal.write(" \(item.symbol)", inColor: .cyan)
} else { terminal.write(" at \(item.location ?? "<unknown>")\n", inColor: .gray)
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")
} }
} else {
case let .testRunOutput(output): terminal.write("\nAn error occurred, here's the raw stack trace for it:\n", inColor: .red)
TestsParser().parse(output, terminal) terminal.write(
" Please create an issue or PR to the Carton repository\n"
case .testPassed: + " with your browser name and this raw stack trace so\n"
Task { await self.stopTest(hadError: false) } + " we can add support for it: https://github.com/swiftwasm/carton\n", inColor: .gray
)
case let .errorReport(output): terminal.write(rawStackTrace + "\n")
terminal.write("\nAn error occurred:\n", inColor: .red)
terminal.write(output + "\n")
Task { await self.stopTest(hadError: true) }
} }
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")
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)
switch kind {
case 1001:
// stdout
let chunk = data.subdata(in: 2..<data.count)
if chunk.isEmpty { return }
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 }
for line in Self.decodeLines(data: chunk) {
terminal.write("stderr: " + line + "\n", inColor: .red)
}
default: break
} }
} }
} }

View File

@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import Foundation
import NIO import NIO
import NIOWebSocket import NIOWebSocket
@ -20,7 +21,8 @@ final class ServerWebSocketHandler: ChannelInboundHandler {
typealias OutboundOut = WebSocketFrame typealias OutboundOut = WebSocketFrame
struct Configuration { struct Configuration {
let onText: @Sendable (String) -> Void var onText: @Sendable (String) -> Void
var onBinary: @Sendable (Data) -> Void
} }
private var awaitingClose: Bool = false private var awaitingClose: Bool = false
@ -43,7 +45,11 @@ final class ServerWebSocketHandler: ChannelInboundHandler {
var data = frame.unmaskedData var data = frame.unmaskedData
let text = data.readString(length: data.readableBytes) ?? "" let text = data.readString(length: data.readableBytes) ?? ""
self.configuration.onText(text) 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. // We ignore these frames.
break break
default: default:

File diff suppressed because one or more lines are too long

View File

@ -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)

View File

@ -44,9 +44,29 @@ const startWasiTask = async () => {
const wasmRunner = WasmRunner( 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) { onStdoutLine(line) {
console.log(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) { onStderrLine(line) {
console.error(line); console.error(line);
} }

2
package-lock.json generated
View File

@ -2183,4 +2183,4 @@
} }
} }
} }
} }