Skip to content

Commit 8d4a62d

Browse files
Allow multiple Dev Proxy instances when asSystemProxy is false (#1589)
* Initial plan * Allow multiple Dev Proxy instances when asSystemProxy is false When asSystemProxy is false, the single-instance check is skipped so multiple Dev Proxy instances can run simultaneously. The AsSystemProxy value is now stored in the state file so the detached mode launch check can determine if the existing instance is acting as the system proxy. Closes #1586 Co-authored-by: waldekmastykarz <11164679+waldekmastykarz@users.noreply.github.com> * Switch to per-instance state files to avoid cross-instance interference Each detached instance now stores its state in a separate file keyed by PID (e.g. state-1234.json) instead of a single shared state.json. This ensures that: - Multiple non-system-proxy instances don't overwrite each other's state - Deleting state on exit only removes the current instance's file - devproxy stop/status/logs still work by preferring the system-proxy instance, then falling back to the most recently started one - Legacy state.json files are still read for backward compatibility StopCommand now passes the specific PID when deleting state to avoid accidentally removing another instance's state file. Co-authored-by: waldekmastykarz <11164679+waldekmastykarz@users.noreply.github.com> * Clarify boolean sort order in LoadStateAsync Co-authored-by: waldekmastykarz <11164679+waldekmastykarz@users.noreply.github.com> * Add PID option to logs, status, and stop commands; enhance instance management --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: waldekmastykarz <11164679+waldekmastykarz@users.noreply.github.com> Co-authored-by: Waldek Mastykarz <waldek@mastykarz.nl>
1 parent cf05b25 commit 8d4a62d

8 files changed

Lines changed: 360 additions & 104 deletions

File tree

DevProxy/Commands/DevProxyConfigOptions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public string? ConfigFile
1919
}
2020

2121
public int? ApiPort => _parseResult?.GetValueOrDefault<int?>(DevProxyCommand.ApiPortOptionName);
22+
public bool? AsSystemProxy => _parseResult?.GetValueOrDefault<bool?>(DevProxyCommand.AsSystemProxyOptionName);
2223
public int? Port => _parseResult?.GetValueOrDefault<int?>(DevProxyCommand.PortOptionName);
2324
public bool Discover => _parseResult?.GetValueOrDefault<bool?>(DevProxyCommand.DiscoverOptionName) ?? false;
2425
public string? IPAddress => _parseResult?.GetValueOrDefault<string?>(DevProxyCommand.IpAddressOptionName);
@@ -138,6 +139,7 @@ public DevProxyConfigOptions()
138139
};
139140

140141
var apiPortOption = new Option<int?>(DevProxyCommand.ApiPortOptionName);
142+
var asSystemProxyOption = new Option<bool?>(DevProxyCommand.AsSystemProxyOptionName);
141143
var portOption = new Option<int?>(DevProxyCommand.PortOptionName, "-p");
142144

143145
var discoverOption = new Option<bool>(DevProxyCommand.DiscoverOptionName, "--discover")
@@ -153,6 +155,7 @@ public DevProxyConfigOptions()
153155
var options = new List<Option>
154156
{
155157
apiPortOption,
158+
asSystemProxyOption,
156159
ipAddressOption,
157160
configFileOption,
158161
portOption,

DevProxy/Commands/LogsCommand.cs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,17 @@ internal sealed class LogsCommand : Command
2828
HelpName = "time"
2929
};
3030

