diff --git a/LibSystemdCli.Models/SystemdServiceData.cs b/LibSystemdCli.Models/SystemdServiceData.cs
new file mode 100644
index 0000000..1efcfd7
--- /dev/null
+++ b/LibSystemdCli.Models/SystemdServiceData.cs
@@ -0,0 +1,46 @@
+using System.Text.Json.Serialization;
+
+namespace LibSystemdCli.Models;
+
+public class SystemdServiceData {
+ [JsonPropertyName("data")]
+ public Dictionary<string, string> SingleData { get; set; } = new();
+
+ [JsonPropertyName("multiData")]
+ public Dictionary<string, List<string>> MultiData { get; set; } = new();
+
+ public bool IsRunning => Status is "active" or "reloading";
+ public string Status => GetSingleData("ActiveState") ?? "unknown";
+ public void AddData(string key, string value) {
+ if (MultiData.ContainsKey(key)) {
+ MultiData[key].Add(value);
+ }
+ else if (SingleData.ContainsKey(key)) {
+ MultiData[key] = new List<string> { SingleData[key], value };
+ SingleData.Remove(key);
+ }
+ else {
+ SingleData[key] = value;
+ }
+ }
+
+ public List<string>? GetData(string key) {
+ if (MultiData.TryGetValue(key, out var data)) {
+ return data;
+ }
+
+ if (SingleData.TryGetValue(key, out var value)) {
+ return new List<string> { value };
+ }
+
+ return null;
+ }
+
+ public string? GetSingleData(string key) {
+ if (SingleData.TryGetValue(key, out var value)) {
+ return value;
+ }
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/LibSystemdCli/SystemdExecutor.cs b/LibSystemdCli/SystemdExecutor.cs
index 06c9538..ead11fd 100644
--- a/LibSystemdCli/SystemdExecutor.cs
+++ b/LibSystemdCli/SystemdExecutor.cs
@@ -4,35 +4,46 @@ using LibSystemdCli.Models;
namespace LibSystemdCli;
-public class SystemdExecutor
-{
- public static async IAsyncEnumerable<SystemdUnitListItem> GetUnits()
- {
+public class SystemdExecutor {
+ public static async IAsyncEnumerable<SystemdUnitListItem> GetUnits() {
var output = await CommandExecutor.ExecuteCommand("systemctl", "list-units --all --no-legend --no-pager --no-legend -o json-pretty");
-
+
var data = JsonSerializer.Deserialize<List<SystemdUnitListItem>>(output);
-
- foreach (var unit in data)
- {
- try
- {
+
+ foreach (var unit in data) {
+ try {
var fragmentOutput = await CommandExecutor.ExecuteCommand("systemctl", $"show -P FragmentPath --no-pager --no-legend -- {unit.Unit} ");
// Console.WriteLine(fragmentOutput);
unit.FragmentPaths = fragmentOutput.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
}
- catch
- {
- }
+ catch { }
yield return unit;
// await Task.Delay(100);
}
}
- public static async IAsyncEnumerable<SystemdJournalLogItem> GetUnitLogs(string serviceName) {
- await foreach (var line in CommandExecutor.ExecuteCommandAsync("journalctl", $"--catalog --all --pager-end --follow --output=json --unit={serviceName}"))
- {
+ public static async IAsyncEnumerable<SystemdJournalLogItem> GetUnitLogs(string serviceName, int contextLines = 100) {
+ await foreach (var line in CommandExecutor.ExecuteCommandAsync("journalctl", $"--catalog --all --pager-end --output=json --follow --lines={contextLines} --unit={serviceName}")) {
yield return JsonSerializer.Deserialize<SystemdJournalLogItem>(line)!;
}
}
+
+ public static async Task<SystemdServiceData> GetUnitData(string serviceName) {
+ var output = await CommandExecutor.ExecuteCommand("systemctl", $"show --no-pager --no-legend -- {serviceName} ");
+ var data = new SystemdServiceData();
+ foreach (var line in output.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) {
+ var parts = line.Split('=', 2);
+ var key = parts[0];
+ var value = parts[1];
+ data.AddData(key, value);
+ }
+
+ return data;
+ }
+
+ private class TypeDef {
+ public bool IsList { get; set; }
+ public string Type { get; set; } = null!;
+ }
}
\ No newline at end of file
diff --git a/SystemdCtl.Client/Pages/ServiceManage.razor b/SystemdCtl.Client/Pages/ServiceManage.razor
index c622833..851f1b6 100644
--- a/SystemdCtl.Client/Pages/ServiceManage.razor
+++ b/SystemdCtl.Client/Pages/ServiceManage.razor
@@ -2,7 +2,10 @@
@using LibSystemdCli.Models
@using LibSystemdCli
@using System.Text.RegularExpressions
+@using ArcaneLibs.Extensions
@using SystemdCtl.Client.Abstractions
+@using System.Diagnostics.Metrics
+@using System.Text.Json
@* @attribute [StreamRendering] *@
@rendermode InteractiveWebAssembly
@inject NavigationManager NavigationManager
@@ -10,7 +13,26 @@
<PageTitle>Manage @ServiceName</PageTitle>
-<h1>Manage @ServiceName</h1>
+<h1>Managing @ServiceName (@ServiceInfo?.Status)</h1>
+
+<div class="row">
+ <div class="col-12">
+ <h3>Actions</h3>
+ <div class="card">
+ <div class="card-body">
+ @if (ServiceInfo.IsRunning) {
+ <LinkButton OnClick="@Stop">Stop</LinkButton>
+ <LinkButton OnClick="@Restart">Restart</LinkButton>
+ <LinkButton OnClick="@Kill">Kill</LinkButton>
+ }
+ else {
+ <LinkButton OnClick="@Start">Start</LinkButton>
+ }
+ </div>
+ </div>
+ </div>
+</div>
+
@* //simple log view *@
<div class="row">
@@ -19,9 +41,14 @@
<div class="card">
<div class="card-body">
<pre>
- @foreach (var line in LogLines) {
- <span>@line</span><br/>
- }
+ <table class="table table-sm table-striped">
+ @foreach (var line in LogLines) {
+ <tr>
+ <td>@line.Exe.Split('/').Last()</td>
+ <td>@line.Message</td>
+ </tr>
+ }
+ </table>
</pre>
</div>
</div>
@@ -36,6 +63,7 @@
private static bool IsClient => !Environment.CommandLine.Contains("/");
private List<SystemdJournalLogItem> LogLines { get; set; } = new();
+ private SystemdServiceData? ServiceInfo { get; set; } = new();
protected override async Task OnInitializedAsync() {
Console.WriteLine("OnInitializedAsync");
@@ -44,17 +72,36 @@
private async Task Run() {
if (!IsClient) return;
-
+ int history = 100;
LogLines.Clear();
+ GetServiceDataTask();
var Http = new StreamingHttpClient() { BaseAddress = new Uri(NavigationManager.BaseUri) };
- var _items = Http.GetAsyncEnumerableFromJsonAsync<SystemdJournalLogItem>($"/api/unit/{ServiceName}/logs");
+ var jso = new JsonSerializerOptions() {
+ DefaultBufferSize = 1,
+ AllowTrailingCommas = true
+ };
+ var _items = Http.GetAsyncEnumerableFromJsonAsync<SystemdJournalLogItem>($"/api/unit/{ServiceName}/logs?contextLines={history}", jso);
await foreach (var item in _items) {
+ // LogLines.RemoveAll(x=>x.SystemdInvocationId != item.SystemdInvocationId);
LogLines.Add(item);
- if (LogLines.Count > 100) LogLines.RemoveAt(0);
+ if (LogLines.Count > history) {
+ LogLines.RemoveAt(0);
+ }
+
StateHasChanged();
}
}
+ private async Task GetServiceDataTask() {
+ if (!IsClient) return;
+ var Http = new StreamingHttpClient() { BaseAddress = new Uri(NavigationManager.BaseUri) };
+ while (true) {
+ ServiceInfo = await Http.GetFromJsonAsync<SystemdServiceData>($"/api/unit/{ServiceName}/data");
+ StateHasChanged();
+ await Task.Delay(TimeSpan.FromSeconds(5));
+ }
+ }
+
private string Capitalize(string input) {
return input switch {
null => throw new ArgumentNullException(nameof(input)),
@@ -63,4 +110,24 @@
};
}
+ private async Task Start() {
+ var Http = new StreamingHttpClient() { BaseAddress = new Uri(NavigationManager.BaseUri) };
+ await Http.GetAsync($"/api/unit/{ServiceName}/start", null);
+ }
+
+ private async Task Stop() {
+ var Http = new StreamingHttpClient() { BaseAddress = new Uri(NavigationManager.BaseUri) };
+ await Http.GetAsync($"/api/unit/{ServiceName}/stop", null);
+ }
+
+ private async Task Restart() {
+ var Http = new StreamingHttpClient() { BaseAddress = new Uri(NavigationManager.BaseUri) };
+ await Http.GetAsync($"/api/unit/{ServiceName}/restart", null);
+ }
+
+ private async Task Kill() {
+ var Http = new StreamingHttpClient() { BaseAddress = new Uri(NavigationManager.BaseUri) };
+ await Http.GetAsync($"/api/unit/{ServiceName}/kill", null);
+ }
+
}
\ No newline at end of file
diff --git a/SystemdCtl.Client/Pages/Services.razor b/SystemdCtl.Client/Pages/Services.razor
index d0f67a7..f2e8bb9 100644
--- a/SystemdCtl.Client/Pages/Services.razor
+++ b/SystemdCtl.Client/Pages/Services.razor
@@ -2,7 +2,6 @@
@using LibSystemdCli.Models
@using System.Text.RegularExpressions
@using SystemdCtl.Client.Abstractions
-@using ArcaneLibs.Blazor.Components
@* @attribute [StreamRendering] *@
@rendermode InteractiveWebAssembly
@inject NavigationManager NavigationManager
diff --git a/SystemdCtl.Client/_Imports.razor b/SystemdCtl.Client/_Imports.razor
index 3da583a..71d1c80 100644
--- a/SystemdCtl.Client/_Imports.razor
+++ b/SystemdCtl.Client/_Imports.razor
@@ -7,4 +7,4 @@
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using SystemdCtl.Client
-
+@using ArcaneLibs.Blazor.Components
diff --git a/SystemdCtl/Controllers/UnitController.cs b/SystemdCtl/Controllers/UnitController.cs
index 00916bf..a128584 100644
--- a/SystemdCtl/Controllers/UnitController.cs
+++ b/SystemdCtl/Controllers/UnitController.cs
@@ -18,12 +18,34 @@ public class UnitController : ControllerBase
}
[HttpGet("unit/{serviceName}/logs")]
- public async IAsyncEnumerable<SystemdJournalLogItem> GetUnitLogs(string serviceName)
+ public async IAsyncEnumerable<SystemdJournalLogItem> GetUnitLogs(string serviceName, [FromQuery] int contextLines = 100)
{
- await foreach (var log in SystemdExecutor.GetUnitLogs(serviceName))
+ await foreach (var log in SystemdExecutor.GetUnitLogs(serviceName, contextLines: contextLines))
{
+ Console.WriteLine(log.Message);
yield return log;
await Response.Body.FlushAsync();
}
}
+
+ [HttpGet("unit/{serviceName}/data")]
+ public Task<SystemdServiceData> GetUnitData(string serviceName) => SystemdExecutor.GetUnitData(serviceName);
+
+ [HttpGet("unit/{serviceName}/start")]
+ public Task StartUnit(string serviceName) => CommandExecutor.ExecuteCommand("systemctl", $"start {serviceName}");
+
+ [HttpGet("unit/{serviceName}/stop")]
+ public Task StopUnit(string serviceName) => CommandExecutor.ExecuteCommand("systemctl", $"stop {serviceName}");
+
+ [HttpGet("unit/{serviceName}/restart")]
+ public Task RestartUnit(string serviceName) => CommandExecutor.ExecuteCommand("systemctl", $"restart {serviceName}");
+
+ [HttpGet("unit/{serviceName}/reload")]
+ public Task ReloadUnit(string serviceName) => CommandExecutor.ExecuteCommand("systemctl", $"reload {serviceName}");
+
+ [HttpGet("unit/{serviceName}/enable")]
+ public Task EnableUnit(string serviceName) => CommandExecutor.ExecuteCommand("systemctl", $"enable {serviceName}");
+
+ [HttpGet("unit/{serviceName}/disable")]
+ public Task DisableUnit(string serviceName) => CommandExecutor.ExecuteCommand("systemctl", $"disable {serviceName}");
}
\ No newline at end of file
diff --git a/SystemdCtl/Program.cs b/SystemdCtl/Program.cs
index c399d31..f0f01c3 100644
--- a/SystemdCtl/Program.cs
+++ b/SystemdCtl/Program.cs
@@ -10,7 +10,8 @@ builder.Services.AddRazorComponents()
builder.Services.AddControllers().AddJsonOptions(options =>
{
options.JsonSerializerOptions.WriteIndented = true;
- options.JsonSerializerOptions.DefaultBufferSize = 128;
+ options.JsonSerializerOptions.DefaultBufferSize = 1;
+ options.JsonSerializerOptions.AllowTrailingCommas = true;
});
var app = builder.Build();
|