Skip to content
Merged
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
4 changes: 4 additions & 0 deletions src/AzLocal.Cli/AzLocal.Cli.csproj
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<ItemGroup>
<PackageReference Include="System.CommandLine" Version="2.0.8" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\AzLocal.Core\AzLocal.Core.csproj" />
</ItemGroup>
Expand Down
36 changes: 35 additions & 1 deletion src/AzLocal.Cli/CertificateHelper.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,39 @@
namespace AzLocal.Cli;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace AzLocal.Cli;

public static class CertificateHelper
{
public static bool TrustDevCert()
{
try
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
Run("dotnet", "dev-certs https -ep /usr/local/share/ca-certificates/azlocal-dev.crt --format PEM --no-password");
Run("sudo", "update-ca-certificates");
}
else
{
// Works on Windows and macOS
Run("dotnet", "dev-certs https --trust");
}
return true;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Certificate error: {ex.Message}");
return false;
}
}

private static void Run(string exe, string args)
{
var psi = new ProcessStartInfo(exe, args) { UseShellExecute = false };
using var proc = Process.Start(psi) ?? throw new Exception($"Could not start '{exe}'.");
proc.WaitForExit();
if (proc.ExitCode != 0)
throw new Exception($"'{exe}' exited with code {proc.ExitCode}.");
}
}
48 changes: 46 additions & 2 deletions src/AzLocal.Cli/Commands/ResetCommand.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,49 @@
namespace AzLocal.Cli.Commands;
using System.CommandLine;
using System.Diagnostics;

public class ResetCommand
namespace AzLocal.Cli.Commands;

public static class ResetCommand
{
public static Command Build()
{
var cmd = new Command("reset", "Stop the AzLocal host and wipe all local state");

cmd.SetAction((ParseResult _) =>
{
var azlocalDir = Path.Combine(Path.GetTempPath(), "azlocal");
var pidFile = Path.Combine(azlocalDir, "host.pid");

// Stop any running host
if (File.Exists(pidFile))
{
var raw = File.ReadAllText(pidFile).Trim();
if (int.TryParse(raw, out var pid))
{
try
{
var proc = Process.GetProcessById(pid);
proc.CloseMainWindow();
if (!proc.WaitForExit(3000))
proc.Kill();
Console.WriteLine($"Stopped running host (pid {pid}).");
}
catch (ArgumentException) { /* already gone */ }
}
}

// Wipe all state under the azlocal temp directory
if (Directory.Exists(azlocalDir))
{
Directory.Delete(azlocalDir, recursive: true);
Console.WriteLine("State cleared.");
}
else
{
Console.WriteLine("Nothing to reset.");
}
});

return cmd;
}
}
87 changes: 85 additions & 2 deletions src/AzLocal.Cli/Commands/StartCommand.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,88 @@
namespace AzLocal.Cli.Commands;
using AzLocal.Core;
using System.CommandLine;
using System.Diagnostics;
using System.Runtime.InteropServices;

public class StartCommand
namespace AzLocal.Cli.Commands;

public static class StartCommand
{
public static Command Build()
{
var portOption = new Option<int>("--port")
{
Description = "Port to listen on",
DefaultValueFactory = _ => EmulatorDefaults.DefaultPort,
};

var cmd = new Command("start", "Start the AzLocal host") { portOption };

cmd.SetAction((ParseResult pr) =>
{
var port = pr.GetValue(portOption);

if (port is < 1 or > 65535)
{
Console.Error.WriteLine($"Invalid port: {port}. Must be 1-65535.");
return;
}

var pidDir = Path.Combine(Path.GetTempPath(), "azlocal");
var pidFile = Path.Combine(pidDir, "host.pid");

if (File.Exists(pidFile) && int.TryParse(File.ReadAllText(pidFile).Trim(), out var existing))
{
try
{
Process.GetProcessById(existing);
Console.Error.WriteLine($"AzLocal is already running (pid {existing}). Run 'stop' first.");
return;
}
catch (ArgumentException) { /* stale pid file */ }
}

Directory.CreateDirectory(pidDir);

var proc = Process.Start(BuildHostProcessInfo(port));

if (proc is null)
{
Console.Error.WriteLine("Failed to start host process.");
return;
}

File.WriteAllText(pidFile, proc.Id.ToString());
Console.WriteLine($"AzLocal started on http://localhost:{port} (pid {proc.Id})");
});

return cmd;
}

private static ProcessStartInfo BuildHostProcessInfo(int port)
{
// When published, AzLocal.Host lives next to this binary.
// Fall back to 'dotnet run' for local development.
var hostExe = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? Path.Combine(AppContext.BaseDirectory, "AzLocal.Host.exe")
: Path.Combine(AppContext.BaseDirectory, "AzLocal.Host");

if (File.Exists(hostExe))
{
return new ProcessStartInfo
{
FileName = hostExe,
Arguments = $"--urls http://localhost:{port}",
UseShellExecute = false,
CreateNoWindow = true,
};
}

return new ProcessStartInfo
{
FileName = "dotnet",
Arguments = $"run --project src/AzLocal.Host --urls http://localhost:{port}",
UseShellExecute = false,
CreateNoWindow = true,
};
}
}
35 changes: 33 additions & 2 deletions src/AzLocal.Cli/Commands/StatusCommand.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
namespace AzLocal.Cli.Commands;
using AzLocal.Core;
using System.CommandLine;

public class StatusCommand
namespace AzLocal.Cli.Commands;

