22// The .NET Foundation licenses this file to you under the MIT license.
33// See the LICENSE file in the project root for more information.
44
5+ using DevProxy . Proxy ;
56using DevProxy . State ;
67using System . CommandLine ;
78using System . Diagnostics ;
@@ -34,18 +35,16 @@ private async Task<int> RunAsync(ParseResult parseResult, CancellationToken canc
3435
3536 if ( pid is not null )
3637 {
37- var state = await StateManager . LoadStateByPidAsync ( pid . Value , cancellationToken ) ;
38- if ( state is null )
39- {
40- Console . WriteLine ( $ "No running Dev Proxy instance with PID { pid . Value } .") ;
41- return 1 ;
42- }
43-
44- return await StopInstanceAsync ( state , force , cancellationToken ) ;
38+ return await StopByPidAsync ( pid . Value , force , cancellationToken ) ;
4539 }
4640
41+ // Reconcile any system-proxy registrations left behind by crashed
42+ // instances first — this must run before LoadAllStatesAsync, which prunes
43+ // (deletes) the stale state files it depends on.
44+ var reconciliation = await SystemProxyManager . ReconcileOrphanedSystemProxiesAsync ( cancellationToken ) ;
45+
4746 var states = await StateManager . LoadAllStatesAsync ( cancellationToken ) ;
48- if ( states . Count == 0 )
47+ if ( states . Count == 0 && reconciliation . Orphans . Count == 0 )
4948 {
5049 Console . WriteLine ( "Dev Proxy is not running." ) ;
5150 return 1 ;
@@ -61,9 +60,55 @@ private async Task<int> RunAsync(ParseResult parseResult, CancellationToken canc
6160 }
6261 }
6362
63+ ReportReconciledOrphans ( reconciliation ) ;
64+
6465 return exitCode ;
6566 }
6667
68+ private static async Task < int > StopByPidAsync ( int pid , bool force , CancellationToken cancellationToken )
69+ {
70+ // Capture a potential orphaned system-proxy record before LoadStateByPidAsync
71+ // prunes (deletes) the stale state file for a dead PID.
72+ var orphan = ( await StateManager . GetOrphanedSystemProxyStatesAsync ( cancellationToken ) )
73+ . Find ( o => o . Pid == pid ) ;
74+
75+ var state = await StateManager . LoadStateByPidAsync ( pid , cancellationToken ) ;
76+ if ( state is not null )
77+ {
78+ return await StopInstanceAsync ( state , force , cancellationToken ) ;
79+ }
80+
81+ if ( orphan is not null )
82+ {
83+ var liveOwner = await StateManager . FindSystemProxyInstanceAsync ( cancellationToken ) ;
84+ if ( liveOwner is null )
85+ {
86+ SystemProxyManager . Disable ( ) ;
87+ Console . WriteLine ( $ "Restored system proxy left by crashed Dev Proxy (PID: { pid } ).") ;
88+ }
89+ else
90+ {
91+ Console . WriteLine ( $ "Removed stale record for crashed Dev Proxy (PID: { pid } ); system proxy is owned by a running instance (PID: { liveOwner . Pid } ).") ;
92+ }
93+
94+ await StateManager . DeleteStateAsync ( pid , cancellationToken ) ;
95+ return 0 ;
96+ }
97+
98+ Console . WriteLine ( $ "No running Dev Proxy instance with PID { pid } .") ;
99+ return 1 ;
100+ }
101+
102+ private static void ReportReconciledOrphans ( SystemProxyManager . OrphanReconciliation reconciliation )
103+ {
104+ foreach ( var orphan in reconciliation . Orphans )
105+ {
106+ Console . WriteLine ( reconciliation . SystemProxyDisabled
107+ ? $ "Restored system proxy left by crashed Dev Proxy (PID: { orphan . Pid } )."
108+ : $ "Removed stale record for crashed Dev Proxy (PID: { orphan . Pid } ); system proxy is owned by a running instance.") ;
109+ }
110+ }
111+
67112 private static async Task < int > StopInstanceAsync ( ProxyInstanceState state , bool force , CancellationToken cancellationToken )
68113 {
69114 if ( force )
@@ -146,7 +191,12 @@ private static async Task<int> StopInstanceAsync(ProxyInstanceState state, bool
146191
147192 private static async Task < int > ForceStopAsync ( ProxyInstanceState state , CancellationToken cancellationToken )
148193 {
149- DisableSystemProxy ( ) ;
194+ // A killed process can't run its own cleanup, so restore the system proxy
195+ // on its behalf before terminating it.
196+ if ( state . AsSystemProxy )
197+ {
198+ SystemProxyManager . Disable ( ) ;
199+ }
150200
151201 try
152202 {
@@ -172,46 +222,4 @@ private static async Task<int> ForceStopAsync(ProxyInstanceState state, Cancella
172222 await StateManager . DeleteStateAsync ( state . Pid , cancellationToken ) ;
173223 return 0 ;
174224 }
175-
176- /// <summary>
177- /// Disables the system proxy on macOS by calling toggle-proxy.sh off.
178- /// This ensures the system proxy settings are cleaned up even when the
179- /// daemon process is killed forcefully (SIGKILL cannot be caught).
180- /// </summary>
181- private static void DisableSystemProxy ( )
182- {
183- if ( ! OperatingSystem . IsMacOS ( ) )
184- {
185- return ;
186- }
187-
188- var bashScriptPath = Path . Join ( AppContext . BaseDirectory , "toggle-proxy.sh" ) ;
189- if ( ! File . Exists ( bashScriptPath ) )
190- {
191- return ;
192- }
193-
194- var startInfo = new ProcessStartInfo
195- {
196- FileName = "/bin/bash" ,
197- Arguments = $ "{ bashScriptPath } off",
198- RedirectStandardOutput = true ,
199- UseShellExecute = false ,
200- CreateNoWindow = true
201- } ;
202-
203- try
204- {
205- using var process = new Process { StartInfo = startInfo } ;
206- process . Start ( ) ;
207- if ( ! process . WaitForExit ( TimeSpan . FromSeconds ( 10 ) ) )
208- {
209- process . Kill ( ) ;
210- }
211- }
212- catch
213- {
214- // Best-effort cleanup — don't block the stop flow
215- }
216- }
217225}
0 commit comments