Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions DevProxy.Abstractions/Proxy/ISystemProxyManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ public interface ISystemProxyManager
void Enable(string? ipAddress, int port);

/// <summary>
/// Removes Dev Proxy as the system proxy, restoring direct connections. Safe to call
/// even if the proxy was never enabled (idempotent best-effort cleanup).
/// Removes Dev Proxy as the system proxy, restoring the settings that were in place
/// before <see cref="Enable(string?, int)"/> when they were recorded, or direct
/// connections otherwise. Safe to call even if the proxy was never enabled
/// (idempotent best-effort cleanup).
/// </summary>
void Disable();
}
140 changes: 140 additions & 0 deletions DevProxy.Tests/SystemProxyManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,143 @@ public void CreateToggleScriptStartInfo_PreservesPathAndArgumentsContainingSpace
startInfo.ArgumentList);
}
}

public class MacOSNetworkSetupTests
{
[Fact]
public void ParseNetworkServices_SkipsHeaderBlankAndDisabledServices()
{
var output = "An asterisk (*) denotes that a network service is disabled.\nWi-Fi\n*Thunderbolt Bridge\nUSB 10/100/1000 LAN\n\n";

var services = MacOSNetworkSetup.ParseNetworkServices(output);

Assert.Equal(["Wi-Fi", "USB 10/100/1000 LAN"], services);
}

[Fact]
public void ParseProxyEndpoint_ReadsEnabledServerAndPort()
{
var output = "Enabled: Yes\nServer: proxy.contoso.com\nPort: 3128\nAuthenticated Proxy Enabled: 0\n";

var endpoint = MacOSNetworkSetup.ParseProxyEndpoint(output);

Assert.True(endpoint.Enabled);
Assert.Equal("proxy.contoso.com", endpoint.Server);
Assert.Equal(3128, endpoint.Port);
}

[Fact]
public void ParseProxyEndpoint_EmptyServer()
{
var endpoint = MacOSNetworkSetup.ParseProxyEndpoint("Enabled: No\nServer: \nPort: 0\n");

Assert.False(endpoint.Enabled);
Assert.Equal(string.Empty, endpoint.Server);
Assert.Equal(0, endpoint.Port);
}

[Fact]
public void GetRestoreCommands_RestoresServerAndTurnsDisabledEndpointsOff()
{
var settings = new MacOSServiceProxySettings
{
Service = "Wi-Fi",
Web = new() { Enabled = true, Server = "proxy.contoso.com", Port = 3128 },
SecureWeb = new() { Enabled = false, Server = "secure.contoso.com", Port = 3129 },
};

var commands = MacOSNetworkSetup.GetRestoreCommands(settings);

Assert.Equal(3, commands.Count);
Assert.Equal(["-setwebproxy", "Wi-Fi", "proxy.contoso.com", "3128"], commands[0]);
Assert.Equal(["-setsecurewebproxy", "Wi-Fi", "secure.contoso.com", "3129"], commands[1]);
Assert.Equal(["-setsecurewebproxystate", "Wi-Fi", "off"], commands[2]);
}

[Fact]
public void GetRestoreCommands_NoPreviousServer_ReturnsNoCommands()
{
var settings = new MacOSServiceProxySettings
{
Service = "Wi-Fi",
Web = new() { Enabled = false, Server = string.Empty, Port = 0 },
SecureWeb = null,
};

Assert.Empty(MacOSNetworkSetup.GetRestoreCommands(settings));
}
}

public class SystemProxySnapshotStoreTests
{
[Fact]
public void SaveLoadDelete_RoundTripsSnapshot()
{
var folder = Directory.CreateTempSubdirectory("devproxy-snapshot-");
try
{
var path = Path.Combine(folder.FullName, "system-proxy-backup.json");
var snapshot = new SystemProxySnapshot
{
Pid = 1234,
CreatedAt = DateTimeOffset.UnixEpoch,
Windows = new() { ProxyEnable = 1, ProxyServer = "corp:8080" },
MacOS = [new() { Service = "Wi-Fi", Web = new() { Enabled = true, Server = "corp", Port = 8080 } }],
};

SystemProxySnapshotStore.Save(path, snapshot);
var loaded = SystemProxySnapshotStore.Load(path);

Assert.NotNull(loaded);
Assert.Equal(1234, loaded.Pid);
Assert.Equal(1, loaded.Windows?.ProxyEnable);
Assert.Equal("corp:8080", loaded.Windows?.ProxyServer);
var service = Assert.Single(loaded.MacOS!);
Assert.Equal("Wi-Fi", service.Service);
Assert.Equal(8080, service.Web?.Port);

SystemProxySnapshotStore.Delete(path);
Assert.Null(SystemProxySnapshotStore.Load(path));
}
finally
{
folder.Delete(recursive: true);
}
}

[Fact]
public void Load_CorruptFile_ReturnsNull()
{
var path = Path.GetTempFileName();
try
{
File.WriteAllText(path, "{ not json");
Assert.Null(SystemProxySnapshotStore.Load(path));
}
finally
{
File.Delete(path);
}
}
}