public static class StatusCommand
{
public static Command Build()
{
var portOption = new Option<int>("--port")
{
Description = "Port to check",
DefaultValueFactory = _ => EmulatorDefaults.DefaultPort,
};

var cmd = new Command("status", "Check if AzLocal is running") { portOption };

cmd.SetAction(async (ParseResult pr, CancellationToken ct) =>
{
var port = pr.GetValue(portOption);

using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(3) };
try
{
var response = await http.GetAsync($"http://localhost:{port}/", ct);
Console.WriteLine($"RUNNING on port {port} (HTTP {(int)response.StatusCode})");
}
catch
{
Console.WriteLine($"NOT running on port {port}");
}
});

return cmd;
}
}
51 changes: 49 additions & 2 deletions src/AzLocal.Cli/Commands/StopCommand.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,52 @@
namespace AzLocal.Cli.Commands;
using System.CommandLine;
using System.Diagnostics;

public class StopCommand
namespace AzLocal.Cli.Commands;

public static class StopCommand
{
public static Command Build()
{
var cmd = new Command("stop", "Stop the AzLocal host");

cmd.SetAction((ParseResult _) =>
{
var pidFile = Path.Combine(Path.GetTempPath(), "azlocal", "host.pid");

if (!File.Exists(pidFile))
{
Console.WriteLine("No running host found.");
return;
}

var raw = File.ReadAllText(pidFile).Trim();

if (!int.TryParse(raw, out var pid))
{
Console.Error.WriteLine($"PID file is corrupt ('{raw}'). Removing it.");
File.Delete(pidFile);
return;
}

try
{
var proc = Process.GetProcessById(pid);
proc.CloseMainWindow(); // graceful first
if (!proc.WaitForExit(3000))
proc.Kill();

Console.WriteLine($"Stopped (pid {pid})");
}
catch (ArgumentException)
{
Console.WriteLine("Process already gone.");
}
finally
{
File.Delete(pidFile);
}
});

return cmd;
}
}
22 changes: 20 additions & 2 deletions src/AzLocal.Cli/Commands/TrustCertCommand.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
namespace AzLocal.Cli.Commands;
using System.CommandLine;

public class TrustCertCommand
namespace AzLocal.Cli.Commands;

public static class TrustCertCommand
{
public static Command Build()
{
var cmd = new Command("trust-cert", "Trust the AzLocal HTTPS developer certificate");

cmd.SetAction((ParseResult _) =>
{
Console.WriteLine("Trusting developer certificate...");

if (CertificateHelper.TrustDevCert())
Console.WriteLine("Certificate trusted successfully.");
else
Console.Error.WriteLine("Failed to trust certificate. Try running as administrator/sudo.");
});

return cmd;
}
}
56 changes: 54 additions & 2 deletions src/AzLocal.Cli/Commands/WaitCommand.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,57 @@
namespace AzLocal.Cli.Commands;
using AzLocal.Core;
using System.CommandLine;

public class WaitCommand
namespace AzLocal.Cli.Commands;

public static class WaitCommand
{
public static Command Build()
{
var portOption = new Option<int>("--port")
{
Description = "Port to poll",
DefaultValueFactory = _ => EmulatorDefaults.DefaultPort,
};

var timeoutOption = new Option<int>("--timeout")
{
Description = "Seconds to wait before giving up",
DefaultValueFactory = _ => 30,
};

var cmd = new Command("wait", "Wait until AzLocal host is ready") { portOption, timeoutOption };

cmd.SetAction(async (ParseResult pr, CancellationToken ct) =>
{
var port = pr.GetValue(portOption);
var timeoutSecs = pr.GetValue(timeoutOption);
var deadline = DateTime.UtcNow.AddSeconds(timeoutSecs);

using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(2) };

Console.Write($"Waiting for AzLocal on port {port}");

while (DateTime.UtcNow < deadline)
{
try
{
var response = await http.GetAsync($"http://localhost:{port}/", ct);
if (response.IsSuccessStatusCode)
{
Console.WriteLine(" ready.");
return;
}
}
catch { /* not up yet */ }

Console.Write(".");
await Task.Delay(1000, ct);
}

Console.WriteLine();
Console.Error.WriteLine($"Timed out after {timeoutSecs}s — host did not respond on port {port}.");
});

return cmd;
}
}
15 changes: 12 additions & 3 deletions src/AzLocal.Cli/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");
using AzLocal.Cli.Commands;
using System.CommandLine;

Console.WriteLine("Hey welcome to this package where big thing are need to happen for ust o ");
var rootCommand = new RootCommand("AzLocal — local Azure emulator CLI");

rootCommand.Add(StartCommand.Build());
rootCommand.Add(StopCommand.Build());
rootCommand.Add(StatusCommand.Build());
rootCommand.Add(WaitCommand.Build());
rootCommand.Add(ResetCommand.Build());
rootCommand.Add(TrustCertCommand.Build());

return await rootCommand.Parse(args).InvokeAsync();
1 change: 1 addition & 0 deletions src/AzLocal.Core/EmulatorDefaults.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace AzLocal.Core;
public static class EmulatorDefaults
{
public const string FakeToken = "fake-azlocal-token";
public const int DefaultPort = 4566;
public const string BaseUrl = "http://localhost:4566";
public const string ObjectId = "00000000-0000-0000-0000-000000000001";
public const string TenantId = "00000000-0000-0000-0000-000000000002";
Expand Down
Loading