Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ on:
- "tools/build_macos.py"
- "tools/make_icon.py"
- ".github/workflows/release.yml"
# The front ends themselves. Neither is built by CI, so a change here
# reaches a compiler for the first time in this workflow - and the macOS
# one cannot be built anywhere else in this project at all.
- "macos/**"
- "winui/**"
workflow_dispatch:

permissions:
Expand Down
6 changes: 4 additions & 2 deletions macos/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ let package = Package(
name: "MThreadDraw",
platforms: [.macOS(.v13)],
targets: [
// No file here is called main.swift, which is what lets @main in
// MThreadDrawApp.swift stand: a file by that name is top-level code,
// and the two cannot coexist.
.executableTarget(
name: "MThreadDraw",
path: "Sources/MThreadDraw",
swiftSettings: [.unsafeFlags(["-parse-as-library"])]
path: "Sources/MThreadDraw"
)
]
)
7 changes: 5 additions & 2 deletions macos/Sources/MThreadDraw/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ final class Model: ObservableObject {
func stop() { engine.post("stop") }
}

@MainActor
struct ContentView: View {
@StateObject private var model = Model()

Expand Down Expand Up @@ -227,7 +228,9 @@ struct ContentView: View {
Text("Portraits, animals, nature").tag("flow")
}
.labelsHidden()
.onChange(of: model.tracer) { Task { await model.preview() } }
// The closure takes the new value: the argument-less form of onChange
// is macOS 14, and this app runs on 13.
.onChange(of: model.tracer) { _ in Task { await model.preview() } }

slider("How much detail", value: $model.detail, range: 1...10,
caption: detailWord(model.detail)) {
Expand Down Expand Up @@ -294,7 +297,7 @@ struct ContentView: View {
.padding(.horizontal, 10).padding(.vertical, 6)
.background {
RoundedRectangle(cornerRadius: 8)
.fill(index == model.current ? .white.opacity(0.14) : .clear)
.fill(Color.white.opacity(index == model.current ? 0.14 : 0))
}
.opacity(layer.visible ? 1 : 0.45)
.onTapGesture { Task { await model.run("layer_select", ["index": index]) } }
Expand Down
51 changes: 41 additions & 10 deletions macos/Sources/MThreadDraw/Engine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,14 @@ final class Engine {
process.arguments = arguments
process.standardInput = input
process.standardOutput = output
process.standardError = Pipe()
// Drained, not ignored. A pipe nobody reads fills at 64 KB and the
// next write blocks for ever, so the engine would hang rather than
// fail - and the last few lines are what says why it did.
let errors = Pipe()
process.standardError = errors
errors.fileHandleForReading.readabilityHandler = { [weak self] handle in
self?.absorbErrors(handle.availableData)
}
if let repository = Engine.findRepository() {
process.currentDirectoryURL = repository
}
Expand All @@ -108,9 +115,12 @@ final class Engine {
self?.readyContinuation = continuation
}
}
group.addTask {
group.addTask { [weak self] in
try await Task.sleep(for: .seconds(30))
throw EngineError.notStarted("It produced no output in thirty seconds.")
let detail = self?.lastErrors ?? ""
let complaint = "It produced no output in thirty seconds."
throw EngineError.notStarted(
detail.isEmpty ? complaint : complaint + "\n\n" + detail)
}
try await group.next()
group.cancelAll()
Expand All @@ -119,6 +129,14 @@ final class Engine {

// MARK: - reading

/// The tail of the engine's standard error, for when it dies without a word.
private(set) var lastErrors = ""

private func absorbErrors(_ data: Data) {
guard !data.isEmpty, let text = String(data: data, encoding: .utf8) else { return }
lastErrors = String((lastErrors + text).suffix(4000))
}

private func absorb(_ data: Data) {
guard !data.isEmpty else { return }
buffer.append(data)
Expand Down Expand Up @@ -179,24 +197,37 @@ final class Engine {
/// Send one request and wait for its reply.
@discardableResult
func call(_ operation: String, _ arguments: [String: Any] = [:]) async throws -> [String: Any] {
lock.lock()
nextId += 1
let id = nextId
lock.unlock()
let id = claimId()

var request: [String: Any] = arguments
request["id"] = id
request["op"] = operation
let line = try JSONSerialization.data(withJSONObject: request) + Data([0x0A])

return try await withCheckedThrowingContinuation { continuation in
lock.lock()
pending[id] = continuation
lock.unlock()
register(id, continuation)
input.fileHandleForWriting.write(line)
}
}

// Both of these exist to be synchronous. Taking a lock directly in an async
// function is a warning today and an error under Swift 6, because the
// thread that resumes after an await need not be the one that took it;
// inside a plain method there is no await to move, so the pair is safe.
private func claimId() -> Int {
lock.lock()
defer { lock.unlock() }
nextId += 1
return nextId
}

private func register(_ id: Int,
_ continuation: CheckedContinuation<[String: Any], Error>) {
lock.lock()
defer { lock.unlock() }
pending[id] = continuation
}

/// Send a request without waiting - for stop, which cannot queue.
func post(_ operation: String) {
let line = "{\"op\": \"\(operation)\"}\n"
Expand Down
7 changes: 5 additions & 2 deletions tools/github.sh
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,17 @@ if [ -z "$TOKEN" ]; then
exit 1
fi

# --location because the log endpoints answer with a redirect to wherever the
# logs are actually stored, and a job log is the only way to see why a build
# failed on a machine you do not have.
if [ -n "$BODY" ]; then
curl -sS -X "$METHOD" \
curl -sS --location -X "$METHOD" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Accept: application/vnd.github+json" \
-d "$BODY" \
"${API}${PATH_UNDER_REPO}"
else
curl -sS -X "$METHOD" \
curl -sS --location -X "$METHOD" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Accept: application/vnd.github+json" \
"${API}${PATH_UNDER_REPO}"
Expand Down
Loading