summary refs log tree commit diff
path: root/SystemdCtl.Client
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2024-01-20 08:34:32 +0100
committerRory& <root@rory.gay>2024-01-20 08:34:32 +0100
commit43e06f4b1b7ead9f8cc97fe547eb49d51f341486 (patch)
treeb700ba441320e0f3944c398080cadd296f03ef07 /SystemdCtl.Client
downloadSystemdCtl-43e06f4b1b7ead9f8cc97fe547eb49d51f341486.tar.xz
Initial commit
Diffstat (limited to 'SystemdCtl.Client')
-rw-r--r--SystemdCtl.Client/Abstractions/StreamingHttpClient.cs143
-rw-r--r--SystemdCtl.Client/Pages/ServiceManage.razor66
-rw-r--r--SystemdCtl.Client/Pages/Services.razor154
-rw-r--r--SystemdCtl.Client/Program.cs5
-rw-r--r--SystemdCtl.Client/SystemdCtl.Client.csproj21
-rw-r--r--SystemdCtl.Client/_Imports.razor10
-rw-r--r--SystemdCtl.Client/wwwroot/appsettings.Development.json8
-rw-r--r--SystemdCtl.Client/wwwroot/appsettings.json8
8 files changed, 415 insertions, 0 deletions
diff --git a/SystemdCtl.Client/Abstractions/StreamingHttpClient.cs b/SystemdCtl.Client/Abstractions/StreamingHttpClient.cs
new file mode 100644
index 0000000..ee39588
--- /dev/null
+++ b/SystemdCtl.Client/Abstractions/StreamingHttpClient.cs
@@ -0,0 +1,143 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Net.Http.Headers;
+using System.Reflection;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using ArcaneLibs.Extensions;
+using Microsoft.AspNetCore.Components.WebAssembly.Http;
+
+namespace SystemdCtl.Client.Abstractions;
+
+public class StreamingHttpClient : HttpClient
+{
+    public Dictionary<string, string> AdditionalQueryParameters { get; set; } = new();
+    internal string? AssertedUserId { get; set; }
+
+    private JsonSerializerOptions GetJsonSerializerOptions(JsonSerializerOptions? options = null)
+    {
+        options ??= new JsonSerializerOptions();
+        options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
+        return options;
+    }
+
+    public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+    {
+        if (request.RequestUri is null) throw new NullReferenceException("RequestUri is null");
+        if (!request.RequestUri.IsAbsoluteUri) request.RequestUri = new Uri(BaseAddress, request.RequestUri);
+        // if (AssertedUserId is not null) request.RequestUri = request.RequestUri.AddQuery("user_id", AssertedUserId);
+        foreach (var (key, value) in AdditionalQueryParameters)
+        {
+            request.RequestUri = request.RequestUri.AddQuery(key, value);
+        }
+
+        Console.WriteLine($"Sending request to {request.RequestUri}");
+
+        try
+        {
+            var webAssemblyEnableStreamingResponseKey =
+                new HttpRequestOptionsKey<bool>("WebAssemblyEnableStreamingResponse");
+            request.Options.Set(webAssemblyEnableStreamingResponseKey, true);
+        }
+        catch (Exception e)
+        {
+            Console.WriteLine("Failed to set browser response streaming:");
+            Console.WriteLine(e);
+        }
+        
+        HttpResponseMessage responseMessage;
+        // try {
+        responseMessage = await base.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
+        // }
+        // catch (Exception e) {
+        // if (requestSettings is { Retries: 0 }) throw;
+        // typeof(HttpRequestMessage).GetField("_sendStatus", BindingFlags.NonPublic | BindingFlags.Instance)
+        // ?.SetValue(request, 0);
+        // await Task.Delay(requestSettings?.RetryDelay ?? 2500, cancellationToken);
+        // if(requestSettings is not null) requestSettings.Retries--;
+        // return await SendAsync(request, cancellationToken);
+        // throw;
+        // }
+
+        if (responseMessage.IsSuccessStatusCode) return responseMessage;
+
+        //error handling
+        // var content = await responseMessage.Content.ReadAsStringAsync(cancellationToken);
+        // typeof(HttpRequestMessage).GetField("_sendStatus", BindingFlags.NonPublic | BindingFlags.Instance)
+        // ?.SetValue(request, 0);
+        // return await SendAsync(request, cancellationToken);
+        return responseMessage;
+    }
+
+    // GetAsync
+    public Task<HttpResponseMessage> GetAsync([StringSyntax("Uri")] string? requestUri, CancellationToken? cancellationToken = null)
+    {
+        return SendAsync(new HttpRequestMessage(HttpMethod.Get, requestUri), cancellationToken ?? CancellationToken.None);
+    }
+
+    // GetFromJsonAsync
+    public async Task<T> GetFromJsonAsync<T>(string requestUri, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default)
+    {
+        options = GetJsonSerializerOptions(options);
+        // Console.WriteLine($"GetFromJsonAsync called for {requestUri} with json options {options?.ToJson(ignoreNull:true)} and cancellation token {cancellationToken}");
+        var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
+        request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
+        var response = await SendAsync(request, cancellationToken);
+        response.EnsureSuccessStatusCode();
+        await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken);
+
+        return await JsonSerializer.DeserializeAsync<T>(responseStream, options, cancellationToken: cancellationToken) ??
+               throw new InvalidOperationException("Failed to deserialize response");
+    }
+
+    // GetStreamAsync
+    public new async Task<Stream> GetStreamAsync(string requestUri, CancellationToken cancellationToken = default)
+    {
+        var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
+        request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
+        var response = await SendAsync(request, cancellationToken);
+        response.EnsureSuccessStatusCode();
+        return await response.Content.ReadAsStreamAsync(cancellationToken);
+    }
+
+    public async Task<HttpResponseMessage> PutAsJsonAsync<T>([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, T value, JsonSerializerOptions? options = null,
+        CancellationToken cancellationToken = default) where T : notnull
+    {
+        options = GetJsonSerializerOptions(options);
+        var request = new HttpRequestMessage(HttpMethod.Put, requestUri);
+        request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
+        Console.WriteLine($"Sending PUT {requestUri}");
+        request.Content = new StringContent(JsonSerializer.Serialize(value, value.GetType(), options),
+            Encoding.UTF8, "application/json");
+        return await SendAsync(request, cancellationToken);
+    }
+
+    public async Task<HttpResponseMessage> PostAsJsonAsync<T>([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, T value, JsonSerializerOptions? options = null,
+        CancellationToken cancellationToken = default) where T : notnull
+    {
+        options ??= new();
+        options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
+        var request = new HttpRequestMessage(HttpMethod.Post, requestUri);
+        request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
+        request.Content = new StringContent(JsonSerializer.Serialize(value, value.GetType(), options),
+            Encoding.UTF8, "application/json");
+        return await SendAsync(request, cancellationToken);
+    }
+
+    public async IAsyncEnumerable<T?> GetAsyncEnumerableFromJsonAsync<T>([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, JsonSerializerOptions? options = null)
+    {
+        options = GetJsonSerializerOptions(options);
+        var res = await GetAsync(requestUri);
+        var result = JsonSerializer.DeserializeAsyncEnumerable<T>(await res.Content.ReadAsStreamAsync(), options);
+        await foreach (var resp in result)
+        {
+            yield return resp;
+        }
+    }
+
+    public IAsyncEnumerable<TValue?> GetFromJsonAsAsyncEnumerable<TValue>(
+        [StringSyntax("Uri")] string? requestUri,
+        CancellationToken cancellationToken = default(CancellationToken)) =>
+        GetAsyncEnumerableFromJsonAsync<TValue>(requestUri);
+}
\ No newline at end of file
diff --git a/SystemdCtl.Client/Pages/ServiceManage.razor b/SystemdCtl.Client/Pages/ServiceManage.razor
new file mode 100644
index 0000000..9a32087
--- /dev/null
+++ b/SystemdCtl.Client/Pages/ServiceManage.razor
@@ -0,0 +1,66 @@
+@page "/Service/{ServiceName}/Manage"

+@using LibSystemdCli.Models

+@using LibSystemdCli

+@using System.Text.RegularExpressions

+@using SystemdCtl.Client.Abstractions

+@* @attribute [StreamRendering] *@

+@rendermode InteractiveWebAssembly

+@inject NavigationManager NavigationManager

+

+

+<PageTitle>Manage @ServiceName</PageTitle>

+

+<h1>Manage @ServiceName</h1>

+

+@* //simple log view *@

+<div class="row">

+    <div class="col-12">

+        <h3>Logs</h3>

+        <div class="card">

+            <div class="card-body">

+                <pre>

+                    @foreach (var line in LogLines) {

+                        <span>@line</span><br/>

+                    }

+                </pre>

+            </div>

+        </div>

+    </div>

+</div>

+

+@code {

+

+    [Parameter]

+    public string ServiceName { get; set; } = "";

+

+    private static bool IsClient => !Environment.CommandLine.Contains("/");

+

+    private List<string> LogLines { get; set; } = new();

+

+    protected override async Task OnInitializedAsync() {

+        Console.WriteLine("OnInitializedAsync");

+        await Run();

+    }

+

+    private async Task Run() {

+        if (!IsClient) return;

+

+        LogLines.Clear();

+        var Http = new StreamingHttpClient() { BaseAddress = new Uri(NavigationManager.BaseUri) };

+        var _items = Http.GetAsyncEnumerableFromJsonAsync<string>($"/api/unit/{ServiceName}/logs");

+        await foreach (var item in _items) {

+            LogLines.Add(item);

+            if (LogLines.Count > 100) LogLines.RemoveAt(0);

+            StateHasChanged();

+        }

+    }

+

+    private string Capitalize(string input) {

+        return input switch {

+            null => throw new ArgumentNullException(nameof(input)),

+            "" => throw new ArgumentException($"{nameof(input)} cannot be empty", nameof(input)),

+            _ => input.First().ToString().ToUpper() + input[1..]

+        };

+    }

+

+}
\ No newline at end of file
diff --git a/SystemdCtl.Client/Pages/Services.razor b/SystemdCtl.Client/Pages/Services.razor
new file mode 100644
index 0000000..d0f67a7
--- /dev/null
+++ b/SystemdCtl.Client/Pages/Services.razor
@@ -0,0 +1,154 @@
+@page "/Services"

+@using LibSystemdCli.Models

+@using System.Text.RegularExpressions

+@using SystemdCtl.Client.Abstractions

+@using ArcaneLibs.Blazor.Components

+@* @attribute [StreamRendering] *@

+@rendermode InteractiveWebAssembly

+@inject NavigationManager NavigationManager

+

+

+<PageTitle>Services</PageTitle>

+

+<h1>Services</h1>

+

+<span>

+    <label>Type: </label>

+    <InputSelect @bind-Value="TypeFilter">

+        <option value="">All</option>

+        @foreach (var i in UnitTypes) {

+            <option value="@i">@Capitalize(i)</option>

+        }

+    </InputSelect>

+</span>

+<span>

+    <InputCheckbox @bind-Value="@ShowSystem"></InputCheckbox>

+    <label>Show system services</label>

+</span>

+

+@if (filteredItems is not { Count: > 0 }) {

+    <p>

+        <em>Loading...</em>

+    </p>

+}

+else {

+    <table class="table">

+        <thead>

+            <tr>

+                <th>Service</th>

+                <th>Description</th>

+                <th>Status</th>

+                <th></th>

+            </tr>

+        </thead>

+        <tbody>

+            @foreach (var unit in filteredItems) {

+                <tr>

+                    <td>@unit.Unit</td>

+                    <td>@unit.Description</td>

+                    <td>@unit.Active</td>

+                    <td><LinkButton href="@($"/Service/{unit.Unit}/Manage")">Manage</LinkButton></td>

+                </tr>

+                @foreach (var frag in unit.FragmentPaths) {

+                    <tr>

+                        <td/>

+                        <td>@frag</td>

+                        <td/>

+                    </tr>

+                }

+            }

+        </tbody>

+    </table>

+}

+

+@code {

+

+    private static Regex[] AlwaysHidden = new Regex[] {

+        //services

+        new Regex(@"^systemd-fsck@.*\.service$"),

+        new Regex(@"^modprobe@.*\.service$"),

+        new Regex(@"^xen.*\.service$"),

+        new Regex(@"^virt.*\.service$"),

+        new Regex(@"^libvirt.*\.service$"),

+        new Regex(@"^systemd-.*\.service$"),

+        //sockets

+        new Regex(@"^virt.*\.socket$"),

+        new Regex(@"^systemd.*\.socket$"),

+        new Regex(@"^libvirt.*\.socket$"),

+        //device

+        new(@"^dev-disk-by.*\.device$"),

+        new(@"^sys-device.*\.device$"),

+        new(@".*-by\\x2d.*\.device$"),

+        //mount

+        new(@"^run-credentials.*\.mount"),

+        //target

+        new(@"^blockdev@dev-disk-by.*\.target$")

+    };

+

+    private static bool IsClient => !Environment.CommandLine.Contains("/");

+    private List<SystemdUnitListItem>? items = new();

+    private List<SystemdUnitListItem>? filteredItems = new();

+    private List<string> UnitTypes = new();

+    private string _typeFilter = "";

+    private bool _showSystem;

+

+    public string TypeFilter {

+        get => _typeFilter;

+        set {

+            _typeFilter = value;

+            FilterItems();

+        }

+    }

+

+    public bool ShowSystem {

+        get => _showSystem;

+        set {

+            _showSystem = value;

+            FilterItems();

+        }

+    }

+

+    protected override async Task OnInitializedAsync() {

+        // await Task.Delay(500);

+

+        // items = await SystemdExecutor.GetUnits();

+        Console.WriteLine("OnInitializedAsync");

+        await ReloadItems();

+    }

+

+    private async Task ReloadItems() {

+        if (!IsClient) return;

+

+        items.Clear();

+        var Http = new StreamingHttpClient() { BaseAddress = new Uri(NavigationManager.BaseUri) };

+        var _items = Http.GetAsyncEnumerableFromJsonAsync<SystemdUnitListItem>("/api/listUnits");

+        await foreach (var item in _items) {

+            items.Add(item);

+            if (items.Count % 10 == 0)

+                await FilterItems();

+        }

+

+        await FilterItems();

+    }

+

+    private async Task FilterItems() {

+        var filter = items.Where(x => true);//!AlwaysHidden.Any(y => y.IsMatch(x.Unit)));

+        if (!_showSystem)

+            filter = filter.Where(x => !x.IsSystem);

+

+        UnitTypes = filter.Select(x => x.UnitType).Distinct().ToList();

+        filter = filter.Where(x => x.Unit.EndsWith(TypeFilter)).ToList();

+

+        filteredItems = filter.ToList();

+        StateHasChanged();

+    }

+

+    private string Capitalize(string input) {

+        return input switch {

+            null => throw new ArgumentNullException(nameof(input)),

+            "" => throw new ArgumentException($"{nameof(input)} cannot be empty", nameof(input)),

+            _ => input.First().ToString().ToUpper() + input[1..]

+        };

+    }

+

+}
\ No newline at end of file
diff --git a/SystemdCtl.Client/Program.cs b/SystemdCtl.Client/Program.cs
new file mode 100644
index 0000000..524c689
--- /dev/null
+++ b/SystemdCtl.Client/Program.cs
@@ -0,0 +1,5 @@
+using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

+

+var builder = WebAssemblyHostBuilder.CreateDefault(args);

+

+await builder.Build().RunAsync();

diff --git a/SystemdCtl.Client/SystemdCtl.Client.csproj b/SystemdCtl.Client/SystemdCtl.Client.csproj
new file mode 100644
index 0000000..e00a924
--- /dev/null
+++ b/SystemdCtl.Client/SystemdCtl.Client.csproj
@@ -0,0 +1,21 @@
+<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

+

+  <PropertyGroup>

+    <TargetFramework>net8.0</TargetFramework>

+    <ImplicitUsings>enable</ImplicitUsings>

+    <Nullable>enable</Nullable>

+    <NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>

+    <StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>

+  </PropertyGroup>

+

+  <ItemGroup>

+    <PackageReference Include="ArcaneLibs" Version="1.0.0-preview7485112379.f69bb51" />

+    <PackageReference Include="ArcaneLibs.Blazor.Components" Version="1.0.0-preview7485112379.f69bb51" />

+    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.0" />

+  </ItemGroup>

+

+  <ItemGroup>

+    <ProjectReference Include="..\LibSystemdCli.Models\LibSystemdCli.Models.csproj" />

+  </ItemGroup>

+

+</Project>

diff --git a/SystemdCtl.Client/_Imports.razor b/SystemdCtl.Client/_Imports.razor
new file mode 100644
index 0000000..3da583a
--- /dev/null
+++ b/SystemdCtl.Client/_Imports.razor
@@ -0,0 +1,10 @@
+@using System.Net.Http

+@using System.Net.Http.Json

+@using Microsoft.AspNetCore.Components.Forms

+@using Microsoft.AspNetCore.Components.Routing

+@using Microsoft.AspNetCore.Components.Web

+@using static Microsoft.AspNetCore.Components.Web.RenderMode

+@using Microsoft.AspNetCore.Components.Web.Virtualization

+@using Microsoft.JSInterop

+@using SystemdCtl.Client

+

diff --git a/SystemdCtl.Client/wwwroot/appsettings.Development.json b/SystemdCtl.Client/wwwroot/appsettings.Development.json
new file mode 100644
index 0000000..ff66ba6
--- /dev/null
+++ b/SystemdCtl.Client/wwwroot/appsettings.Development.json
@@ -0,0 +1,8 @@
+{

+  "Logging": {

+    "LogLevel": {

+      "Default": "Information",

+      "Microsoft.AspNetCore": "Warning"

+    }

+  }

+}

diff --git a/SystemdCtl.Client/wwwroot/appsettings.json b/SystemdCtl.Client/wwwroot/appsettings.json
new file mode 100644
index 0000000..ff66ba6
--- /dev/null
+++ b/SystemdCtl.Client/wwwroot/appsettings.json
@@ -0,0 +1,8 @@
+{

+  "Logging": {

+    "LogLevel": {

+      "Default": "Information",

+      "Microsoft.AspNetCore": "Warning"

+    }

+  }

+}