@@ -269,7 +269,7 @@ public struct KiroStatusProbe: Sendable {
269269 contextUsage: contextUsage)
270270 }
271271
272- private struct KiroCLIResult {
272+ struct KiroCLIResult {
273273 let stdout : String
274274 let stderr : String
275275 let terminationStatus : Int32
@@ -396,7 +396,7 @@ public struct KiroStatusProbe: Sendable {
396396 return self . parseContextUsage ( output: output)
397397 }
398398
399- private func runCommand(
399+ func runCommand(
400400 arguments: [ String ] ,
401401 timeout: TimeInterval ,
402402 idleTimeout: TimeInterval = 5.0 ) async throws -> KiroCLIResult
@@ -419,123 +419,100 @@ public struct KiroStatusProbe: Sendable {
419419 env [ " TERM " ] = " xterm-256color "
420420 process. environment = env
421421
422- // Thread-safe state for activity tracking
423422 final class ActivityState : @unchecked Sendable {
424423 private let lock = NSLock ( )
425424 private var _lastActivityAt = Date ( )
426425 private var _hasReceivedOutput = false
427- private var _stdoutData = Data ( )
428- private var _stderrData = Data ( )
429426
430427 var lastActivityAt : Date {
431- self . lock. lock ( )
432- defer { lock. unlock ( ) }
433- return self . _lastActivityAt
428+ self . lock. withLock { self . _lastActivityAt }
434429 }
435430
436431 var hasReceivedOutput : Bool {
437- self . lock. lock ( )
438- defer { lock. unlock ( ) }
439- return self . _hasReceivedOutput
432+ self . lock. withLock { self . _hasReceivedOutput }
440433 }
441434
442- func appendStdout( _ data: Data ) {
443- self . lock. lock ( )
444- defer { lock. unlock ( ) }
445- self . _stdoutData. append ( data)
446- self . _lastActivityAt = Date ( )
447- self . _hasReceivedOutput = true
448- }
449-
450- func appendStderr( _ data: Data ) {
451- self . lock. lock ( )
452- defer { lock. unlock ( ) }
453- self . _stderrData. append ( data)
454- self . _lastActivityAt = Date ( )
455- self . _hasReceivedOutput = true
456- }
457-
458- func getOutput( ) -> ( stdout: Data , stderr: Data ) {
459- self . lock. lock ( )
460- defer { lock. unlock ( ) }
461- return ( self . _stdoutData, self . _stderrData)
435+ func markActivity( ) {
436+ self . lock. withLock {
437+ self . _lastActivityAt = Date ( )
438+ self . _hasReceivedOutput = true
439+ }
462440 }
463441 }
464442
465443 let state = ActivityState ( )
466- stdoutPipe. fileHandleForReading. readabilityHandler = { handle in
467- let data = handle. availableData
468- if !data. isEmpty {
469- state. appendStdout ( data)
470- }
471- }
472- stderrPipe. fileHandleForReading. readabilityHandler = { handle in
473- let data = handle. availableData
474- if !data. isEmpty {
475- state. appendStderr ( data)
476- }
477- }
444+ let stdoutCapture = ProcessPipeCapture ( pipe: stdoutPipe, onData: { state. markActivity ( ) } )
445+ let stderrCapture = ProcessPipeCapture ( pipe: stderrPipe, onData: { state. markActivity ( ) } )
478446
479447 do {
480448 try process. run ( )
481449 } catch {
482- stdoutPipe . fileHandleForReading . readabilityHandler = nil
483- stderrPipe . fileHandleForReading . readabilityHandler = nil
450+ stdoutCapture . stop ( )
451+ stderrCapture . stop ( )
484452 throw error
485453 }
454+ stdoutCapture. start ( )
455+ stderrCapture. start ( )
456+ let pid = process. processIdentifier
457+ let processGroup : pid_t ? = setpgid ( pid, pid) == 0 ? pid : nil
486458
487459 let deadline = Date ( ) . addingTimeInterval ( timeout)
488460 var didHitDeadline = false
489461 var didTerminateForIdle = false
490462
491- while process. isRunning {
492- if Date ( ) >= deadline {
493- didHitDeadline = true
494- break
495- }
496- if state. hasReceivedOutput,
497- Date ( ) . timeIntervalSince ( state. lastActivityAt) >= idleTimeout
498- {
499- didTerminateForIdle = true
500- break
463+ do {
464+ while process. isRunning {
465+ try Task . checkCancellation ( )
466+ if Date ( ) >= deadline {
467+ didHitDeadline = true
468+ break
469+ }
470+ if state. hasReceivedOutput,
471+ Date ( ) . timeIntervalSince ( state. lastActivityAt) >= idleTimeout
472+ {
473+ didTerminateForIdle = true
474+ break
475+ }
476+ try await Task . sleep ( for: . milliseconds( 100 ) )
501477 }
502- try await Task . sleep ( for: . milliseconds( 100 ) )
478+ } catch {
479+ await Self . terminateProcess ( process, processGroup: processGroup)
480+ stdoutCapture. stop ( )
481+ stderrCapture. stop ( )
482+ throw error
503483 }
504484
505485 if process. isRunning {
506- Self . terminateProcess ( process)
486+ await Self . terminateProcess ( process, processGroup: processGroup)
487+ guard !process. isRunning else {
488+ stdoutCapture. stop ( )
489+ stderrCapture. stop ( )
490+ throw KiroStatusProbeError . timeout
491+ }
507492 if didHitDeadline || !state. hasReceivedOutput {
508- stdoutPipe . fileHandleForReading . readabilityHandler = nil
509- stderrPipe . fileHandleForReading . readabilityHandler = nil
493+ stdoutCapture . stop ( )
494+ stderrCapture . stop ( )
510495 throw KiroStatusProbeError . timeout
511496 }
512497 }
513498
514- try await Task . sleep ( for: . milliseconds( 100 ) )
515- stdoutPipe. fileHandleForReading. readabilityHandler = nil
516- stderrPipe. fileHandleForReading. readabilityHandler = nil
517-
518- let output = state. getOutput ( )
519- let stdoutOutput = String ( data: output. stdout, encoding: . utf8) ?? " "
520- let stderrOutput = String ( data: output. stderr, encoding: . utf8) ?? " "
499+ async let stdoutData = stdoutCapture. finish ( timeout: . seconds( 1 ) )
500+ async let stderrData = stderrCapture. finish ( timeout: . seconds( 1 ) )
501+ let output = await ( stdout: stdoutData, stderr: stderrData)
521502 return KiroCLIResult (
522- stdout: stdoutOutput ,
523- stderr: stderrOutput ,
503+ stdout: String ( data : output . stdout , encoding : . utf8 ) ?? " " ,
504+ stderr: String ( data : output . stderr , encoding : . utf8 ) ?? " " ,
524505 terminationStatus: process. terminationStatus,
525506 terminatedForIdle: didTerminateForIdle)
526507 }
527508
528- private static func terminateProcess( _ process: Process ) {
529- guard process. isRunning else { return }
530- process. terminate ( )
531- let deadline = Date ( ) . addingTimeInterval ( 0.4 )
532- while process. isRunning, Date ( ) < deadline {
533- usleep ( 50000 )
534- }
535- if process. isRunning {
536- kill ( process. processIdentifier, SIGKILL)
509+ private static func terminateProcess( _ process: Process , processGroup: pid_t ? ) async {
510+ await withCheckedContinuation { continuation in
511+ DispatchQueue . global ( qos: . userInitiated) . async {
512+ SubprocessRunner . terminateProcess ( process, processGroup: processGroup)
513+ continuation. resume ( )
514+ }
537515 }
538- process. waitUntilExit ( )
539516 }
540517
541518 func parse(
0 commit comments