31+
private readonly Option<int?> _pidOption = new("--pid")
32+
{
33+
Description = "Show logs from a specific Dev Proxy instance by PID"
34+
};
35+
3136
public LogsCommand() : base("logs", "Show logs from running Dev Proxy instance")
3237
{
3338
Add(_followOption);
3439
Add(_linesOption);
3540
Add(_sinceOption);
41+
Add(_pidOption);
3642

3743
SetAction(RunAsync);
3844
}
@@ -42,13 +48,26 @@ private async Task<int> RunAsync(ParseResult parseResult, CancellationToken canc
4248
var follow = parseResult.GetValue(_followOption);
4349
var lines = parseResult.GetValue(_linesOption);
4450
var since = parseResult.GetValue(_sinceOption);
51+
var pid = parseResult.GetValue(_pidOption);
4552

46-
var state = await StateManager.LoadStateAsync(cancellationToken);
47-
48-
if (state == null)
53+
ProxyInstanceState? state;
54+
if (pid is not null)
4955
{
50-
Console.WriteLine("Dev Proxy is not running.");
51-
return 1;
56+
state = await StateManager.LoadStateByPidAsync(pid.Value, cancellationToken);
57+
if (state is null)
58+
{
59+
Console.WriteLine($"No running Dev Proxy instance with PID {pid.Value}.");
60+
return 1;
61+
}
62+
}
63+
else
64+
{
65+
state = await StateManager.LoadStateAsync(cancellationToken);
66+
if (state is null)
67+
{
68+
Console.WriteLine("Dev Proxy is not running.");
69+
return 1;
70+
}
5271
}
5372

5473
var logFile = state.LogFile;

DevProxy/Commands/StatusCommand.cs

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,56 @@ namespace DevProxy.Commands;
1010

1111
internal sealed class StatusCommand : Command
1212
{
13-
public StatusCommand() : base("status", "Show status of running Dev Proxy instance")
13+
private readonly Option<int?> _pidOption = new("--pid")
1414
{
15+
Description = "Show status of a specific Dev Proxy instance by PID"
16+
};
17+
18+
public StatusCommand() : base("status", "Show status of running Dev Proxy instances")
19+
{
20+
Add(_pidOption);
1521
SetAction(RunAsync);
1622
}
1723

1824
private async Task<int> RunAsync(ParseResult parseResult, CancellationToken cancellationToken)
1925
{
20-
var state = await StateManager.LoadStateAsync(cancellationToken);
26+
var pid = parseResult.GetValue(_pidOption);
2127

22-
if (state == null)
28+
if (pid is not null)
29+
{
30+
var state = await StateManager.LoadStateByPidAsync(pid.Value, cancellationToken);
31+
if (state is null)
32+
{
33+
Console.WriteLine($"No running Dev Proxy instance with PID {pid.Value}.");
34+
return 1;
35+
}
36+
37+
await PrintInstanceStatusAsync(state, cancellationToken);
38+
return 0;
39+
}
40+
41+
var states = await StateManager.LoadAllStatesAsync(cancellationToken);
42+
if (states.Count == 0)
2343
{
2444
Console.WriteLine("Dev Proxy is not running.");
2545
return 1;
2646
}
2747

48+
for (var i = 0; i < states.Count; i++)
49+
{
50+
if (i > 0)
51+
{
52+
Console.WriteLine();
53+
}
54+
55+
await PrintInstanceStatusAsync(states[i], cancellationToken);
56+
}
57+
58+
return 0;
59+
}
60+
61+
private static async Task PrintInstanceStatusAsync(ProxyInstanceState state, CancellationToken cancellationToken)
62+
{
2863
// Try to get live status from the API
2964
try
3065
{
@@ -37,18 +72,19 @@ private async Task<int> RunAsync(ParseResult parseResult, CancellationToken canc
3772

3873
Console.WriteLine("Dev Proxy is running.");
3974
Console.WriteLine();
40-
Console.WriteLine($" PID: {state.Pid}");
41-
Console.WriteLine($" API URL: {state.ApiUrl}");
42-
Console.WriteLine($" Port: {state.Port}");
43-
Console.WriteLine($" Recording: {(proxyInfo?.Recording == true ? "Yes" : "No")}");
75+
Console.WriteLine($" PID: {state.Pid}");
76+
Console.WriteLine($" API URL: {state.ApiUrl}");
77+
Console.WriteLine($" Port: {state.Port}");
78+
Console.WriteLine($" System proxy: {(state.AsSystemProxy ? "Yes" : "No")}");
79+
Console.WriteLine($" Recording: {(proxyInfo?.Recording == true ? "Yes" : "No")}");
4480
if (!string.IsNullOrEmpty(state.ConfigFile))
4581
{
46-
Console.WriteLine($" Config: {state.ConfigFile}");
82+
Console.WriteLine($" Config: {state.ConfigFile}");
4783
}
48-
Console.WriteLine($" Log file: {state.LogFile}");
49-
Console.WriteLine($" Started: {state.StartedAt.LocalDateTime:g}");
84+
Console.WriteLine($" Log file: {state.LogFile}");
85+
Console.WriteLine($" Started: {state.StartedAt.LocalDateTime:g}");
5086

51-
return 0;
87+
return;
5288
}
5389
}
5490
catch (HttpRequestException)
@@ -63,12 +99,11 @@ private async Task<int> RunAsync(ParseResult parseResult, CancellationToken canc
6399
// Fall back to state file info
64100
Console.WriteLine("Dev Proxy appears to be running (API not responding).");
65101
Console.WriteLine();
66-
Console.WriteLine($" PID: {state.Pid}");
67-
Console.WriteLine($" API URL: {state.ApiUrl}");
68-
Console.WriteLine($" Log file: {state.LogFile}");
69-
Console.WriteLine($" Started: {state.StartedAt.LocalDateTime:g}");
70-
71-
return 0;
102+
Console.WriteLine($" PID: {state.Pid}");
103+
Console.WriteLine($" API URL: {state.ApiUrl}");
104+
Console.WriteLine($" System proxy: {(state.AsSystemProxy ? "Yes" : "No")}");
105+
Console.WriteLine($" Log file: {state.LogFile}");
106+
Console.WriteLine($" Started: {state.StartedAt.LocalDateTime:g}");
72107
}
73108

74109
private sealed class ProxyStatusInfo

DevProxy/Commands/StopCommand.cs

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,57 @@ internal sealed class StopCommand : Command
1515
Description = "Force stop the proxy by killing the process"
1616
};
1717

18-
public StopCommand() : base("stop", "Stop running Dev Proxy instance")
18+
private readonly Option<int?> _pidOption = new("--pid")
19+
{
20+
Description = "Stop a specific Dev Proxy instance by PID"
21+
};
22+
23+
public StopCommand() : base("stop", "Stop running Dev Proxy instances")
1924
{
2025
Add(_forceOption);
26+
Add(_pidOption);
2127
SetAction(RunAsync);
2228
}
2329

2430
private async Task<int> RunAsync(ParseResult parseResult, CancellationToken cancellationToken)
2531
{
2632
var force = parseResult.GetValue(_forceOption);
27-
var state = await StateManager.LoadStateAsync(cancellationToken);
33+
var pid = parseResult.GetValue(_pidOption);
34+
35+
if (pid is not null)
36+
{
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);
45+
}
2846

29-
if (state == null)
47+
var states = await StateManager.LoadAllStatesAsync(cancellationToken);
48+
if (states.Count == 0)
3049
{
3150
Console.WriteLine("Dev Proxy is not running.");
3251
return 1;
3352
}
3453

54+
var exitCode = 0;
55+
foreach (var state in states)
56+
{
57+
var result = await StopInstanceAsync(state, force, cancellationToken);
58+
if (result != 0)
59+
{
60+
exitCode = result;
61+
}
62+
}
63+
64+
return exitCode;
65+
}
66+
67+
private static async Task<int> StopInstanceAsync(ProxyInstanceState state, bool force, CancellationToken cancellationToken)
68+
{
3569
if (force)
3670
{
3771
return await ForceStopAsync(state, cancellationToken);
@@ -45,7 +79,7 @@ private async Task<int> RunAsync(ParseResult parseResult, CancellationToken canc
4579

4680
if (response.IsSuccessStatusCode || response.StatusCode == System.Net.HttpStatusCode.Accepted)
4781
{
48-
Console.WriteLine("Stopping Dev Proxy...");
82+
Console.WriteLine($"Stopping Dev Proxy (PID: {state.Pid})...");
4983

5084
// Wait for process to exit
5185
var stopwatch = Stopwatch.StartNew();
@@ -82,30 +116,30 @@ private async Task<int> RunAsync(ParseResult parseResult, CancellationToken canc
82116

83117
if (!exited)
84118
{
85-
Console.WriteLine("Dev Proxy did not stop in time.");
86-
Console.WriteLine("Use --force to forcefully terminate the process.");
119+
Console.WriteLine($"Dev Proxy (PID: {state.Pid}) did not stop in time.");
120+
Console.WriteLine($"Use --force --pid {state.Pid} to forcefully terminate the process.");
87121
return 1;
88122
}
89123

90-
await StateManager.DeleteStateAsync(cancellationToken);
91-
Console.WriteLine("Dev Proxy stopped.");
124+
await StateManager.DeleteStateAsync(state.Pid, cancellationToken);
125+
Console.WriteLine($"Dev Proxy (PID: {state.Pid}) stopped.");
92126
return 0;
93127
}
94128

95-
Console.WriteLine($"Failed to stop Dev Proxy: {response.StatusCode}");
96-
Console.WriteLine("Use --force to forcefully terminate the process.");
129+
Console.WriteLine($"Failed to stop Dev Proxy (PID: {state.Pid}): {response.StatusCode}");
130+
Console.WriteLine($"Use --force --pid {state.Pid} to forcefully terminate the process.");
97131
return 1;
98132
}
99133
catch (HttpRequestException ex)
100134
{
101-
Console.WriteLine($"Failed to connect to Dev Proxy API: {ex.Message}");
102-
Console.WriteLine("Use --force to forcefully terminate the process.");
135+
Console.WriteLine($"Failed to connect to Dev Proxy API (PID: {state.Pid}): {ex.Message}");
136+
Console.WriteLine($"Use --force --pid {state.Pid} to forcefully terminate the process.");
103137
return 1;
104138
}
105139
catch (TaskCanceledException)
106140
{
107-
Console.WriteLine("Timeout waiting for Dev Proxy to respond.");
108-
Console.WriteLine("Use --force to forcefully terminate the process.");
141+
Console.WriteLine($"Timeout waiting for Dev Proxy (PID: {state.Pid}) to respond.");
142+
Console.WriteLine($"Use --force --pid {state.Pid} to forcefully terminate the process.");
109143
return 1;
110144
}
111145
}
@@ -135,7 +169,7 @@ private static async Task<int> ForceStopAsync(ProxyInstanceState state, Cancella
135169
return 1;
136170
}
137171

138-
await StateManager.DeleteStateAsync(cancellationToken);
172+
await StateManager.DeleteStateAsync(state.Pid, cancellationToken);
139173
return 0;
140174
}
141175

DevProxy/Program.cs

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -51,23 +51,50 @@ static async Task<int> StartDetachedProcessAsync(string[] args)
5151
{
5252
var isJsonOutput = IsJsonOutputRequested(args);
5353

54-
// Check if an instance is already running
55-
if (await StateManager.IsInstanceRunningAsync())
54+
// Check if an instance is already running as system proxy
55+
var systemProxyInstance = await StateManager.FindSystemProxyInstanceAsync();
56+
if (systemProxyInstance is not null)
5657
{
57-
var existingState = await StateManager.LoadStateAsync();
5858
if (isJsonOutput)
5959
{
6060
await Console.Error.WriteLineAsync(
61-
FormatJsonLogEntry("error", $"Dev Proxy is already running (PID: {existingState?.Pid}). Use 'devproxy stop' to stop it first."));
61+
FormatJsonLogEntry("error", $"Dev Proxy is already running as system proxy (PID: {systemProxyInstance.Pid}). Use 'devproxy stop' to stop it first."));
6262
}
6363
else
6464
{
65-
await Console.Error.WriteLineAsync($"Dev Proxy is already running (PID: {existingState?.Pid}).");
65+
await Console.Error.WriteLineAsync($"Dev Proxy is already running as system proxy (PID: {systemProxyInstance.Pid}).");
6666
await Console.Error.WriteLineAsync("Use 'devproxy stop' to stop it first.");
6767
}
6868
return 1;
6969
}
7070

71+
// Check for port conflicts with existing instances
72+
var existingStates = await StateManager.LoadAllStatesAsync();
73+
if (existingStates.Count > 0)
74+
{
75+
var earlyOptions = new DevProxyConfigOptions();
76+
earlyOptions.ParseOptions(args);
77+
var requestedPort = earlyOptions.Port ?? 8000;
78+
var requestedApiPort = earlyOptions.ApiPort ?? 8897;
79+
80+
foreach (var existing in existingStates)
81+
{
82+
if (requestedPort != 0 && existing.Port == requestedPort)
83+
{
84+
await Console.Error.WriteLineAsync($"Port {requestedPort} is already in use by Dev Proxy instance (PID: {existing.Pid}).");
85+
await Console.Error.WriteLineAsync("Use a different --port or stop the existing instance first.");
86+
return 1;
87+
}
88+
89+
if (requestedApiPort != 0 && existing.ApiUrl.EndsWith($":{requestedApiPort}", StringComparison.Ordinal))
90+
{
91+
await Console.Error.WriteLineAsync($"API port {requestedApiPort} is already in use by Dev Proxy instance (PID: {existing.Pid}).");
92+
await Console.Error.WriteLineAsync("Use a different --api-port or stop the existing instance first.");
93+
return 1;
94+
}
95+
}
96+
}
97+
7198
// Clean up old log files
7299
StateManager.CleanupOldLogs();
73100

@@ -135,7 +162,7 @@ await Console.Error.WriteLineAsync(
135162
{
136163
await Task.Delay(200);
137164

138-
var state = await StateManager.LoadStateAsync();
165+
var state = await StateManager.LoadStateByPidAsync(process.Id);
139166
if (state != null)
140167
{
141168
if (isJsonOutput)
@@ -330,26 +357,6 @@ static async Task<int> RunProxyAsync(string[] args, DevProxyConfigOptions option
330357
var app = BuildApplication(options);
331358
try
332359
{
333-
// If running as daemon, save state so other commands can find us
334-
if (DevProxyCommand.IsInternalDaemon)
335-
{
336-
var ipAddress = options.IPAddress ?? app.Configuration.GetValue("ipAddress", "127.0.0.1") ?? "127.0.0.1";
337-
var apiPort = options.ApiPort ?? app.Configuration.GetValue("apiPort", 8897);
338-
var port = options.Port ?? app.Configuration.GetValue("port", 8000);
339-
340-
var state = new ProxyInstanceState
341-
{
342-
Pid = Environment.ProcessId,
343-
ApiUrl = $"http://{(ipAddress is "0.0.0.0" or "::" ? "127.0.0.1" : ipAddress)}:{apiPort}",
344-
LogFile = DevProxyCommand.DetachedLogFilePath,
345-
StartedAt = DateTimeOffset.UtcNow,
346-
ConfigFile = options.ConfigFile,
347-
Port = port
348-
};
349-
350-
await StateManager.SaveStateAsync(state);
351-
}
352-
353360
var devProxyCommand = app.Services.GetRequiredService<DevProxyCommand>();
354361
return await devProxyCommand.InvokeAsync(args, app);
355362
}

0 commit comments

Comments
 (0)