public class SystemProxyResetDetectionTests
{
[Theory]
[InlineData("system-proxy", "reset")]
[InlineData("system-proxy", "reset", "--force")]
[InlineData("--output=json", "system-proxy", "reset")]
[InlineData("system-proxy", "--no-color", "reset")]
[InlineData("--log-level", "debug", "system-proxy", "reset", "--bogus")]
public void IsResetInvocation_ReturnsTrue(params string[] args) =>
Assert.True(DevProxy.Commands.SystemProxyCommand.IsResetInvocation(args));

[Theory]
[InlineData("system-proxy")]
[InlineData("reset")]
[InlineData("stop")]
[InlineData("system-proxy", "reset", "--force", "--help")]
[InlineData]
public void IsResetInvocation_ReturnsFalse(params string[] args) =>
Assert.False(DevProxy.Commands.SystemProxyCommand.IsResetInvocation(args));
}
19 changes: 19 additions & 0 deletions DevProxy/Commands/DevProxyCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ sealed class DevProxyCommand : RootCommand
private static bool _isJwtCommandResolved;
private static bool _isRootCommandResolved;
private static bool _isConfigValidateCommandResolved;
private static bool _isSystemProxyResetCommandResolved;
private static bool _isDetachedModeResolved;
private static bool _isInternalDaemonResolved;
private static bool _stdioLogFilePathResolved;
Expand Down Expand Up @@ -132,6 +133,23 @@ public static bool IsConfigValidateCommand
}
}

public static bool IsSystemProxyResetCommand
{
get
{
if (_isSystemProxyResetCommandResolved)
{
return field;
}

var args = Environment.GetCommandLineArgs();
field = SystemProxyCommand.IsResetInvocation(args.Skip(1).ToArray()) &&
!args.Any(arg => helpOptions.Contains(arg));
_isSystemProxyResetCommandResolved = true;
return field;
}
}

public static bool IsDetachedMode
{
get
Expand Down Expand Up @@ -636,6 +654,7 @@ private void ConfigureCommand(DevProxyConfigOptions configOptions)
ActivatorUtilities.CreateInstance<StdioCommand>(_serviceProvider),
ActivatorUtilities.CreateInstance<StatusCommand>(_serviceProvider),
ActivatorUtilities.CreateInstance<StopCommand>(_serviceProvider),
ActivatorUtilities.CreateInstance<SystemProxyCommand>(_serviceProvider),
Comment thread
waldekmastykarz marked this conversation as resolved.
Resolved
ActivatorUtilities.CreateInstance<LogsCommand>(_serviceProvider)
};
var pluginCommands = _plugins.SelectMany(p => p.GetCommands());
Expand Down
33 changes: 24 additions & 9 deletions DevProxy/Commands/StopCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ private async Task<int> RunAsync(ParseResult parseResult, CancellationToken canc
// Reconcile any system-proxy registrations left behind by crashed
// instances first — this must run before LoadAllStatesAsync, which prunes
// (deletes) the stale state files it depends on.
var reconciliation = await SystemProxyManager.ReconcileOrphanedSystemProxiesAsync(cancellationToken);
var reconciliation = await SystemProxyManager.ReconcileOrphanedSystemProxiesAsync(cancellationToken: cancellationToken);

var states = await StateManager.LoadAllStatesAsync(cancellationToken);
if (states.Count == 0 && reconciliation.Orphans.Count == 0)
if (states.Count == 0 && reconciliation.OrphanedPids.Count == 0)
{
Console.WriteLine("Dev Proxy is not running.");
return 1;
Expand All @@ -61,7 +61,10 @@ private async Task<int> RunAsync(ParseResult parseResult, CancellationToken canc
}
}

ReportReconciledOrphans(reconciliation);
if (!ReportReconciledOrphans(reconciliation))
{
exitCode = 1;
}

return exitCode;
}
Expand All @@ -84,7 +87,12 @@ private static async Task<int> StopByPidAsync(int pid, bool force, CancellationT
var liveOwner = await StateManager.FindSystemProxyInstanceAsync(cancellationToken);
if (liveOwner is null)
{
new SystemProxyManager(NullLogger<SystemProxyManager>.Instance).Disable();
if (new SystemProxyManager(NullLogger<SystemProxyManager>.Instance).Restore() is SystemProxyRestoreResult.Failed)
{
// Keep the state record so a later stop or start retries.
await Console.Error.WriteLineAsync($"Couldn't restore the system proxy left by crashed Dev Proxy (PID: {pid}). Run 'devproxy system-proxy reset' or see https://go.devproxy.net?go=proxy-reset.");
return 1;
}
Console.WriteLine($"Restored system proxy left by crashed Dev Proxy (PID: {pid}).");
}
else
Expand All @@ -100,14 +108,21 @@ private static async Task<int> StopByPidAsync(int pid, bool force, CancellationT
return 1;
}

private static void ReportReconciledOrphans(SystemProxyManager.OrphanReconciliation reconciliation)
private static bool ReportReconciledOrphans(SystemProxyManager.OrphanReconciliation reconciliation)
{
foreach (var orphan in reconciliation.Orphans)
if (reconciliation.Outcome is SystemProxyManager.OrphanRestoreOutcome.Failed)
{
Console.Error.WriteLine($"Couldn't restore the system proxy left by crashed Dev Proxy (PID: {string.Join(", ", reconciliation.OrphanedPids)}). Run 'devproxy system-proxy reset' or see https://go.devproxy.net?go=proxy-reset.");
return false;
}

foreach (var pid in reconciliation.OrphanedPids)
{
Console.WriteLine(reconciliation.SystemProxyDisabled
? $"Restored system proxy left by crashed Dev Proxy (PID: {orphan.Pid})."
: $"Removed stale record for crashed Dev Proxy (PID: {orphan.Pid}); system proxy is owned by a running instance.");
Console.WriteLine(reconciliation.Outcome is SystemProxyManager.OrphanRestoreOutcome.Restored
? $"Restored system proxy left by crashed Dev Proxy (PID: {pid})."
: $"Removed stale record for crashed Dev Proxy (PID: {pid}); system proxy is owned by a running instance.");
}
return true;
}

private static async Task<int> StopInstanceAsync(ProxyInstanceState state, bool force, CancellationToken cancellationToken)
Expand Down
Loading
Loading