Show stdout/stderr from Wasm in terminal (#472)
This commit is contained in:
parent
3cb3877d98
commit
6cc68d3ba8
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
@ -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)
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue