@@ -141,11 +141,17 @@ final class AppRegistry: ObservableObject {
141141 guard !pendingOps. contains ( app. id) else { return }
142142 pendingOps. insert ( app. id)
143143 statuses [ app. id] = . checking
144+ errorMessage = nil
144145
145146 Task {
146147 defer { pendingOps. remove ( app. id) }
147148
148- await DevkitCLI . start ( app. name)
149+ let result = await DevkitCLI . start ( app. name)
150+ guard result. exitCode == 0 else {
151+ statuses [ app. id] = . stopped
152+ errorMessage = result. output. trimmingCharacters ( in: . whitespacesAndNewlines)
153+ return
154+ }
149155
150156 // Poll until the port responds (up to 6s)
151157 for _ in 0 ..< 10 {
@@ -163,57 +169,34 @@ final class AppRegistry: ObservableObject {
163169 guard !pendingOps. contains ( app. id) else { return }
164170 pendingOps. insert ( app. id)
165171 statuses [ app. id] = . checking
172+ errorMessage = nil
166173
167174 Task {
168175 defer { pendingOps. remove ( app. id) }
169176
170- // 1. Tell devkit to stop it (updates its own process table )
171- await DevkitCLI . stop ( app . name )
172-
173- // 2. Kill by port — targets the LISTENING process only (-sTCP:LISTEN),
174- // works regardless of what devkit's stop command does.
175- // SIGTERM first, then SIGKILL 500ms later if still alive.
176- await killListeningProcess ( on : app . port )
177+ let result = await DevkitCLI . stop ( app . name )
178+ guard result . exitCode == 0 else {
179+ let stillUp = await PortChecker . isReachable ( port : app . port )
180+ statuses [ app . id ] = stillUp ? . running : . stopped
181+ errorMessage = result . output . trimmingCharacters ( in : . whitespacesAndNewlines )
182+ return
183+ }
177184
178- // 3. Verify: poll until port is actually closed (up to 4s )
179- for _ in 0 ..< 8 {
185+ // Verify: poll until port is actually closed (up to 6s )
186+ for _ in 0 ..< 12 {
180187 try ? await Task . sleep ( for: . milliseconds( 500 ) )
181188 if !( await PortChecker . isReachable ( port: app. port) ) {
182189 statuses [ app. id] = . stopped
183190 return
184191 }
185192 }
186193
187- // Port is still open after 4s — let polling decide the real status
188- // (process might be managed externally and respawned)
194+ // Port is still open after 6s — let polling decide the real status.
189195 let stillUp = await PortChecker . isReachable ( port: app. port)
190196 statuses [ app. id] = stillUp ? . running : . stopped
191197 }
192198 }
193199
194- // MARK: – Port kill
195-
196- /// Kills the process listening on the given TCP port.
197- /// Uses `-sTCP:LISTEN` so only the server process is targeted, not clients connected to it.
198- private func killListeningProcess( on port: Int ) async {
199- await withCheckedContinuation { ( cont: CheckedContinuation < Void , Never > ) in
200- let task = Process ( )
201- task. executableURL = URL ( fileURLWithPath: " /bin/sh " )
202- task. arguments = [ " -c " , """
203- pids=$(lsof -nP -iTCP: \( port) -sTCP:LISTEN -t 2>/dev/null)
204- if [ -n " $pids " ]; then
205- echo " $pids " | xargs kill -TERM 2>/dev/null
206- sleep 0.5
207- pids=$(lsof -nP -iTCP: \( port) -sTCP:LISTEN -t 2>/dev/null)
208- [ -n " $pids " ] && echo " $pids " | xargs kill -KILL 2>/dev/null
209- fi
210- """ ]
211- task. terminationHandler = { _ in cont. resume ( ) }
212- do { try task. run ( ) }
213- catch { cont. resume ( ) }
214- }
215- }
216-
217200 // MARK: – Derived
218201
219202 func status( for app: AppEntry ) -> AppStatus { statuses [ app. id] ?? . checking }
0 commit comments