diff --git a/src/paradigm-ehb.CommandCenter.Core/Factories/AgentClientFactory.cs b/src/paradigm-ehb.CommandCenter.Core/Factories/AgentClientFactory.cs
index b191440..9b2ea81 100644
--- a/src/paradigm-ehb.CommandCenter.Core/Factories/AgentClientFactory.cs
+++ b/src/paradigm-ehb.CommandCenter.Core/Factories/AgentClientFactory.cs
@@ -10,8 +10,8 @@
using paradigm_ehb.CommandCenter.Core.Interfaces;
using paradigm_ehb.CommandCenter.Core.Models;
using paradigm_ehb.CommandCenter.Core.Services;
-using Resources.V1;
-using Services.V2;
+using Resources.V2;
+using Services.V3;
namespace paradigm_ehb.CommandCenter.Core.Factories
{
diff --git a/src/paradigm-ehb.CommandCenter.Core/Models/AgentClient.cs b/src/paradigm-ehb.CommandCenter.Core/Models/AgentClient.cs
index 4c5fe1d..11dec64 100644
--- a/src/paradigm-ehb.CommandCenter.Core/Models/AgentClient.cs
+++ b/src/paradigm-ehb.CommandCenter.Core/Models/AgentClient.cs
@@ -3,8 +3,8 @@
using System;
using paradigm_ehb.CommandCenter.Core.Services;
using Journal.V1;
-using Services.V2;
-using Resources.V1;
+using Services.V3;
+using Resources.V2;
namespace paradigm_ehb.CommandCenter.Core.Models
{
diff --git a/src/paradigm-ehb.CommandCenter.Core/Protos/Resources/V2/resources.proto b/src/paradigm-ehb.CommandCenter.Core/Protos/Resources/V2/resources.proto
new file mode 100644
index 0000000..8508ae9
--- /dev/null
+++ b/src/paradigm-ehb.CommandCenter.Core/Protos/Resources/V2/resources.proto
@@ -0,0 +1,121 @@
+syntax = "proto3";
+
+package resources.v2;
+
+
+service ResourcesService {
+ rpc GetSystemResources(GetSystemResourcesRequest)
+ returns (GetSystemResourcesResponse);
+ rpc ProcessAction(ProcessActionRequest)
+ returns (ProcessActionReply);
+}
+
+message GetSystemResourcesRequest {}
+
+message GetSystemResourcesResponse {
+ SystemResources resources = 1;
+}
+
+/**
+ *
+ * Get a complete system snapshots
+ * @return Cpu, Memory, Device, Disk ( partitions ) and processes
+ *
+ * */
+message SystemResources {
+ Cpu cpu = 1;
+ Memory memory = 2;
+ Device device = 3;
+ repeated Disk disks = 4;
+ repeated Process processes = 5;
+}
+
+/**
+ * Cpu information
+ *
+ * @return vendor, model, frequency, cores
+ * */
+message Cpu {
+ string vendor = 1;
+ string model = 2;
+ string frequency = 3;
+ uint32 max_core = 4;
+ uint64 total_time = 5;
+ uint64 idle_time = 6;
+}
+
+/**
+ * Memory
+ * @return total memory on system, available memory on system
+ * */
+message Memory {
+ string total = 1;
+ string free = 2;
+}
+
+/**
+ * Device specific information
+ * @return os version ( distro information ), and uptime in string format
+ *
+ * */
+
+message Device {
+ string os_version = 1;
+ string uptime = 2;
+}
+
+/**
+ *
+ * Disk information
+ * @disk a list of all available partitions
+ *
+ * */
+
+message Disk {
+ repeated DiskPartition partitions = 1;
+}
+
+/**
+ * Partition
+ * @return partition name, major, minor, blocks
+ * */
+message DiskPartition {
+ string name = 1;
+ uint32 major = 2;
+ uint32 minor = 3;
+ uint64 blocks = 4;
+}
+
+enum ProcessState {
+ PROCESS_STATE_UNSPECIFIED = 0;
+ PROCESS_STATE_RUNNING = 1;
+ PROCESS_STATE_SLEEPING = 2;
+ PROCESS_STATE_DISK_SLEEPING = 3;
+ PROCESS_STATE_STOPPED = 4;
+ PROCESS_STATE_TRACING_STOPPED = 5;
+ PROCESS_STATE_ZOMBIE = 6;
+ PROCESS_STATE_DEAD = 7;
+ PROCESS_STATE_IDLE = 8;
+ PROCESS_STATE_UNDEFINED = 9;
+}
+
+/**
+ * Process information
+ * @return pid ( process identifier ), process name, the state, todo :)
+ * */
+message Process {
+ int32 pid = 1;
+ string name = 2;
+ ProcessState state = 3;
+ uint64 utime = 4;
+ uint32 num_threads = 5;
+}
+
+message ProcessActionRequest {
+ int32 pid = 1;
+ int32 signal = 2;
+}
+
+message ProcessActionReply {
+ bool succes = 1;
+}
\ No newline at end of file
diff --git a/src/paradigm-ehb.CommandCenter.Core/Protos/Services/V3/services.proto b/src/paradigm-ehb.CommandCenter.Core/Protos/Services/V3/services.proto
new file mode 100644
index 0000000..e4822af
--- /dev/null
+++ b/src/paradigm-ehb.CommandCenter.Core/Protos/Services/V3/services.proto
@@ -0,0 +1,122 @@
+syntax = "proto3";
+
+package services.v3;
+
+
+service HandlerService {
+ rpc PerformUnitAction (UnitActionRequest) returns (UnitActionReply);
+ rpc PerformUnitFileAction (UnitFileActionRequest) returns (UnitFileActionReply);
+ rpc GetAllUnits (GetUnitsRequest) returns (GetUnitsReply);
+ rpc GetLoadedUnits (GetUnitsRequest) returns (GetUnitsReply);
+ rpc GetFilteredUnits (GetUnitsFilteredRequest) returns (GetUnitsReply);
+ rpc GetUnitStatus (GetUnitStatusRequest) returns (GetUnitStatusReply);
+}
+
+
+message UnitActionRequest {
+ string unit_name = 1;
+
+ enum UnitAction {
+ UNIT_ACTION_UNSPECIFIED = 0;
+ UNIT_ACTION_START = 1;
+ UNIT_ACTION_STOP = 2;
+ UNIT_ACTION_RESTART = 3;
+ }
+
+ UnitAction action = 2;
+ bool force = 3;
+}
+
+message UnitActionReply {
+ bytes status = 1;
+ bool success = 2;
+ string error_message = 3;
+}
+
+
+message UnitFileActionRequest {
+ string unit_name = 1;
+
+ enum UnitFileAction {
+ UNIT_FILE_ACTION_UNSPECIFIED = 0;
+ UNIT_FILE_ACTION_ENABLE = 1;
+ UNIT_FILE_ACTION_DISABLE = 2;
+ }
+
+ UnitFileAction action = 2;
+ bool runtime = 3;
+ bool force = 4;
+}
+
+message UnitFileActionReply {
+ bytes status = 1;
+ bool success = 2;
+ string error_message = 3;
+}
+
+
+message LoadedUnit {
+ string name = 1;
+ string description = 2;
+ string load_state = 3;
+ string sub_state = 4;
+ string active_state = 5;
+ string dep_unit = 6;
+
+ string object_path = 7;
+
+ uint32 queued_job = 8;
+ string job_type = 9;
+ string job_path = 10;
+}
+
+message Unit {
+ string name = 1;
+ string state = 2;
+}
+
+
+message GetUnitsRequest {
+
+ enum UnitState {
+ UNIT_STATE_UNSPECIFIED = 0;
+ ENABLED = 1;
+ DISABLED = 2;
+ }
+
+ string name = 1;
+ UnitState state = 2;
+}
+
+message GetUnitsFilteredRequest {
+
+ enum UnitFileState {
+
+ UNIT_FILE_STATE_UNSPECIFIED = 0;
+ LOADED = 1;
+ NOT_FOUND = 2;
+ BAD_SETTING = 3;
+ ERROR = 4;
+ MASKED = 5;
+ }
+
+ repeated UnitFileState filters = 1;
+
+}
+
+message GetUnitsReply {
+ repeated LoadedUnit units = 1;
+ bool success = 2;
+ string error_message = 3;
+}
+
+
+message GetUnitStatusRequest {
+ string unit_name = 1;
+}
+
+message GetUnitStatusReply {
+ string state = 1;
+ bool success = 2;
+ string error_message = 3;
+}
\ No newline at end of file
diff --git a/src/paradigm-ehb.CommandCenter.Core/paradigm-ehb.CommandCenter.Core.csproj b/src/paradigm-ehb.CommandCenter.Core/paradigm-ehb.CommandCenter.Core.csproj
index 9c527a5..243b060 100644
--- a/src/paradigm-ehb.CommandCenter.Core/paradigm-ehb.CommandCenter.Core.csproj
+++ b/src/paradigm-ehb.CommandCenter.Core/paradigm-ehb.CommandCenter.Core.csproj
@@ -26,8 +26,10 @@
+
+
diff --git a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/ServerMainPage.xaml b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/ServerMainPage.xaml
index f18a832..cb5fb04 100644
--- a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/ServerMainPage.xaml
+++ b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/ServerMainPage.xaml
@@ -42,14 +42,14 @@
-
+
+ Text="Processes" Icon="ViewAll"/>
diff --git a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ProcessesPage.xaml b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ProcessesPage.xaml
index 1ed8159..cbc3ec6 100644
--- a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ProcessesPage.xaml
+++ b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ProcessesPage.xaml
@@ -9,64 +9,85 @@
mc:Ignorable="d"
NavigationCacheMode="Enabled">
-
+
-
+
+
+ Default
+ Name
+ Process ID
+ Uptime
+ State
+
-
+
+ Padding="12,6"
+ Style="{StaticResource AccentButtonStyle}"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
-
-
-
-
-
-
-
-
-
-
+
-
-
+
+
+
+
+
@@ -74,25 +95,30 @@
-
+
-
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ProcessesPage.xaml.cs b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ProcessesPage.xaml.cs
index e610781..90c43b4 100644
--- a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ProcessesPage.xaml.cs
+++ b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ProcessesPage.xaml.cs
@@ -1,5 +1,4 @@
using Grpc.Core;
-using Resources.V1;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
@@ -16,10 +15,13 @@
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
+using System.Threading;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.ApplicationModel.VoiceCommands;
+using Resources.V2;
+using System.Runtime.InteropServices.ObjectiveC;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
@@ -33,95 +35,458 @@ public sealed partial class ProcessesPage : Page
{
AgentClient? client = null;
- public ObservableCollection processes { get; } = new();
+ public ObservableCollection processes { get; }
+
+ private Collection allProcesses { get; }
+
+ // track initialization to avoid re-running when page is cached
+ private bool _initialized = false;
+ private Guid? _lastEndpointId;
+
+ // Cancellation support for initialization
+ private CancellationTokenSource? _initCts;
public ProcessesPage()
{
+ // enable page caching so SelectorBar can reuse cached pages
+ this.NavigationCacheMode = NavigationCacheMode.Enabled;
+
+ processes = new ObservableCollection();
+ allProcesses = new Collection();
+
InitializeComponent();
}
+
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
- // Fire-and-forget; exceptions observed inside the task
- _ = InitializeAsync(e);
+ // Expect an AgentEndpoint parameter
+ if (e.Parameter is not AgentEndpoint endpoint)
+ {
+ _ = ShowErrorInfoBarAsync("Invalid navigation parameter; expected AgentEndpoint.");
+ return;
+ }
+
+ bool shouldInit = !_initialized || _lastEndpointId != endpoint.Id;
+
+ // Cancel any previous initialization
+ _initCts?.Cancel();
+ _initCts?.Dispose();
+ _initCts = new CancellationTokenSource();
+ CancellationToken ct = _initCts.Token;
+
+ // Resolve client from registry (parent is expected to have created/registered it)
+ _ = ResolveClientAndMaybeInitAsync(endpoint, shouldInit, ct);
+ }
+
+ protected override void OnNavigatedFrom(NavigationEventArgs e)
+ {
+ base.OnNavigatedFrom(e);
+
+ // Cancel any in-progress initialization when leaving the page
+ _initCts?.Cancel();
+ _initCts?.Dispose();
+ _initCts = null;
+ }
+
+ private async Task ResolveClientAndMaybeInitAsync(AgentEndpoint endpoint, bool shouldInit, CancellationToken ct)
+ {
+ try
+ {
+ var registry = App.Services.GetRequiredService();
+ client = await registry.GetAsync(endpoint.Id).ConfigureAwait(false);
+
+ if (shouldInit)
+ {
+ await InitializeAsync(endpoint, ct).ConfigureAwait(false);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // ignore
+ }
+ catch (Exception ex)
+ {
+ await ShowErrorInfoBarAsync($"Failed to resolve AgentClient: {ex.Message}");
+ }
}
private void OnFilterChanged(object sender, RoutedEventArgs args)
{
+ IEnumerable filtered = allProcesses.Where(process => Filter(process));
+ Remove_NonMatching(filtered);
+ AddBack_Processes(filtered);
+ Order_services();
+ }
+ private bool Filter(ProcessInfo process)
+ {
+ if (string.IsNullOrWhiteSpace(FilterByName.Text))
+ {
+ return true;
+ }
+ return process.ProcessName.Contains(FilterByName.Text, StringComparison.OrdinalIgnoreCase)
+ || process.ProcessId.ToString().Contains(FilterByName.Text, StringComparison.OrdinalIgnoreCase);
}
- private void RefreshButton_Click(object sender, RoutedEventArgs e)
+ private void Remove_NonMatching(IEnumerable filteredData)
{
+ for (int i = processes.Count - 1; i >= 0; i--)
+ {
+ ProcessInfo process = processes[i];
+ if (!filteredData.Contains(process))
+ processes.Remove(process);
+ }
+
}
- private void ServiceStartMenuItem_Click(object sender, RoutedEventArgs e)
+ private void AddBack_Processes(IEnumerable filteredData)
{
+ foreach (ProcessInfo process in filteredData)
+ {
+ if (!processes.Contains(process))
+ processes.Add(process);
+ }
}
- private void ServiceStopMenuItem_Click(object sender, RoutedEventArgs e)
+ private async void RefreshButton_Click(object sender, RoutedEventArgs e)
{
+ await ClearAllProcesses();
+ // Refresh is user-initiated; use non-cancellable token
+ await LoadAllProcesses(CancellationToken.None);
}
- private void ServiceRestartMenuItem_Click(object sender, RoutedEventArgs e)
+ private async void ProcessTerminate_Click(object sender, RoutedEventArgs e)
{
+ MenuFlyoutItem? menuFlyoutItem = sender as MenuFlyoutItem;
+ ProcessInfo? processInfo = menuFlyoutItem?.DataContext as ProcessInfo;
+
+ if (processInfo is null)
+ {
+ await ShowErrorInfoBarAsync("No process selected to terminate.");
+ return;
+ }
+
+ ContentDialog dialog = new()
+ {
+ XamlRoot = this.XamlRoot,
+ Title = $"Are you sure you want to Terminate {processInfo.ProcessName}?",
+ CloseButtonText = "Cancel",
+ PrimaryButtonText = "Terminate",
+ };
+
+ ContentDialogResult result = await dialog.ShowAsync();
+
+ if (result == ContentDialogResult.Primary)
+ {
+ try
+ {
+ ProcessActionReply terminateResult = await client!.Resources.ProcessActionAsync(new ProcessActionRequest
+ {
+ Pid = processInfo.ProcessId,
+ Signal = 15 // SIGTERM
+ });
+ if (terminateResult.Succes)
+ {
+ await ShowInfoBarAsync("Process Terminated", $"Process {processInfo.ProcessName} (PID {processInfo.ProcessId}) was terminated successfully.", InfoBarSeverity.Success);
+ await UpdateProcessVisualStateAsync(processInfo, "killed");
+ }
+ else
+ {
+ await ShowErrorInfoBarAsync($"Failed to terminate process {processInfo.ProcessName} (PID {processInfo.ProcessId})!");
+ }
+ }
+ catch (Exception ex)
+ {
+ await ShowErrorInfoBarAsync($"Error terminating process: {ex.Message}");
+ }
+ }
+ else
+ {
+ // Cancel, do nothing
+ }
}
- private void ServiceViewMenuItem_Click(object sender, RoutedEventArgs e)
+ private async void ProcessKill_Click(object sender, RoutedEventArgs e)
{
+ MenuFlyoutItem? menuFlyoutItem = sender as MenuFlyoutItem;
+ ProcessInfo? processInfo = menuFlyoutItem?.DataContext as ProcessInfo;
+
+ if (processInfo is null)
+ {
+ await ShowErrorInfoBarAsync("No process selected to kill.");
+ return;
+ }
+
+ ContentDialog dialog = new()
+ {
+ XamlRoot = this.XamlRoot,
+ Title = $"Are you sure you want to Kill {processInfo.ProcessName}?",
+ CloseButtonText = "Cancel",
+ PrimaryButtonText = "Kill",
+ };
+
+ ContentDialogResult result = await dialog.ShowAsync();
+
+ if (result == ContentDialogResult.Primary)
+ {
+ try
+ {
+ ProcessActionReply killResult = await client!.Resources.ProcessActionAsync(new ProcessActionRequest
+ {
+ Pid = processInfo.ProcessId,
+ Signal = 9 // SIGKILL
+ });
+
+ if (killResult.Succes)
+ {
+ await ShowInfoBarAsync("Process Killed", $"Process {processInfo.ProcessName} (PID {processInfo.ProcessId}) was killed successfully.", InfoBarSeverity.Success);
+ await UpdateProcessVisualStateAsync(processInfo, "killed");
+ }
+ else
+ {
+ await ShowErrorInfoBarAsync($"Failed to kill process {processInfo.ProcessName} (PID {processInfo.ProcessId})!");
+ }
+
+ }
+ catch (Exception ex)
+ {
+ await ShowErrorInfoBarAsync($"Error killing process: {ex.Message}");
+ }
+ }
+ else
+ {
+ // Cancel, do nothing
+ }
}
- private async Task InitializeAsync(NavigationEventArgs e)
+ private async void ProcessReload_Click(object sender, RoutedEventArgs e)
{
- try
+ MenuFlyoutItem? menuFlyoutItem = sender as MenuFlyoutItem;
+ ProcessInfo? processInfo = menuFlyoutItem?.DataContext as ProcessInfo;
+
+ if (processInfo is null)
+ {
+ await ShowErrorInfoBarAsync("No process selected to restart.");
+ return;
+ }
+
+ ContentDialog dialog = new()
{
- // Determine the AgentClient to use based on navigation parameter
+ XamlRoot = this.XamlRoot,
+ Title = $"Are you sure you want to Restart {processInfo.ProcessName}?",
+ CloseButtonText = "Cancel",
+ PrimaryButtonText = "Restart",
+ };
- if (e.Parameter is AgentClient passedClient)
+ ContentDialogResult result = await dialog.ShowAsync();
+
+ if (result == ContentDialogResult.Primary)
+ {
+ try
{
- client = passedClient;
+ ProcessActionReply restartResult = await client!.Resources.ProcessActionAsync(new ProcessActionRequest
+ {
+ Pid = processInfo.ProcessId,
+ Signal = 1
+ });
+ if (restartResult.Succes)
+ {
+ await ShowInfoBarAsync("Process Restarted", $"Process {processInfo.ProcessName} (PID {processInfo.ProcessId}) was restarted successfully.", InfoBarSeverity.Success);
+ await UpdateProcessVisualStateAsync(processInfo, "restarted");
+ }
+ else
+ {
+ await ShowErrorInfoBarAsync($"Failed to restart process {processInfo.ProcessName} (PID {processInfo.ProcessId})!");
+ }
}
- else if (e.Parameter is AgentEndpoint endpoint)
+ catch (Exception ex)
{
- // Try to obtain an existing registered client only.
- IAgentClientRegistry clientRegistry = App.Services.GetRequiredService();
- client = await clientRegistry.GetAsync(endpoint.Id).ConfigureAwait(false);
+ await ShowErrorInfoBarAsync($"Error restarting process: {ex.Message}");
}
+ }
+ else
+ {
+ // Cancel, do nothing
+ }
+ }
- if (client is null || client.Service is null)
+ private async void ProcessPause_Click(object sender, RoutedEventArgs args)
+ {
+ MenuFlyoutItem? menuFlyoutItem = sender as MenuFlyoutItem;
+ ProcessInfo? processInfo = menuFlyoutItem?.DataContext as ProcessInfo;
+ if (processInfo is null)
+ {
+ await ShowErrorInfoBarAsync("No process selected to pause.");
+ return;
+ }
+ ContentDialog dialog = new()
+ {
+ XamlRoot = this.XamlRoot,
+ Title = $"Are you sure you want to Pause {processInfo.ProcessName}?",
+ CloseButtonText = "Cancel",
+ PrimaryButtonText = "Pause",
+ };
+ ContentDialogResult result = await dialog.ShowAsync();
+ if (result == ContentDialogResult.Primary)
+ {
+ try
{
- await DispatcherQueue.EnqueueAsync(() =>
+ ProcessActionReply pauseResult = await client!.Resources.ProcessActionAsync(new ProcessActionRequest
{
- processes.Clear();
- processes.Add(new ProcessInfo
- {
- ProcessId = 0,
- ProcessName = "(no client)",
- State = ProcessState.Unspecified,
- Uptime = 0,
- NumThreads = 0
- });
- }).ConfigureAwait(false);
- return;
+ Pid = processInfo.ProcessId,
+ Signal = 19 // SIGSTOP
+ });
+ if (pauseResult.Succes)
+ {
+ await ShowInfoBarAsync("Process Paused", $"Process {processInfo.ProcessName} (PID {processInfo.ProcessId}) was paused successfully.", InfoBarSeverity.Success);
+ await UpdateProcessVisualStateAsync(processInfo, "paused");
+ }
+ else
+ {
+ await ShowErrorInfoBarAsync($"Failed to pause process {processInfo.ProcessName} (PID {processInfo.ProcessId})!");
+ }
+ }
+ catch (Exception ex)
+ {
+ await ShowErrorInfoBarAsync($"Error pausing process: {ex.Message}");
}
+ }
+ else
+ {
+ // Cancel, do nothing
+ }
+ }
- GetSystemResourcesResponse response = await client.Resources.GetSystemResourcesAsync(new GetSystemResourcesRequest());
+ private async void ProcessResume_Click(object sender, RoutedEventArgs args)
+ {
+ MenuFlyoutItem? menuFlyoutItem = sender as MenuFlyoutItem;
+ ProcessInfo? processInfo = menuFlyoutItem?.DataContext as ProcessInfo;
+ if (processInfo is null)
+ {
+ await ShowErrorInfoBarAsync("No process selected to resume.");
+ return;
+ }
+ ContentDialog dialog = new()
+ {
+ XamlRoot = this.XamlRoot,
+ Title = $"Are you sure you want to Resume {processInfo.ProcessName}?",
+ CloseButtonText = "Cancel",
+ PrimaryButtonText = "Resume",
+ };
+ ContentDialogResult result = await dialog.ShowAsync();
+ if (result == ContentDialogResult.Primary)
+ {
+ try
+ {
+ ProcessActionReply resumeResult = await client!.Resources.ProcessActionAsync(new ProcessActionRequest
+ {
+ Pid = processInfo.ProcessId,
+ Signal = 18 // SIGCONT
+ });
+ if (resumeResult.Succes)
+ {
+ await ShowInfoBarAsync("Process Resumed", $"Process {processInfo.ProcessName} (PID {processInfo.ProcessId}) was resumed successfully.", InfoBarSeverity.Success);
+ await UpdateProcessVisualStateAsync(processInfo, "resumed");
+ }
+ else
+ {
+ await ShowErrorInfoBarAsync($"Failed to resume process {processInfo.ProcessName} (PID {processInfo.ProcessId})!");
+ }
+ }
+ catch (Exception ex)
+ {
+ await ShowErrorInfoBarAsync($"Error resuming process: {ex.Message}");
+ }
+ }
+ else
+ {
+ // Cancel, do nothing
+ }
+ }
- foreach (Process process in response.Resources.Processes)
+ private async void ProcessCall_Click(object sender, RoutedEventArgs args)
+ {
+ MenuFlyoutItem? menuFlyoutItem = sender as MenuFlyoutItem;
+ ProcessInfo? processInfo = menuFlyoutItem?.DataContext as ProcessInfo;
+ if (processInfo is null)
+ {
+ await ShowErrorInfoBarAsync("No process selected to call.");
+ return;
+ }
+ ContentDialog dialog = new()
+ {
+ XamlRoot = this.XamlRoot,
+ Title = $"Are you sure you want to Call {processInfo.ProcessName}?",
+ CloseButtonText = "Cancel",
+ PrimaryButtonText = "Call",
+ };
+ ContentDialogResult result = await dialog.ShowAsync();
+ if (result == ContentDialogResult.Primary)
+ {
+ try
{
- await DispatcherQueue.EnqueueAsync(() =>
+ ProcessActionReply callResult = await client!.Resources.ProcessActionAsync(new ProcessActionRequest
+ {
+ Pid = processInfo.ProcessId,
+ Signal = 10 // SIGUSR1
+ });
+ if (callResult.Succes)
+ {
+ await ShowInfoBarAsync("Process Called", $"Process {processInfo.ProcessName} (PID {processInfo.ProcessId}) was called successfully.", InfoBarSeverity.Success);
+ }
+ else
{
- processes.Add(new ProcessInfo
- {
- ProcessId = (int)process.Pid,
- ProcessName = process.Name,
- State = process.State,
- Uptime = process.Utime,
- NumThreads = (int)process.NumThreads
- });
- }).ConfigureAwait(false);
+ await ShowErrorInfoBarAsync($"Failed to call process {processInfo.ProcessName} (PID {processInfo.ProcessId})!");
+ }
}
+ catch (Exception ex)
+ {
+ await ShowErrorInfoBarAsync($"Error calling process: {ex.Message}");
+ }
+ }
+ else
+ {
+ // Cancel, do nothing
+ }
+ }
+
+ private async Task InitializeAsync(AgentEndpoint endpoint, CancellationToken ct)
+ {
+ try
+ {
+ await DispatcherQueue.EnqueueAsync(() => LoadingProgressRing.IsActive = true);
+
+ // Clear previous state
+ await ClearAllProcesses();
+
+ // If client isn't resolved yet, try to get it now (parent should have registered it)
+ if (client is null)
+ {
+ IAgentClientRegistry registry = App.Services.GetRequiredService();
+ client = await registry.GetAsync(endpoint.Id).ConfigureAwait(false);
+ }
+
+ if (client is null)
+ {
+ await ShowErrorInfoBarAsync("No AgentClient available for this server.");
+ return;
+ }
+
+ ct.ThrowIfCancellationRequested();
+
+ // Load processes using resolved client
+ await LoadAllProcesses(ct);
+
+ // remember endpoint id when initialization successful
+ _lastEndpointId = endpoint.Id;
+
+ _initialized = true;
+ }
+ catch (OperationCanceledException)
+ {
+ // initialization was cancelled
}
catch (Exception ex)
{
@@ -138,12 +503,196 @@ await DispatcherQueue.EnqueueAsync(() =>
NumThreads = 0
});
}).ConfigureAwait(false);
+
+ await ShowErrorInfoBarAsync($"Initialization failed: {ex.Message}");
+ }
+ finally
+ {
+ await DispatcherQueue.EnqueueAsync(() => LoadingProgressRing.IsActive = false);
+ }
+ }
+
+ private async Task ClearAllProcesses()
+ {
+ await DispatcherQueue.EnqueueAsync(() =>
+ {
+ allProcesses.Clear();
+ processes.Clear();
+ });
+ }
+
+ private async Task LoadAllProcesses(CancellationToken ct)
+ {
+ if (client is null || client.Service is null)
+ {
+ await ShowErrorInfoBarAsync("No valid AgentClient available.");
+ return;
+ }
+
+ ct.ThrowIfCancellationRequested();
+
+ GetSystemResourcesResponse? response;
+ try
+ {
+ response = await client.Resources.GetSystemResourcesAsync(request: new GetSystemResourcesRequest(), cancellationToken: ct).ConfigureAwait(false);
+ }
+ catch (RpcException rpcEx) when (rpcEx.StatusCode == StatusCode.Cancelled || ct.IsCancellationRequested)
+ {
+ // treated as cancellation
+ return;
+ }
+ catch (Exception ex)
+ {
+ await ShowErrorInfoBarAsync($"Failed to load processes: {ex.Message}");
+ return;
}
+
+ if (response is null)
+ {
+ throw new InvalidOperationException("Received null response from ActionAsync");
+ }
+
+ foreach (Process? process in response.Resources.Processes)
+ {
+ await DispatcherQueue.EnqueueAsync(() =>
+ {
+ SolidColorBrush brush = process.State switch
+ {
+ ProcessState.Unspecified => (SolidColorBrush)Application.Current.Resources["SystemFillColorNeutralBrush"],
+ ProcessState.Running => (SolidColorBrush)Application.Current.Resources["SystemFillColorAttentionBrush"],
+ ProcessState.Sleeping => (SolidColorBrush)Application.Current.Resources["SystemFillColorCautionBrush"],
+ ProcessState.Stopped => (SolidColorBrush)Application.Current.Resources["SystemFillColorCriticalBrush"],
+ _ => (SolidColorBrush)Application.Current.Resources["SystemFillColorCriticalBackgroundBrush"],
+ };
+ ProcessInfo processInfo = new()
+ {
+ Fill = brush,
+ ProcessId = process.Pid,
+ ProcessName = process.Name,
+ State = process.State,
+ Uptime = process.Utime,
+ NumThreads = (int)process.NumThreads
+ };
+ allProcesses.Add(processInfo);
+ processes.Add(processInfo);
+ });
+ }
+ await DispatcherQueue.EnqueueAsync(() =>
+ {
+ Order_services();
+ });
+ }
+
+ public void OnOrderChanged(object sender, SelectionChangedEventArgs args)
+ {
+ Order_services();
+ }
+
+ private void Order_services()
+ {
+ string order = OrderByCombo?.SelectedValue as string ?? "State";
+
+ List ordered = order switch
+ {
+ "Name" => processes.OrderBy(p => p.ProcessName).ThenBy(p => p.ProcessId).ToList(),
+ "Pid" => processes.OrderBy(p => p.ProcessId).ToList(),
+ "Uptime" => processes.OrderByDescending(p => p.Uptime).ToList(),
+ "State" => processes.OrderBy(p => p.State).ThenBy(p => p.ProcessName).ToList(),
+ _ => processes
+ .OrderBy(p =>
+ {
+ if (p.State.Equals(ProcessState.Running))
+ return 0;
+ if (p.State.Equals(ProcessState.Sleeping))
+ return 1;
+ return 2;
+ })
+ .ThenBy(p => p.State)
+ .ThenBy(p => p.ProcessName)
+ .ToList()
+ };
+
+ processes.Clear();
+ foreach (ProcessInfo process in ordered)
+ {
+ processes.Add(process);
+ }
+ }
+ private async Task UpdateProcessVisualStateAsync(ProcessInfo processInfo, string action)
+ {
+ await DispatcherQueue.EnqueueAsync(() =>
+ {
+ ProcessState state = action switch
+ {
+ "started" => ProcessState.Running,
+ "killed" => ProcessState.Stopped,
+ "restarted" => ProcessState.Running,
+ _ => processInfo.State
+ };
+
+ SolidColorBrush brush = action switch
+ {
+ "started" => (SolidColorBrush)Application.Current.Resources["SystemFillColorAttentionBrush"],
+ "killed" => (SolidColorBrush)Application.Current.Resources["SystemFillColorCriticalBrush"],
+ "restarted" => (SolidColorBrush)Application.Current.Resources["SystemFillColorCautionBrush"],
+ _ => processInfo.Fill
+ };
+
+ processInfo.Fill = brush;
+ processInfo.State = state;
+ });
+ }
+
+ private async Task ShowInfoBarAsync(string title, string message, InfoBarSeverity severity)
+ {
+ // Ensure UI thread
+ await DispatcherQueue.EnqueueAsync(() =>
+ {
+ if (GridRoot == null)
+ {
+ // fallback to page XamlRoot; but InfoBar must be in visual tree to be visible
+ // if GridRoot is not available the InfoBar won't be shown persistently.
+ return;
+ }
+
+ InfoBar infoBar = new()
+ {
+ Title = title,
+ Message = message,
+ Severity = severity,
+ IsOpen = true,
+ VerticalAlignment = VerticalAlignment.Top,
+ Margin = new Thickness(0, 0, 0, 0)
+ };
+
+ // Add to visual tree so it is visible
+ GridRoot.Children.Add(infoBar);
+
+ // Remove from visual tree when closed
+ void OnClosed(object? s, InfoBarClosedEventArgs args)
+ {
+ infoBar.Closed -= OnClosed;
+ if (GridRoot.Children.Contains(infoBar))
+ {
+ GridRoot.Children.Remove(infoBar);
+ }
+ }
+
+ infoBar.Closed += OnClosed;
+ });
+ }
+
+ private async Task ShowErrorInfoBarAsync(string message)
+ {
+ await ShowInfoBarAsync("Service Action Result", $"Error: {message}", InfoBarSeverity.Error);
}
}
public class ProcessInfo
{
+ // Fill
+ public SolidColorBrush Fill { get; set; } = new SolidColorBrush(Microsoft.UI.Colors.Transparent);
+
// PID
public int ProcessId { get; set; }
diff --git a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml
index 2e401d9..7dbffea 100644
--- a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml
+++ b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml
@@ -81,15 +81,15 @@
+ Style="{ThemeResource BodyLargeTextBlockStyle}"
+ Margin="12,8,0,0"/>
+ Margin="12,0,0,8"/>
diff --git a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml.cs b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml.cs
index d78cd8d..bfc727a 100644
--- a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml.cs
+++ b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml.cs
@@ -9,8 +9,8 @@
using Microsoft.Windows.AppNotifications.Builder;
using paradigm_ehb.CommandCenter.Core.Interfaces;
using paradigm_ehb.CommandCenter.Core.Models;
-using Resources.V1;
-using Services.V2;
+using Resources.V2;
+using Services.V3;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@@ -32,7 +32,6 @@ public sealed partial class ServicesPage : Page
private Collection allServices { get; }
- // Add fields
private bool _initialized;
private Guid? _lastEndpointId;
private System.Threading.CancellationTokenSource? _initCts;
diff --git a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/srvOverview.xaml.cs b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/srvOverview.xaml.cs
index b0b4507..eab91a0 100644
--- a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/srvOverview.xaml.cs
+++ b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/srvOverview.xaml.cs
@@ -4,7 +4,7 @@
using Microsoft.UI.Xaml.Controls;
using paradigm_ehb.CommandCenter.Core.Interfaces;
using paradigm_ehb.CommandCenter.Core.Models;
-using Resources.V1;
+using Resources.V2;
using System;
using System.Linq;
using System.Timers;