From 9bb2af243480ca4a2c210082e630ae425bd48b31 Mon Sep 17 00:00:00 2001 From: Sam Estep Date: Wed, 9 Sep 2026 18:05:42 -0400 Subject: [PATCH] Run ErrorReportingTask operations on the caller's actor (#1324) VZVirtualMachine asserts that it is used on the queue it was created with, which for tart is the main queue. Since #1262 replaced the unstructured Task in "tart run"'s SIGUSR2 handler with ErrorReportingTask, that assertion fails: Task.init carries @_inheritActorContext, but ErrorReportingTask.init did not, so an operation written inside MainActor-isolated Run.runOnMainThread() is formed in a nonisolated init and runs on the cooperative pool, not the main queue. The result is that asking a VM to stop gracefully kills it instead. Sending SIGUSR2, which #842 hooked to requestStop() for exactly this purpose, crashes the process: Thread 1 queue: com.apple.root.default-qos.cooperative _dispatch_assert_queue_fail dispatch_assert_queue -[VZVirtualMachine requestStopWithError:] closure in Run.runOnMainThread() closure in ErrorReportingTask.init(_:operation:) The guest then loses power without a chance to flush, and on a Linux guest with ext4's default delayed allocation that discards whatever had not been written back yet. The same applies to the requestStop() in applicationShouldTerminate(), i.e. closing the window of a VM run with a GUI. Give the operation the same @_inheritActorContext that Task.init has, so that wrapping a call in ErrorReportingTask no longer changes where it runs. Co-authored-by: Claude Opus 5 --- Sources/tart/Utils.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/tart/Utils.swift b/Sources/tart/Utils.swift index 1f2dc70..b7442e3 100644 --- a/Sources/tart/Utils.swift +++ b/Sources/tart/Utils.swift @@ -6,8 +6,11 @@ import Foundation struct ErrorReportingTask { let task: Task + // Inherit the caller's actor context, exactly as Task.init does. Without this, an + // operation written inside a @MainActor function runs on the cooperative pool + // rather than the main queue, trapping in callees that assert their queue. @discardableResult - init(_ context: String, operation: @escaping @Sendable () async throws -> Void) { + init(_ context: String, @_inheritActorContext operation: @escaping @Sendable () async throws -> Void) { task = Task { do { try await operation()