Implement rebuilds and reloads for `carton dev`

This commit is contained in:
Max Desiatov 2020-06-08 20:41:40 +01:00
parent 28a0658269
commit e6aec262b3
No known key found for this signature in database
GPG Key ID: FE08EBF9CF58CBA2
7 changed files with 443 additions and 30 deletions

View File

@ -21,30 +21,63 @@ struct BuilderError: Error, CustomStringConvertible {
let description: String
}
struct Builder {
final class Builder {
let publisher: AnyPublisher<String, Error>
private let process: TSCBasic.Process
init(_ arguments: [String]) {
init(_ arguments: [String], _ terminal: TerminalController) {
let subject = PassthroughSubject<String, Error>()
publisher = subject.eraseToAnyPublisher()
publisher = subject
.handleEvents(receiveOutput: {
terminal.write($0)
}, receiveCompletion: {
switch $0 {
case .finished:
terminal.write("Build completed successfully\n\n", inColor: .green, bold: false)
case let .failure(error):
let errorString = String(describing: error)
if errorString.isEmpty {
terminal.write("Build failed, check the build process output above.\n", inColor: .red)
} else {
terminal.write("Build failed and produced following output: \n", inColor: .red)
print(error)
}
}
})
.eraseToAnyPublisher()
let stdout: TSCBasic.Process.OutputClosure = {
guard let string = String(data: Data($0), encoding: .utf8) else { return }
subject.send(string)
DispatchQueue.global().async {
let stdout: TSCBasic.Process.OutputClosure = {
guard let string = String(data: Data($0), encoding: .utf8) else { return }
subject.send(string)
}
var stderrBuffer = [UInt8]()
let stderr: TSCBasic.Process.OutputClosure = {
stderrBuffer.append(contentsOf: $0)
}
let process = Process(
arguments: arguments,
outputRedirection: .stream(stdout: stdout, stderr: stderr),
verbose: true,
startNewProcessGroup: true
)
let result = Result<ProcessResult, Error> {
try process.launch()
return try process.waitUntilExit()
}
switch result.map(\.exitStatus) {
case .success(.terminated(code: 0)):
subject.send(completion: .finished)
case let .failure(error):
subject.send(completion: .failure(error))
default:
let errorDescription = String(data: Data(stderrBuffer), encoding: .utf8) ?? ""
return subject.send(completion: .failure(BuilderError(description: errorDescription)))
}
}
let stderr: TSCBasic.Process.OutputClosure = {
guard let string = String(data: Data($0), encoding: .utf8) else { return }
subject.send(completion: .failure(BuilderError(description: string)))
}
process = Process(
arguments: arguments,
outputRedirection: .stream(stdout: stdout, stderr: stderr),
verbose: true,
startNewProcessGroup: true
)
}
}

View File

@ -0,0 +1,332 @@
// MIT License
// Copyright (c) 2019 Sergej Jaskiewicz
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
//
// Auto-generated from GYB template. DO NOT EDIT!
//
//
//
//
// Publishers.Catch.swift
//
//
// Created by Sergej Jaskiewicz on 25.12.2019.
//
import COpenCombineHelpers
import OpenCombine
extension Publisher {
/// Handles errors from an upstream publisher by replacing it with another publisher.
///
/// The following example replaces any error from the upstream publisher and replaces
/// the upstream with a `Just` publisher. This continues the stream by publishing
/// a single value and completing normally.
/// ```
/// enum SimpleError: Error { case error }
/// let errorPublisher = (0..<10).publisher.tryMap { v -> Int in
/// if v < 5 {
/// return v
/// } else {
/// throw SimpleError.error
/// }
/// }
///
/// let noErrorPublisher = errorPublisher.catch { _ in
/// return Just(100)
/// }
/// ```
/// Backpressure note: This publisher passes through `request` and `cancel` to
/// the upstream. After receiving an error, the publisher sends sends any unfulfilled
/// demand to the new `Publisher`.
///
/// - Parameter handler: A closure that accepts the upstream failure as input and
/// returns a publisher to replace the upstream publisher.
/// - Returns: A publisher that handles errors from an upstream publisher by replacing
/// the failed publisher with another publisher.
public func `catch`<NewPublisher: Publisher>(
_ handler: @escaping (Failure) -> NewPublisher
) -> Publishers.Catch<Self, NewPublisher>
where NewPublisher.Output == Output {
return .init(upstream: self, handler: handler)
}
}
extension Publishers {
/// A publisher that handles errors from an upstream publisher by replacing the failed
/// publisher with another publisher.
public struct Catch<Upstream: Publisher, NewPublisher: Publisher>: Publisher
where Upstream.Output == NewPublisher.Output {
public typealias Output = Upstream.Output
public typealias Failure = NewPublisher.Failure
/// The publisher that this publisher receives elements from.
public let upstream: Upstream
/// A closure that accepts the upstream failure as input and returns a publisher
/// to replace the upstream publisher.
public let handler: (Upstream.Failure) -> NewPublisher
/// Creates a publisher that handles errors from an upstream publisher by
/// replacing the failed publisher with another publisher.
///
/// - Parameters:
/// - upstream: The publisher that this publisher receives elements from.
/// - handler: A closure that accepts the upstream failure as input and returns
/// a publisher to replace the upstream publisher.
public init(upstream: Upstream,
handler: @escaping (Upstream.Failure) -> NewPublisher) {
self.upstream = upstream
self.handler = handler
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Input == Output, Downstream.Failure == Failure {
let inner = Inner(downstream: subscriber, handler: handler)
let uncaughtS = Inner.UncaughtS(inner: inner)
upstream.subscribe(uncaughtS)
}
}
}
extension Publishers.Catch {
private final class Inner<Downstream: Subscriber>:
Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Upstream.Output,
Downstream.Failure == NewPublisher.Failure {
struct UncaughtS: Subscriber,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible {
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
let inner: Inner
var combineIdentifier: CombineIdentifier { inner.combineIdentifier }
func receive(subscription: Subscription) {
inner.receivePre(subscription: subscription)
}
func receive(_ input: Input) -> Subscribers.Demand {
inner.receivePre(input)
}
func receive(completion: Subscribers.Completion<Failure>) {
inner.receivePre(completion: completion)
}
var description: String { inner.description }
var customMirror: Mirror { inner.customMirror }
var playgroundDescription: Any { description }
}
struct CaughtS: Subscriber,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible {
typealias Input = NewPublisher.Output
typealias Failure = NewPublisher.Failure
let inner: Inner
var combineIdentifier: CombineIdentifier { inner.combineIdentifier }
func receive(subscription: Subscription) {
inner.receivePost(subscription: subscription)
}
func receive(_ input: Input) -> Subscribers.Demand {
inner.receivePost(input)
}
func receive(completion: Subscribers.Completion<Failure>) {
inner.receivePost(completion: completion)
}
var description: String { inner.description }
var customMirror: Mirror { inner.customMirror }
var playgroundDescription: Any { description }
}
private enum State {
case pendingPre
case pre(Subscription)
case pendingPost
case post(Subscription)
case cancelled
}
private let lock = __UnfairLock.allocate() // 0x10
private var demand = Subscribers.Demand.none // 0x18
private var state = State.pendingPre // 0x20
private let downstream: Downstream
private let handler: (Upstream.Failure) -> NewPublisher
init(downstream: Downstream,
handler: @escaping (Upstream.Failure) -> NewPublisher) {
self.downstream = downstream
self.handler = handler
}
deinit {
lock.deallocate()
}
func receivePre(subscription: Subscription) {
lock.lock()
guard case .pendingPre = state else {
lock.unlock()
subscription.cancel()
return
}
state = .pre(subscription)
lock.unlock()
downstream.receive(subscription: self)
}
func receivePre(_ input: Upstream.Output) -> Subscribers.Demand {
lock.lock()
demand -= 1
lock.unlock()
let newDemand = downstream.receive(input)
lock.lock()
demand += newDemand
lock.unlock()
return newDemand
}
func receivePre(completion: Subscribers.Completion<Upstream.Failure>) {
switch completion {
case .finished:
lock.lock()
if case .pre = state {
state = .cancelled
lock.unlock()
downstream.receive(completion: .finished)
} else {
lock.unlock()
}
case let .failure(error):
lock.lock()
if case .pre = state {
state = .pendingPost
lock.unlock()
handler(error).subscribe(CaughtS(inner: self))
} else {
lock.unlock()
}
}
}
func receivePost(subscription: Subscription) {
lock.lock()
guard case .pendingPost = state else {
lock.unlock()
subscription.cancel()
return
}
state = .post(subscription)
let demand = self.demand
lock.unlock()
if demand > 0 {
subscription.request(demand)
}
}
func receivePost(_ input: NewPublisher.Output) -> Subscribers.Demand {
downstream.receive(input)
}
func receivePost(completion: Subscribers.Completion<NewPublisher.Failure>) {
lock.lock()
guard case .post = state else {
lock.unlock()
return
}
state = .cancelled
lock.unlock()
downstream.receive(completion: completion)
}
func request(_ demand: Subscribers.Demand) {
if demand == .none {
fatalError("API Violation: demand must not be zero")
}
lock.lock()
switch state {
case .pendingPre:
lock.unlock()
case let .pre(subscription):
self.demand += demand
lock.unlock()
subscription.request(demand)
case .pendingPost:
self.demand += demand
lock.unlock()
case let .post(subscription):
lock.unlock()
subscription.request(demand)
case .cancelled:
lock.unlock()
}
}
func cancel() {
lock.lock()
switch state {
case let .pre(subscription), let .post(subscription):
state = .cancelled
lock.unlock()
subscription.cancel()
default:
state = .cancelled
lock.unlock()
}
}
var description: String { "Catch" }
var customMirror: Mirror {
let children: [Mirror.Child] = [
("downstream", downstream),
("demand", demand),
]
return Mirror(self, children: children)
}
var playgroundDescription: Any { description }
}
}

View File

@ -31,7 +31,7 @@ extension Publisher where Output == Progress, Failure: CustomStringConvertible {
switch $0 {
case .finished:
progressAnimation.complete(success: true)
terminal.write("Build finished succesfully\n", inColor: .green, bold: false)
terminal.write("Build finished succesfully\n", inColor: .green)
case let .failure(error):
progressAnimation.complete(success: false)
terminal.write(error.description, inColor: .red, bold: false)

View File

@ -14,6 +14,7 @@
import ArgumentParser
import Foundation
import OpenCombine
import TSCBasic
func processDataOutput(_ arguments: [String]) throws -> Data {
@ -41,15 +42,17 @@ struct Dev: ParsableCommand {
else { fatalError("failed to create an instance of `TerminalController`") }
// try checkDevDependencies(on: localFileSystem, terminal)
let fm = FileManager.default
terminal.write("Inferring basic settings...\n", inColor: .yellow)
let swiftPath: String
if
let data = FileManager.default.contents(atPath: ".swift-version"),
let data = fm.contents(atPath: ".swift-version"),
// get the first line of the file
let swiftVersion = String(data: data, encoding: .utf8)?.components(
separatedBy: CharacterSet.newlines
).first {
swiftPath = FileManager.default.homeDirectoryForCurrentUser
swiftPath = fm.homeDirectoryForCurrentUser
.appending(".swiftenv", "versions", swiftVersion, "usr", "bin", "swift")
.path
} else {
@ -89,13 +92,33 @@ struct Dev: ParsableCommand {
guard let sources = localFileSystem.currentWorkingDirectory?.appending(component: "Sources")
else { fatalError("failed to infer the sources directory") }
terminal.write("\nWatching this directory for changes: ", inColor: .green, bold: false)
terminal.write("\nBuilding the project before spinning up a server...\n", inColor: .yellow)
let builderArguments = [swiftPath, "build", "--triple", "wasm32-unknown-wasi"]
var subscription: AnyCancellable?
try await { completion in
subscription = Builder(builderArguments, terminal).publisher
.sink(
receiveCompletion: { _ in completion(Result<(), Never>.success(())) },
receiveValue: { _ in }
)
}
guard fm.fileExists(atPath: mainWasmURL.path) else {
return terminal.write(
"Failed to build the main executable binary, fix the build errors and restart\n"
)
}
terminal.write("\nWatching this directory for changes: ", inColor: .green)
terminal.logLookup("", sources)
terminal.write("\n")
try Server(
builderArguments: builderArguments,
pathsToWatch: localFileSystem.traverseRecursively(sources),
mainWasmPath: mainWasmURL.path
mainWasmPath: mainWasmURL.path,
terminal
).run()
}
}

View File

@ -22,9 +22,15 @@ final class Server {
private var wsConnection: WebSocket?
private var subscriptions = [AnyCancellable]()
private let watcher: Watcher
private var builder: Builder?
private let app: Application
init(pathsToWatch: [AbsolutePath], mainWasmPath: String) throws {
init(
builderArguments: [String],
pathsToWatch: [AbsolutePath],
mainWasmPath: String,
_ terminal: TerminalController
) throws {
watcher = try Watcher(pathsToWatch)
var env = Environment.development
@ -35,9 +41,24 @@ final class Server {
}
watcher.publisher
.sink { [weak self] _ in
self?.wsConnection?.send("reload")
.flatMap(maxPublishers: .max(1)) { changes -> AnyPublisher<String, Never> in
terminal.write("\nThese paths have changed, rebuilding...\n", inColor: .yellow)
for change in changes.map(\.pathString) {
terminal.write("- \(change)\n", inColor: .cyan)
}
terminal.write("\n")
return Builder(builderArguments, terminal)
.publisher
.handleEvents(receiveCompletion: { [weak self] in
guard case .finished = $0 else { return }
self?.wsConnection?.send("reload")
})
.catch { _ in Empty().eraseToAnyPublisher() }
.eraseToAnyPublisher()
}
.sink(
receiveValue: { _ in }
)
.store(in: &subscriptions)
}

View File

@ -22,4 +22,4 @@ divElement.innerText = "Hello, world"
let body = document.body.object!
_ = body.appendChild!(divElement)
print("hello, world")
print("Hello, world!")

View File

@ -16,7 +16,11 @@ const wasi = new WASI({
const socket = new WebSocket("ws://127.0.0.1:8080/watcher");
socket.onmessage = (message) => console.log(message.data);
socket.onmessage = (message) => {
if (message.data === "reload") {
location.reload();
}
};
const startWasiTask = async () => {
// Fetch our Wasm File