@@ -825,87 +825,6 @@ extension RunnerTests {
825825 XCTAssertTrue ( response. error? . hint? . contains ( " runner session will be restarted " ) == true )
826826 }
827827
828- func testRunMainThreadWorkExecutesOffMainCallerOnMainThread( ) {
829- final class ResultBox {
830- var observedMainThread : Bool ?
831- var error : Error ?
832- }
833- let box = ResultBox ( )
834- let finished = expectation ( description: " off-main caller finished " )
835-
836- DispatchQueue ( label: " agent-device.runner.tests.off-main " ) . async {
837- do {
838- box. observedMainThread = try self . runMainThreadWork (
839- timeout: 1 ,
840- timeoutError: self . mainThreadExecutionTimeoutError
841- ) {
842- Thread . isMainThread
843- }
844- } catch {
845- box. error = error
846- }
847- finished. fulfill ( )
848- }
849-
850- wait ( for: [ finished] , timeout: 2 )
851- XCTAssertNil ( box. error)
852- XCTAssertEqual ( box. observedMainThread, true )
853- }
854-
855- func testRunMainThreadWorkTimeoutMarksAbandonedUntilDrained( ) {
856- final class ResultBox {
857- var error : Error ?
858- var abandonedCount : Int ?
859- var abandonedSinceSet : Bool ?
860- var drainedCount : Int ?
861- var drainedSinceCleared : Bool ?
862- }
863- let box = ResultBox ( )
864- let releaseWork = DispatchSemaphore ( value: 0 )
865- let observedAbandoned = DispatchSemaphore ( value: 0 )
866- let finished = expectation ( description: " off-main caller timed out " )
867- let drained = expectation ( description: " abandoned main work drained " )
868-
869- DispatchQueue ( label: " agent-device.runner.tests.timeout " ) . async {
870- do {
871- _ = try self . runMainThreadWork (
872- timeout: 0 ,
873- timeoutError: self . mainThreadExecutionTimeoutError,
874- onAbandoned: {
875- box. abandonedCount = self . abandonedMainThreadWorkCount
876- box. abandonedSinceSet = self . abandonedMainThreadWorkSince != nil
877- observedAbandoned. signal ( )
878- } ,
879- onDrained: {
880- self . mainThreadWorkLock. lock ( )
881- box. drainedCount = self . abandonedMainThreadWorkCount
882- box. drainedSinceCleared = self . abandonedMainThreadWorkSince == nil
883- self . mainThreadWorkLock. unlock ( )
884- drained. fulfill ( )
885- }
886- ) {
887- _ = releaseWork. wait ( timeout: . now( ) + 1 )
888- return true
889- }
890- } catch {
891- box. error = error
892- }
893- finished. fulfill ( )
894- }
895-
896- DispatchQueue ( label: " agent-device.runner.tests.release-timeout " ) . async {
897- _ = observedAbandoned. wait ( timeout: . now( ) + 1 )
898- releaseWork. signal ( )
899- }
900-
901- wait ( for: [ finished, drained] , timeout: 2 )
902- XCTAssertEqual ( ( box. error as NSError ? ) ? . code, RunnerErrorCode . mainThreadExecutionTimedOut)
903- XCTAssertEqual ( box. abandonedCount, 1 )
904- XCTAssertEqual ( box. abandonedSinceSet, true )
905- XCTAssertEqual ( box. drainedCount, 0 )
906- XCTAssertEqual ( box. drainedSinceCleared, true )
907- }
908-
909828 func testPostSnapshotDelayMarkDoesNotQueueBehindAbandonedTreeCapture( ) {
910829 abandonedTreeCaptureCount = 1
911830 defer {
@@ -977,13 +896,6 @@ extension RunnerTests {
977896 )
978897 }
979898
980- /// Tracks one main-queue dispatch so the watchdog and the dispatched block can agree —
981- /// under `mainThreadWorkLock` — on exactly one of: finished in time, or abandoned.
982- private final class MainThreadWorkState {
983- var finished = false
984- var abandoned = false
985- }
986-
987899 struct ActiveCommandContext {
988900 let app : XCUIApplication
989901 /// Set when `app` is a system surface served in place over the still-bound session app (#2438).
@@ -995,31 +907,6 @@ extension RunnerTests {
995907 case context( ActiveCommandContext )
996908 }
997909
998- enum MainThreadBusyState {
999- case idle
1000- case busy( abandonedForSeconds: TimeInterval )
1001- case wedged( abandonedForSeconds: TimeInterval )
1002-
1003- /// Whether the main thread is occupied by watchdog-abandoned work, for the occupancy stamp that
1004- /// every successful response carries. Wedged is still occupied: it only differs in that a
1005- /// restart, not waiting, is the cure.
1006- var reportsMainThreadBusy : Bool {
1007- if case . idle = self { return false }
1008- return true
1009- }
1010- }
1011-
1012- func currentMainThreadBusyState( ) -> MainThreadBusyState {
1013- mainThreadWorkLock. lock ( )
1014- defer { mainThreadWorkLock. unlock ( ) }
1015- guard abandonedMainThreadWorkCount > 0 else { return . idle }
1016- let abandonedFor = abandonedMainThreadWorkSince. map { Date ( ) . timeIntervalSince ( $0) } ?? 0
1017- if abandonedFor > mainThreadWedgeThreshold {
1018- return . wedged( abandonedForSeconds: abandonedFor)
1019- }
1020- return . busy( abandonedForSeconds: abandonedFor)
1021- }
1022-
1023910 private func runnerBusyResponse( command: Command , abandonedForSeconds: TimeInterval ) -> Response {
1024911 NSLog (
1025912 " AGENT_DEVICE_RUNNER_BUSY command=%@ commandId=%@ abandonedForSeconds=%.1f " ,
@@ -1118,77 +1005,6 @@ extension RunnerTests {
11181005 }
11191006 }
11201007
1121- func runMainThreadWork< T> (
1122- timeout: TimeInterval ,
1123- timeoutError: @escaping ( ) -> Error ,
1124- onAbandoned: ( ( ) -> Void ) ? = nil ,
1125- onDrained: ( ( ) -> Void ) ? = nil ,
1126- _ work: @escaping ( ) throws -> T
1127- ) throws -> T {
1128- if Thread . isMainThread {
1129- return try work ( )
1130- }
1131- var result : Result < T , Error > ?
1132- let semaphore = DispatchSemaphore ( value: 0 )
1133- let workState = MainThreadWorkState ( )
1134- DispatchQueue . main. async {
1135- do {
1136- result = . success( try work ( ) )
1137- } catch {
1138- result = . failure( error)
1139- }
1140- self . mainThreadWorkLock. lock ( )
1141- if workState. abandoned {
1142- self . abandonedMainThreadWorkCount -= 1
1143- if self . abandonedMainThreadWorkCount == 0 {
1144- self . abandonedMainThreadWorkSince = nil
1145- NSLog ( " AGENT_DEVICE_RUNNER_ABANDONED_WORK_DRAINED " )
1146- }
1147- self . mainThreadWorkLock. unlock ( )
1148- onDrained ? ( )
1149- } else {
1150- workState. finished = true
1151- self . mainThreadWorkLock. unlock ( )
1152- }
1153- semaphore. signal ( )
1154- }
1155- let waitResult = semaphore. wait ( timeout: . now( ) + timeout)
1156- if waitResult == . timedOut {
1157- mainThreadWorkLock. lock ( )
1158- let stillRunning = !workState. finished
1159- if stillRunning {
1160- workState. abandoned = true
1161- abandonedMainThreadWorkCount += 1
1162- if abandonedMainThreadWorkSince == nil {
1163- abandonedMainThreadWorkSince = Date ( )
1164- }
1165- onAbandoned ? ( )
1166- }
1167- mainThreadWorkLock. unlock ( )
1168- throw timeoutError ( )
1169- }
1170- switch result {
1171- case . success( let value) :
1172- return value
1173- case . failure( let error) :
1174- throw error
1175- case . none:
1176- throw NSError (
1177- domain: RunnerErrorDomain . general,
1178- code: RunnerErrorCode . noResponseFromMainThread,
1179- userInfo: [ NSLocalizedDescriptionKey: " no response from main thread " ]
1180- )
1181- }
1182- }
1183-
1184- private func mainThreadExecutionTimeoutError( ) -> Error {
1185- NSError (
1186- domain: RunnerErrorDomain . general,
1187- code: RunnerErrorCode . mainThreadExecutionTimedOut,
1188- userInfo: [ NSLocalizedDescriptionKey: " main thread execution timed out " ]
1189- )
1190- }
1191-
11921008 // MARK: - Command Handling
11931009
11941010 private func executeOnMainSafely(
0 commit comments