diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 3839048..8439b28 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -5,7 +5,7 @@
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle
-name: Java CI with Gradle
+name: Edge builds
on:
workflow_dispatch:
@@ -23,44 +23,18 @@ jobs:
steps:
- uses: actions/checkout@v4
- - uses: actions/setup-dotnet@v4
- with:
- dotnet-version: '8.0.200'
- - name: Set up JDK 21
+
+ - name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: '22'
distribution: 'corretto'
- # Configure Gradle for optimal use in GitHub Actions, including caching of downloaded dependencies.
- # See: https://github.com/gradle/actions/blob/main/setup-gradle/README.md
- - name: Setup Gradle
- uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0
-
- name: Build with Gradle Wrapper
- run: ./gradlew :target:desktop:createDistributable
+ run: cd app; ./gradlew :target:desktop:createDistributable
- name: Archive production artifacts
uses: actions/upload-artifact@v4
with:
name: cleanmeter
- path: target\desktop\build\compose\binaries\main\app
-
- dependency-submission:
-
- runs-on: ubuntu-latest
- permissions:
- contents: write
-
- steps:
- - uses: actions/checkout@v4
- - name: Set up JDK 22
- uses: actions/setup-java@v4
- with:
- java-version: '22'
- distribution: 'corretto'
-
- # Generates and submits a dependency graph, enabling Dependabot Alerts for all project dependencies.
- # See: https://github.com/gradle/actions/blob/main/dependency-submission/README.md
- - name: Generate and submit dependency graph
- uses: gradle/actions/dependency-submission@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0
+ path: app\target\desktop\build\compose\binaries\main\app
diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
new file mode 100644
index 0000000..ee7c2fc
--- /dev/null
+++ b/.github/workflows/nightly.yml
@@ -0,0 +1,110 @@
+name: Nightly Release
+
+on:
+ schedule:
+ # Run every night at 2 AM UTC
+ - cron: '0 2 * * *'
+ workflow_dispatch:
+
+jobs:
+ check-changes:
+ name: Check for changes since last release
+ runs-on: ubuntu-latest
+ outputs:
+ has-changes: ${{ steps.check.outputs.has-changes }}
+ new-tag: ${{ steps.tag.outputs.new-tag }}
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Get latest release tag
+ id: latest-release
+ run: |
+ latest_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
+ echo "latest-tag=$latest_tag" >> $GITHUB_OUTPUT
+ echo "Latest release tag: $latest_tag"
+
+ - name: Check for changes since last release
+ id: check
+ run: |
+ latest_tag="${{ steps.latest-release.outputs.latest-tag }}"
+ if [ -z "$latest_tag" ]; then
+ echo "No previous releases found, will create first release"
+ echo "has-changes=true" >> $GITHUB_OUTPUT
+ else
+ # Check if there are commits since the last release
+ commits_since=$(git rev-list --count $latest_tag..HEAD 2>/dev/null || echo "1")
+ echo "Commits since last release: $commits_since"
+ if [ "$commits_since" -gt "0" ]; then
+ echo "has-changes=true" >> $GITHUB_OUTPUT
+ else
+ echo "has-changes=false" >> $GITHUB_OUTPUT
+ fi
+ fi
+
+ - name: Generate new tag
+ id: tag
+ if: steps.check.outputs.has-changes == 'true'
+ run: |
+ # Generate nightly tag with date
+ new_tag="nightly-$(date +%Y%m%d)"
+ echo "new-tag=$new_tag" >> $GITHUB_OUTPUT
+ echo "New tag will be: $new_tag"
+
+ build-and-release:
+ name: Build and create nightly release
+ runs-on: windows-latest
+ needs: check-changes
+ if: needs.check-changes.outputs.has-changes == 'true'
+ permissions:
+ contents: write
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up JDK
+ uses: actions/setup-java@v4
+ with:
+ java-version: '22'
+ distribution: 'corretto'
+
+ - name: Build with Gradle Wrapper
+ run: cd app; ./gradlew :target:desktop:createDistributable
+
+ - name: Zip distributable
+ run: |
+ cd app
+ Compress-Archive -Path "target/desktop/build/compose/binaries/main/app" -DestinationPath "../cleanmeter-nightly.zip"
+
+ - name: Create Release
+ id: create-release
+ uses: actions/create-release@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ tag_name: ${{ needs.check-changes.outputs.new-tag }}
+ release_name: Nightly Release ${{ needs.check-changes.outputs.new-tag }}
+ body: |
+ 🌙 **Nightly Release** - Automatically generated from latest changes on main branch
+
+ This is an automated nightly build containing the latest changes from the main branch.
+
+ **⚠️ Note:** This is a development build and may contain unstable features.
+
+ **Installation:** Download and extract the cleanmeter-nightly.zip file.
+
+ Built from commit: ${{ github.sha }}
+ draft: false
+ prerelease: true
+
+ - name: Upload Release Asset
+ uses: actions/upload-release-asset@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ upload_url: ${{ steps.create-release.outputs.upload_url }}
+ asset_path: ./cleanmeter-nightly.zip
+ asset_name: cleanmeter-nightly.zip
+ asset_content_type: application/zip
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..a0bd399
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,37 @@
+name: Publish
+
+on:
+ workflow_dispatch:
+ push:
+ tags:
+ - '*'
+
+jobs:
+ build:
+ name: Publish binaries
+ runs-on: windows-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up JDK
+ uses: actions/setup-java@v4
+ with:
+ java-version: '22'
+ distribution: 'corretto'
+
+ - name: Build with Gradle Wrapper
+ run: cd app; ./gradlew :target:desktop:createDistributable
+
+ - name: Zip distributable
+ run: zip -r cleanmeter.zip target/desktop/build/compose/binaries/main/app
+
+ - name: Upload binaries to release
+ uses: svenstaro/upload-release-action@v2
+ with:
+ repo_token: ${{ secrets.GITHUB_TOKEN }}
+ file: cleanmeter.zip
+ asset_name: cleanmeter
+ tag: ${{ github.ref }}
+ overwrite: false
+ draft: true
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index a684c59..7f15036 100644
--- a/.gitignore
+++ b/.gitignore
@@ -155,4 +155,7 @@ bin/
/httpRequests/
# Datasource local storage ignored files
/dataSources/
-/dataSources.local.xml
\ No newline at end of file
+/dataSources.local.xml
+
+!app/bin/*.bat
+preferences.json
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 9d08854..312bf2e 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -1,13 +1,6 @@
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index 9fc29e4..0819acb 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,7 +1,10 @@
+
+
+
-
+
diff --git a/HardwareMonitor/HardwareMonitor.sln b/HardwareMonitor/HardwareMonitor.sln
index cf580ab..7a32d91 100644
--- a/HardwareMonitor/HardwareMonitor.sln
+++ b/HardwareMonitor/HardwareMonitor.sln
@@ -2,10 +2,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HardwareMonitor", "HardwareMonitor\HardwareMonitor.csproj", "{FE819E03-339E-44E6-B7A3-1C6D997126D0}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HardwareMonitorTester", "HardwareMonitorTester\HardwareMonitorTester.csproj", "{574A6483-C331-496D-9F25-1350914859C8}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Updater", "Updater\Updater.csproj", "{A5991FC1-069F-43D7-8E8D-158D1EE8DE02}"
-EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
diff --git a/HardwareMonitor/HardwareMonitor/HardwareMonitor.csproj b/HardwareMonitor/HardwareMonitor/HardwareMonitor.csproj
index 6430d7c..9199963 100644
--- a/HardwareMonitor/HardwareMonitor/HardwareMonitor.csproj
+++ b/HardwareMonitor/HardwareMonitor/HardwareMonitor.csproj
@@ -2,33 +2,56 @@
Exe
- net8.0
+ net9.0
enable
enable
- ..\..\target\desktop\src\main\resources\imgs\favicon.ico
+ ..\..\app\target\desktop\src\main\resources\imgs\favicon.ico
true
-
-
-
- none
+ true
+ true
+ true
+ win-x64
+ None
-
+
+
-
+
-
+
+ $(PublishDir)
+ $(TargetDir)\publish
+
+
+
+
+
+
+
+
+
+
+
+ win-x64
+ osx-arm64
+ linux-x64
+ $(RuntimeIdentifier)
+
+
+
+
diff --git a/HardwareMonitor/HardwareMonitor/Monitor/MonitorPacketCommand.cs b/HardwareMonitor/HardwareMonitor/Monitor/MonitorPacketCommand.cs
index 9385409..3f9d56b 100644
--- a/HardwareMonitor/HardwareMonitor/Monitor/MonitorPacketCommand.cs
+++ b/HardwareMonitor/HardwareMonitor/Monitor/MonitorPacketCommand.cs
@@ -6,5 +6,6 @@ public enum MonitorPacketCommand : short
RefreshPresentMonApps = 1,
SelectPresentMonApp = 2,
PresentMonApps = 3,
- SelectPollingRate = 4
+ SelectPollingRate = 4,
+ SetForegroundApplication = 5
}
\ No newline at end of file
diff --git a/HardwareMonitor/HardwareMonitor/Monitor/MonitorPoller.cs b/HardwareMonitor/HardwareMonitor/Monitor/MonitorPoller.cs
index 5a49adb..1219f4a 100644
--- a/HardwareMonitor/HardwareMonitor/Monitor/MonitorPoller.cs
+++ b/HardwareMonitor/HardwareMonitor/Monitor/MonitorPoller.cs
@@ -15,7 +15,7 @@ namespace HardwareMonitor.Monitor;
public class MonitorPoller(
IHostApplicationLifetime hostApplicationLifetime,
ILogger logger
-) : BackgroundService
+) : BackgroundService, IDisposable
{
private readonly Computer _computer = new()
{
@@ -38,68 +38,99 @@ ILogger logger
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
- logger.LogInformation("Starting monitor");
+ logger.LogInformation("Starting hardware monitor service");
- _computer.Open();
- _computer.Accept(new UpdateVisitor());
- _presentMonPoller.Start(stoppingToken);
- _presentMonPoller.OnUpdateApps += SendPresentMonAppsToClients;
- _socketHost.StartServer();
- _socketHost.OnClientData += OnClientData;
- _socketHost.OnClientConnected += OnClientConnected;
+ try
+ {
+ _computer.Open();
+ _computer.Accept(new UpdateVisitor());
+ _presentMonPoller.Start(stoppingToken);
+ _presentMonPoller.OnUpdateApps += SendPresentMonAppsToClients;
+ _socketHost.StartServer();
+ _socketHost.OnClientData += OnClientData;
+ _socketHost.OnClientConnected += OnClientConnected;
- var sharedMemoryData = QueryHardwareData();
+ var sharedMemoryData = QueryHardwareData();
- using var memoryStream = new MemoryStream();
- using var writer = new BinaryWriter(memoryStream);
- var accumulator = 0;
+ using var memoryStream = new MemoryStream();
+ using var writer = new BinaryWriter(memoryStream);
+ var accumulator = 0;
- WriteDataToStream(writer, sharedMemoryData);
+ WriteDataToStream(writer, sharedMemoryData);
- while (!stoppingToken.IsCancellationRequested)
- {
- if (!_socketHost.HasConnections())
- {
- //logger.LogInformation("No clients connected, waiting for connections...");
- await Task.Delay(1000, stoppingToken);
- continue;
- }
+ logger.LogInformation("Hardware monitor service started successfully");
- foreach (var hardware in sharedMemoryData.Hardwares)
+ while (!stoppingToken.IsCancellationRequested)
{
- try
+ if (!_socketHost.HasConnections())
{
- hardware.Update();
+ //logger.LogInformation("No clients connected, waiting for connections...");
+ await Task.Delay(1000, stoppingToken);
+ continue;
}
- catch
+
+ foreach (var hardware in sharedMemoryData.Hardwares)
{
- hardware.StopUpdates();
- logger.LogError("Stopping updates of {HardwareName} - {HardwareIdentifier}", hardware.Name, hardware.Identifier);
+ try
+ {
+ hardware.Update();
+ }
+ catch
+ {
+ hardware.StopUpdates();
+ logger.LogError("Stopping updates of {HardwareName} - {HardwareIdentifier}", hardware.Name,
+ hardware.Identifier);
+ }
}
- }
- WriteDataToStream(writer, sharedMemoryData);
+ WriteDataToStream(writer, sharedMemoryData);
- if (_socketHost.HasConnections())
- {
- _socketHost.SendToAll(memoryStream.ToArray());
- } else
- {
- //logger.LogInformation("No clients connected, not sending data");
- }
+ if (_socketHost.HasConnections())
+ {
+ _socketHost.SendToAll(memoryStream.ToArray());
+ }
+ else
+ {
+ //logger.LogInformation("No clients connected, not sending data");
+ }
- if (accumulator >= 1000)
- {
- GC.Collect();
- accumulator = 0;
- }
+ if (accumulator >= 1000)
+ {
+ GC.Collect();
+ accumulator = 0;
+ }
- accumulator += 500;
- await Task.Delay(_pollingRate, stoppingToken);
+ accumulator += 500;
+ await Task.Delay(_pollingRate, stoppingToken);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ logger.LogInformation("Hardware monitor service shutdown requested");
}
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Unexpected error in hardware monitor service");
+ throw;
+ }
+ finally
+ {
+ logger.LogInformation("Shutting down hardware monitor service");
+ }
+ }
- Stop();
- hostApplicationLifetime.StopApplication();
+ public override async Task StopAsync(CancellationToken cancellationToken)
+ {
+ logger.LogInformation("Stop requested for hardware monitor service");
+
+ try
+ {
+ await base.StopAsync(cancellationToken);
+ }
+ finally
+ {
+ Stop();
+ }
}
private static void WriteDataToStream(BinaryWriter writer, SharedMemoryData sharedMemoryData)
@@ -155,6 +186,9 @@ private void OnClientData(byte[] data)
case MonitorPacketCommand.SelectPollingRate:
SelectPollingRate(data);
break;
+ case MonitorPacketCommand.SetForegroundApplication:
+ SetForegroundApplication(data);
+ break;
// server -> client cases
case MonitorPacketCommand.Data:
@@ -181,6 +215,14 @@ private void SelectPresentMonApp(byte[] data)
_presentMonPoller.SetSelectedApp(appName);
}
+ private void SetForegroundApplication(byte[] data)
+ {
+ // start at 2 because the first 2 were the command
+ var size = BitConverter.ToInt16(data, 2);
+ var appName = Encoding.UTF8.GetString(data, 4, size);
+ _presentMonPoller.SetForegroundApplication(appName);
+ }
+
private void SendPresentMonAppsToClients()
{
using var memoryStream = new MemoryStream();
@@ -244,10 +286,37 @@ private SharedMemoryData QueryHardwareData()
private void Stop()
{
- _computer.Close();
- _presentMonPoller.Stop();
- _socketHost.Close();
- _socketHost.OnClientData -= OnClientData;
+ logger.LogInformation("Stopping monitor services");
+
+ try
+ {
+ _presentMonPoller.Stop();
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error stopping PresentMon poller");
+ }
+
+ try
+ {
+ _socketHost.Close();
+ _socketHost.OnClientData -= OnClientData;
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error closing socket host");
+ }
+
+ try
+ {
+ _computer.Close();
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error closing hardware computer");
+ }
+
+ logger.LogInformation("Monitor services stopped");
}
private static SharedMemoryHardware MapHardware(IHardware hardware) => new()
@@ -283,4 +352,11 @@ public static unsafe bool IsNaN(float f)
int binary = *(int*)(&f);
return ((binary & 0x7F800000) == 0x7F800000) && ((binary & 0x007FFFFF) != 0);
}
+
+ public void Dispose()
+ {
+ Stop();
+ _computer?.Close();
+ GC.SuppressFinalize(this);
+ }
}
\ No newline at end of file
diff --git a/HardwareMonitor/HardwareMonitor/PresentMon/PresentMonPoller.cs b/HardwareMonitor/HardwareMonitor/PresentMon/PresentMonPoller.cs
index bc93a98..8d76da9 100644
--- a/HardwareMonitor/HardwareMonitor/PresentMon/PresentMonPoller.cs
+++ b/HardwareMonitor/HardwareMonitor/PresentMon/PresentMonPoller.cs
@@ -24,6 +24,7 @@ public class PresentMonPoller(ILogger logger)
private CultureInfo _cultureInfo = (CultureInfo)CultureInfo.CurrentCulture.Clone();
private string _currentSelectedApp = NO_SELECTED_APP;
+ private string _currentForegroundApp;
public async void Start(CancellationToken stoppingToken)
{
@@ -68,7 +69,31 @@ public async void Start(CancellationToken stoppingToken)
public void Stop()
{
- _process.Kill(true);
+ try
+ {
+ if (_process != null && !_process.HasExited)
+ {
+ logger.LogInformation("Stopping PresentMon process");
+
+ // Try graceful shutdown first
+ _process.CancelOutputRead();
+ _process.CancelErrorRead();
+
+ // Give it a moment to exit gracefully
+ if (!_process.WaitForExit(1000))
+ {
+ // Force kill if it doesn't exit gracefully
+ _process.Kill(true);
+ }
+
+ _process.Dispose();
+ logger.LogInformation("PresentMon process stopped");
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error stopping PresentMon process");
+ }
}
private void ParseData(string? argsData)
@@ -84,6 +109,11 @@ private void ParseData(string? argsData)
return;
}
+ if (_currentSelectedApp == NO_SELECTED_APP && _currentForegroundApp != parts[0])
+ {
+ return;
+ }
+
if (float.TryParse(parts[9], NumberStyles.Any, _cultureInfo, out var frametime))
{
Frametime.Value = frametime;
@@ -112,6 +142,11 @@ public void SetSelectedApp(string appName)
_currentSelectedApp = appName;
}
+ public void SetForegroundApplication(string appName)
+ {
+ _currentForegroundApp = appName;
+ }
+
private async Task TerminateCurrentPresentMon()
{
var processStartInfo = new ProcessStartInfo
diff --git a/HardwareMonitor/HardwareMonitor/Program.cs b/HardwareMonitor/HardwareMonitor/Program.cs
index 3906db8..7ccd7dc 100644
--- a/HardwareMonitor/HardwareMonitor/Program.cs
+++ b/HardwareMonitor/HardwareMonitor/Program.cs
@@ -3,22 +3,86 @@
using HardwareMonitor.Monitor;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
using Serilog;
var builder = Host.CreateDefaultBuilder(args)
- .ConfigureServices(services => services.AddHostedService())
- .UseWindowsService()
- .UseSerilog((context, services, loggerConfiguration) => loggerConfiguration
- .ReadFrom.Configuration(context.Configuration)
- .ReadFrom.Services(services)
- .Enrich.FromLogContext()
- .WriteTo.File(
- Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "LogFiles",
- $"{DateTime.Now.Year}-{DateTime.Now.Month}-{DateTime.Now.Day}", "Log.txt"),
- rollingInterval: RollingInterval.Infinite,
- outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{Level}] {Message}{NewLine}{Exception}")
- .WriteTo.Console()
- );
+ .ConfigureServices(services =>
+ {
+ services.AddHostedService();
+ services.Configure(options =>
+ {
+ options.ShutdownTimeout = TimeSpan.FromSeconds(30);
+ });
+ })
+ .UseWindowsService(options =>
+ {
+ options.ServiceName = "CleanMeter Hardware Monitor";
+ })
+ .ConfigureLogging((context, logging) =>
+ {
+ logging.ClearProviders();
+ if (Environment.UserInteractive)
+ {
+ logging.AddConsole();
+ }
+ logging.AddEventLog();
+ })
+ .UseSerilog((context, services, loggerConfiguration) =>
+ {
+ var logPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "LogFiles");
+ Directory.CreateDirectory(logPath);
+
+ loggerConfiguration
+ .ReadFrom.Configuration(context.Configuration)
+ .ReadFrom.Services(services)
+ .Enrich.FromLogContext()
+ .WriteTo.File(
+ Path.Combine(logPath, "cleanmeter-hardware-monitor-.log"),
+ rollingInterval: RollingInterval.Day,
+ retainedFileCountLimit: 30,
+ outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{Level}] {Message}{NewLine}{Exception}");
+
+ // Only log to console when running interactively (not as service)
+ if (Environment.UserInteractive)
+ {
+ loggerConfiguration.WriteTo.Console();
+ }
+ });
var host = builder.Build();
-host.Run();
\ No newline at end of file
+
+// Handle Windows shutdown signals
+var lifetime = host.Services.GetRequiredService();
+var logger = host.Services.GetRequiredService>();
+
+lifetime.ApplicationStopping.Register(() =>
+{
+ logger.LogInformation("Application shutdown signal received");
+});
+
+// Handle console cancel events (Ctrl+C) only when running interactively
+if (Environment.UserInteractive)
+{
+ Console.CancelKeyPress += (sender, e) =>
+ {
+ logger.LogInformation("Console cancel event received");
+ e.Cancel = true;
+ lifetime.StopApplication();
+ };
+}
+
+try
+{
+ logger.LogInformation("Starting CleanMeter Hardware Monitor Service");
+ await host.RunAsync();
+}
+catch (Exception ex)
+{
+ logger.LogCritical(ex, "Application terminated unexpectedly");
+ throw;
+}
+finally
+{
+ logger.LogInformation("CleanMeter Hardware Monitor Service stopped");
+}
\ No newline at end of file
diff --git a/HardwareMonitor/HardwareMonitor/Sockets/PipeHost.cs b/HardwareMonitor/HardwareMonitor/Sockets/PipeHost.cs
index 4e4aae5..6d8a523 100644
--- a/HardwareMonitor/HardwareMonitor/Sockets/PipeHost.cs
+++ b/HardwareMonitor/HardwareMonitor/Sockets/PipeHost.cs
@@ -1,10 +1,14 @@
-using System.IO.Pipes;
+using System.Diagnostics.CodeAnalysis;
+using System.IO.Pipes;
+using System.Security.AccessControl;
+using System.Security.Principal;
using Microsoft.Extensions.Logging;
// ReSharper disable FieldCanBeMadeReadOnly.Local
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
namespace HardwareMonitor.Sockets;
+[SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")]
public class PipeHost(ILogger logger)
{
private readonly string _pipeName = "HardwareMonitor_31337";
@@ -27,14 +31,18 @@ private async Task AcceptClientsAsync()
{
try
{
- var pipeServer = new NamedPipeServerStream(
+ var pipeSecurity = new PipeSecurity();
+ var everyoneSid = new SecurityIdentifier(WellKnownSidType.WorldSid, null);
+ pipeSecurity.AddAccessRule(new PipeAccessRule(everyoneSid, PipeAccessRights.FullControl, AccessControlType.Allow));
+ var pipeServer = NamedPipeServerStreamAcl.Create(
_pipeName,
PipeDirection.InOut,
NamedPipeServerStream.MaxAllowedServerInstances,
PipeTransmissionMode.Byte,
PipeOptions.Asynchronous,
4096, // inBufferSize
- 4096 // outBufferSize
+ 4096, // outBufferSize
+ pipeSecurity
);
logger.LogInformation("Waiting for client connection on pipe: {PipeName}", _pipeName);
@@ -130,7 +138,8 @@ public void Close()
try
{
- _serverTask?.Wait(5000);
+ // Reduce timeout for faster shutdown during Windows shutdown
+ _serverTask?.Wait(1000);
}
catch { }
diff --git a/HardwareMonitor/HardwareMonitorTester/HardwareMonitorTester.csproj b/HardwareMonitor/HardwareMonitorTester/HardwareMonitorTester.csproj
deleted file mode 100644
index c2a4ef4..0000000
--- a/HardwareMonitor/HardwareMonitorTester/HardwareMonitorTester.csproj
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
- Exe
- net8.0
- enable
- enable
-
-
-
-
-
-
-
diff --git a/HardwareMonitor/HardwareMonitorTester/Program.cs b/HardwareMonitor/HardwareMonitorTester/Program.cs
deleted file mode 100644
index 72571bd..0000000
--- a/HardwareMonitor/HardwareMonitorTester/Program.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-// See https://aka.ms/new-console-template for more information
-
-using System.Net;
-using System.Net.Sockets;
-using HardwareMonitor.Monitor;
-
-await (new TestClass()).Main();
-
-public class TestClass
-{
- byte[] buffer = new byte[500_000];
-
- public async Task Main()
- {
- var ipAddress = IPAddress.Loopback;
- var localEndPoint = new IPEndPoint(ipAddress, 31337);
-
- var socket = new Socket(ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
- await socket.ConnectAsync(localEndPoint);
- var buffer = new byte[500_000];
-
- while (true)
- {
- // Receive ack.
- var received = await socket.ReceiveAsync(buffer, SocketFlags.None);
- var command = (MonitorPacketCommand)BitConverter.ToInt16(buffer, 0);
- Console.WriteLine($"Received {command} {received} bytes");
- }
- }
-}
\ No newline at end of file
diff --git a/HardwareMonitor/Updater/Program.cs b/HardwareMonitor/Updater/Program.cs
deleted file mode 100644
index 6737561..0000000
--- a/HardwareMonitor/Updater/Program.cs
+++ /dev/null
@@ -1,92 +0,0 @@
-// See https://aka.ms/new-console-template for more information
-
-using System.Diagnostics;
-using System.IO.Compression;
-
-var arguments = Environment.GetCommandLineArgs();
-
-new Updater().Update(arguments);
-
-class Updater
-{
- public void Update(string[] arguments)
- {
- var dict = ParseArguments(arguments);
- if (!dict.ContainsKey("--package") || !dict.ContainsKey("--path") || !dict.ContainsKey("--autostart")) return;
- var isAutostart = dict["--autostart"] == "true";
- if (!isAutostart)
- {
- ChangeService("stop");
- // DeleteService();
- }
-
- RenameCurrentExecutable();
- UnzipPackage(dict["--package"], dict["--path"]);
- LaunchApp(isAutostart, dict["--path"]);
- Environment.Exit(0);
- }
-
- private void ChangeService(string action)
- {
- var processStartInfo = new ProcessStartInfo
- {
- CreateNoWindow = true,
- RedirectStandardOutput = true,
- UseShellExecute = false,
- FileName = "cmd.exe",
- Arguments = $"/c sc {action} svcleanmeter"
- };
- var process = new Process();
- process.StartInfo = processStartInfo;
- process.Start();
- }
-
- private void RenameCurrentExecutable()
- {
- var currentPath = Environment.ProcessPath!;
- var newPath = Path.ChangeExtension(currentPath, ".bak");
- File.Move(currentPath, newPath, true);
- }
-
- private void UnzipPackage(string package, string destination)
- {
- ZipFile.ExtractToDirectory(package, $@"{destination}\..\", true);
- }
-
- private void LaunchApp(bool isAutostart, string path)
- {
- if (isAutostart)
- {
- ChangeService("start");
- }
-
- var processStartInfo = new ProcessStartInfo
- {
- CreateNoWindow = true,
- RedirectStandardOutput = true,
- UseShellExecute = false,
- FileName = "cmd.exe",
- Arguments = $"/c {path}\\cleanmeter.exe"
- };
- var process = new Process();
- process.StartInfo = processStartInfo;
- process.Start();
- }
-
- private Dictionary ParseArguments(string[] args)
- {
- var arguments = new Dictionary();
-
- foreach (var argument in args)
- {
- var splitted = argument.Split('=');
-
- if (splitted.Length == 2)
- {
- arguments[splitted[0]] = splitted[1];
- }
- }
-
- return arguments;
- }
-}
\ No newline at end of file
diff --git a/HardwareMonitor/Updater/Updater.csproj b/HardwareMonitor/Updater/Updater.csproj
deleted file mode 100644
index 2f4fc77..0000000
--- a/HardwareMonitor/Updater/Updater.csproj
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
- Exe
- net8.0
- enable
- enable
-
-
-
diff --git a/app/.idea/.gitignore b/app/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/app/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/app/.idea/.name b/app/.idea/.name
new file mode 100644
index 0000000..ec7ebd6
--- /dev/null
+++ b/app/.idea/.name
@@ -0,0 +1 @@
+CleanMeter
\ No newline at end of file
diff --git a/app/.idea/artifacts/native_jvm.xml b/app/.idea/artifacts/native_jvm.xml
new file mode 100644
index 0000000..add699c
--- /dev/null
+++ b/app/.idea/artifacts/native_jvm.xml
@@ -0,0 +1,8 @@
+
+
+ $PROJECT_DIR$/core/native/build/libs
+
+
+
+
+
\ No newline at end of file
diff --git a/app/.idea/codeStyles/Project.xml b/app/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000..1bec35e
--- /dev/null
+++ b/app/.idea/codeStyles/Project.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/.idea/codeStyles/codeStyleConfig.xml b/app/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..79ee123
--- /dev/null
+++ b/app/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/.idea/compiler.xml b/app/.idea/compiler.xml
new file mode 100644
index 0000000..b589d56
--- /dev/null
+++ b/app/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/.idea/gradle.xml b/app/.idea/gradle.xml
new file mode 100644
index 0000000..543c034
--- /dev/null
+++ b/app/.idea/gradle.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/.idea/kotlinc.xml b/app/.idea/kotlinc.xml
new file mode 100644
index 0000000..d4b7acc
--- /dev/null
+++ b/app/.idea/kotlinc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/.idea/misc.xml b/app/.idea/misc.xml
new file mode 100644
index 0000000..b1f50a1
--- /dev/null
+++ b/app/.idea/misc.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/.idea/vcs.xml b/app/.idea/vcs.xml
new file mode 100644
index 0000000..6c0b863
--- /dev/null
+++ b/app/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/LICENSE b/app/LICENSE
similarity index 100%
rename from LICENSE
rename to app/LICENSE
diff --git a/README.md b/app/README.md
similarity index 100%
rename from README.md
rename to app/README.md
diff --git a/app/bin/win-x64/registry-delete.bat b/app/bin/win-x64/registry-delete.bat
new file mode 100644
index 0000000..e125ed2
--- /dev/null
+++ b/app/bin/win-x64/registry-delete.bat
@@ -0,0 +1,59 @@
+@echo off
+:: -------------------------------
+:: Auto-elevate to Administrator
+:: -------------------------------
+net session >nul 2>&1
+if %errorLevel% neq 0 (
+ echo Requesting administrative privileges...
+ powershell -Command "Start-Process '%~f0' -ArgumentList '%*' -Verb RunAs"
+ exit /b
+)
+
+:: -------------------------------
+:: Registry Delete
+:: -------------------------------
+set REGISTRY_KEY=HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
+set ENTRY_NAME=%~1
+
+echo ========================================
+echo CleanMeter Registry Deleter
+echo ========================================
+echo.
+
+echo DEBUG: Entry Name = "%ENTRY_NAME%"
+echo.
+
+:: Check if parameter is provided
+if "%ENTRY_NAME%"=="" (
+ echo ERROR: Missing entry name parameter.
+ echo Usage: %~nx0 "entry_name"
+ echo Example: %~nx0 "MyApp"
+ exit /b 1
+)
+
+echo Deleting registry entry...
+echo Key: %REGISTRY_KEY%
+echo Name: %ENTRY_NAME%
+echo.
+
+:: Check if entry exists
+reg query "%REGISTRY_KEY%" /v "%ENTRY_NAME%" >nul 2>&1
+if %errorLevel% neq 0 (
+ echo WARNING: Registry entry "%ENTRY_NAME%" does not exist.
+ echo Operation completed.
+ exit /b 0
+)
+
+echo DEBUG: Executing command: reg delete "%REGISTRY_KEY%" /v "%ENTRY_NAME%" /f
+reg delete "%REGISTRY_KEY%" /v "%ENTRY_NAME%" /f
+set DELETE_RESULT=%errorLevel%
+echo DEBUG: Command result: %DELETE_RESULT%
+
+if %DELETE_RESULT% neq 0 (
+ echo ERROR: Failed to delete registry entry (Error Code: %DELETE_RESULT%)
+ exit /b %DELETE_RESULT%
+) else (
+ echo SUCCESS: Registry entry deleted successfully!
+)
+
+echo Operation completed.
diff --git a/app/bin/win-x64/registry-write.bat b/app/bin/win-x64/registry-write.bat
new file mode 100644
index 0000000..3819d87
--- /dev/null
+++ b/app/bin/win-x64/registry-write.bat
@@ -0,0 +1,61 @@
+@echo off
+:: -------------------------------
+:: Auto-elevate to Administrator
+:: -------------------------------
+net session >nul 2>&1
+if %errorLevel% neq 0 (
+ echo Requesting administrative privileges...
+ powershell -Command "Start-Process '%~f0' -ArgumentList '%*' -Verb RunAs"
+ exit /b
+)
+
+:: -------------------------------
+:: Registry Write
+:: -------------------------------
+set REGISTRY_KEY=HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
+set ENTRY_NAME=%~1
+set ENTRY_VALUE=%~2
+
+echo ========================================
+echo CleanMeter Registry Writer
+echo ========================================
+echo.
+
+echo DEBUG: Entry Name = "%ENTRY_NAME%"
+echo DEBUG: Entry Value = "%ENTRY_VALUE%"
+echo.
+
+:: Check if parameters are provided
+if "%ENTRY_NAME%"=="" (
+ echo ERROR: Missing entry name parameter.
+ echo Usage: %~nx0 "entry_name" "entry_value"
+ echo Example: %~nx0 "MyApp" "C:\Path\To\MyApp.exe"
+ exit /b 1
+)
+
+if "%ENTRY_VALUE%"=="" (
+ echo ERROR: Missing entry value parameter.
+ echo Usage: %~nx0 "entry_name" "entry_value"
+ echo Example: %~nx0 "MyApp" "C:\Path\To\MyApp.exe"
+ exit /b 1
+)
+
+echo Writing registry entry...
+echo Key: %REGISTRY_KEY%
+echo Name: %ENTRY_NAME%
+echo Value: %ENTRY_VALUE%
+echo.
+
+echo DEBUG: Executing command: reg add "%REGISTRY_KEY%" /v "%ENTRY_NAME%" /t REG_SZ /d "%ENTRY_VALUE%" /f
+reg add "%REGISTRY_KEY%" /v "%ENTRY_NAME%" /t REG_SZ /d "%ENTRY_VALUE%" /f
+set WRITE_RESULT=%errorLevel%
+echo DEBUG: Command result: %WRITE_RESULT%
+
+if %WRITE_RESULT% neq 0 (
+ echo ERROR: Failed to write registry entry (Error Code: %WRITE_RESULT%)
+ exit /b %WRITE_RESULT%
+) else (
+ echo SUCCESS: Registry entry written successfully!
+)
+
+echo Operation completed.
diff --git a/app/bin/win-x64/service-create.bat b/app/bin/win-x64/service-create.bat
new file mode 100644
index 0000000..76f0ecc
--- /dev/null
+++ b/app/bin/win-x64/service-create.bat
@@ -0,0 +1,83 @@
+@echo off
+:: -------------------------------
+:: Auto-elevate to Administrator
+:: -------------------------------
+net session >nul 2>&1
+if %errorLevel% neq 0 (
+ echo Requesting administrative privileges...
+ powershell -Command "Start-Process '%~f0' -Verb RunAs"
+ exit /b
+)
+
+:: -------------------------------
+:: Service config
+:: -------------------------------
+set SERVICE_NAME=CleanMeterHardwareMonitor
+set SERVICE_DESCRIPTION=Hardware monitoring service for CleanMeter application
+set DISPLAY_NAME=CleanMeter Hardware Monitor
+set SCRIPT_DIR=%~dp0
+set EXE_PATH=%SCRIPT_DIR%HardwareMonitor.exe
+
+echo ========================================
+echo CleanMeter Hardware Monitor - CREATE
+echo ========================================
+echo.
+
+REM Check if executable exists
+echo Checking for executable at: %EXE_PATH%
+if not exist "%EXE_PATH%" (
+ echo ERROR: HardwareMonitor.exe not found at: %EXE_PATH%
+ echo Please build and publish the project first using:
+ echo dotnet publish -c Release -r win-x64 --self-contained false
+ pause
+ exit /b 1
+)
+echo Found executable: %EXE_PATH%
+
+REM Stop the service if it's running
+echo Stopping service if running...
+sc stop "%SERVICE_NAME%" >nul
+echo Stop command result: %errorLevel% >nul
+
+REM Delete existing service if it exists
+echo Removing existing service if it exists...
+sc delete "%SERVICE_NAME%" >nul
+echo Delete command result: %errorLevel% >nul
+
+REM Create the service
+echo.
+echo Creating service with command:
+echo sc create "%SERVICE_NAME%" binPath= "\"%EXE_PATH%\"" DisplayName= "%DISPLAY_NAME%" start= auto
+sc create "%SERVICE_NAME%" binPath= "\"%EXE_PATH%\"" DisplayName= "%DISPLAY_NAME%" start= auto
+
+REM Wait a moment for service registration to complete
+echo.
+echo Waiting for service registration to complete...
+timeout /t 5 /nobreak >nul
+
+echo Service created successfully!
+
+REM Set service description
+echo Setting service description...
+sc description "%SERVICE_NAME%" "%SERVICE_DESCRIPTION%"
+echo Description command result: %errorLevel%
+
+REM Configure service recovery options
+echo Configuring service recovery options...
+sc failure "%SERVICE_NAME%" reset= 86400 actions= restart/30000/restart/60000/restart/120000
+echo Recovery options result: %errorLevel%
+
+REM Start the service
+echo Starting service...
+sc start "%SERVICE_NAME%"
+set START_RESULT=%errorLevel%
+echo Start command result: %START_RESULT%
+
+if %START_RESULT% neq 0 (
+ echo WARNING: Service created but failed to start (Error Code: %START_RESULT%)
+ echo Check the Event Log for details or start manually using: sc start "%SERVICE_NAME%"
+) else (
+ echo SUCCESS: CleanMeter Hardware Monitor Service installed and started successfully!
+)
+
+echo Operation completed.
diff --git a/app/bin/win-x64/service-delete.bat b/app/bin/win-x64/service-delete.bat
new file mode 100644
index 0000000..f3a6ca7
--- /dev/null
+++ b/app/bin/win-x64/service-delete.bat
@@ -0,0 +1,60 @@
+@echo off
+:: -------------------------------
+:: Auto-elevate to Administrator
+:: -------------------------------
+net session >nul 2>&1
+if %errorLevel% neq 0 (
+ echo Requesting administrative privileges...
+ powershell -Command "Start-Process '%~f0' -Verb RunAs"
+ exit /b
+)
+
+:: -------------------------------
+:: Service config
+:: -------------------------------
+set SERVICE_NAME=CleanMeterHardwareMonitor
+set SERVICE_DESCRIPTION=Hardware monitoring service for CleanMeter application
+set DISPLAY_NAME=CleanMeter Hardware Monitor
+
+echo ========================================
+echo CleanMeter Hardware Monitor - DELETE
+echo ========================================
+echo.
+
+REM Check if service exists
+echo Checking if service exists...
+sc query "%SERVICE_NAME%" >nul 2>&1
+if %errorLevel% neq 0 (
+ echo Service "%SERVICE_NAME%" does not exist or is already uninstalled.
+ exit /b 1
+)
+echo Service found, proceeding with removal...
+
+REM Stop the service
+echo Stopping service...
+sc stop "%SERVICE_NAME%"
+echo Stop command result: %errorLevel%
+
+REM Wait a moment for the service to stop
+echo Waiting for service to stop...
+timeout /t 5 /nobreak >nul
+
+REM Delete the service
+echo Removing service...
+sc delete "%SERVICE_NAME%"
+set DELETE_RESULT=%errorLevel%
+echo Delete command result: %DELETE_RESULT%
+
+if %DELETE_RESULT% neq 0 (
+ echo ERROR: Failed to remove service (Error Code: %DELETE_RESULT%)
+) else (
+ echo SUCCESS: Service removed successfully!
+ echo.
+ echo Verifying removal:
+ sc query "%SERVICE_NAME%" >nul 2>&1
+ if %errorLevel% neq 0 (
+ echo Service successfully removed from system.
+ ) else (
+ echo WARNING: Service may still be present in system.
+ )
+)
diff --git a/app/bin/win-x64/service-stop.bat b/app/bin/win-x64/service-stop.bat
new file mode 100644
index 0000000..54ea58a
--- /dev/null
+++ b/app/bin/win-x64/service-stop.bat
@@ -0,0 +1,65 @@
+@echo off
+:: -------------------------------
+:: Auto-elevate to Administrator
+:: -------------------------------
+net session >nul 2>&1
+if %errorLevel% neq 0 (
+ echo Requesting administrative privileges...
+ powershell -Command "Start-Process '%~f0' -Verb RunAs"
+ exit /b
+)
+
+:: -------------------------------
+:: Service config
+:: -------------------------------
+set SERVICE_NAME=CleanMeterHardwareMonitor
+set SERVICE_DESCRIPTION=Hardware monitoring service for CleanMeter application
+set DISPLAY_NAME=CleanMeter Hardware Monitor
+
+echo ========================================
+echo CleanMeter Hardware Monitor - STOP
+echo ========================================
+echo.
+
+REM Check if service exists
+echo Checking if service exists...
+sc query "%SERVICE_NAME%" >nul 2>&1
+if %errorLevel% neq 0 (
+ echo Service "%SERVICE_NAME%" does not exist.
+ echo Operation completed.
+ exit /b 1
+)
+
+REM Check if service is running
+echo Checking service status...
+sc query "%SERVICE_NAME%" | find "RUNNING" >nul
+if %errorLevel% neq 0 (
+ echo Service "%SERVICE_NAME%" is not running.
+ echo Operation completed.
+ exit /b 0
+)
+
+REM Stop the service
+echo Stopping service...
+sc stop "%SERVICE_NAME%"
+set STOP_RESULT=%errorLevel%
+echo Stop command result: %STOP_RESULT%
+
+if %STOP_RESULT% neq 0 (
+ echo WARNING: Failed to stop service (Error Code: %STOP_RESULT%)
+ echo Check the Event Log for details or try stopping manually using: sc stop "%SERVICE_NAME%"
+) else (
+ echo Waiting for service to stop...
+ timeout /t 3 /nobreak >nul
+
+ REM Verify service has stopped
+ sc query "%SERVICE_NAME%" | find "STOPPED" >nul
+ if %errorLevel% equ 0 (
+ echo SUCCESS: CleanMeter Hardware Monitor Service stopped successfully!
+ ) else (
+ echo WARNING: Service may still be stopping...
+ )
+)
+
+echo Operation completed.
+
diff --git a/build.gradle.kts b/app/build.gradle.kts
similarity index 100%
rename from build.gradle.kts
rename to app/build.gradle.kts
diff --git a/core/common/build.gradle.kts b/app/core/common/build.gradle.kts
similarity index 79%
rename from core/common/build.gradle.kts
rename to app/core/common/build.gradle.kts
index 96ecdf0..2cd7c42 100644
--- a/core/common/build.gradle.kts
+++ b/app/core/common/build.gradle.kts
@@ -3,6 +3,10 @@ plugins {
kotlin("plugin.serialization")
}
+kotlin {
+ jvmToolchain(17)
+}
+
repositories {
mavenCentral()
}
@@ -10,11 +14,4 @@ repositories {
dependencies {
implementation(libs.kotlinx.serialization)
implementation(libs.kotlinx.coroutines.core)
-}
-
-tasks.test {
- useJUnitPlatform()
-}
-kotlin {
- jvmToolchain(20)
}
\ No newline at end of file
diff --git a/core/common/src/main/kotlin/app/cleanmeter/core/common/hardwaremonitor/HardwareMonitorData.kt b/app/core/common/src/main/kotlin/app/cleanmeter/core/common/hardwaremonitor/HardwareMonitorData.kt
similarity index 100%
rename from core/common/src/main/kotlin/app/cleanmeter/core/common/hardwaremonitor/HardwareMonitorData.kt
rename to app/core/common/src/main/kotlin/app/cleanmeter/core/common/hardwaremonitor/HardwareMonitorData.kt
diff --git a/core/common/src/main/kotlin/app/cleanmeter/core/common/hardwaremonitor/PresentMonReading.kt b/app/core/common/src/main/kotlin/app/cleanmeter/core/common/hardwaremonitor/PresentMonReading.kt
similarity index 100%
rename from core/common/src/main/kotlin/app/cleanmeter/core/common/hardwaremonitor/PresentMonReading.kt
rename to app/core/common/src/main/kotlin/app/cleanmeter/core/common/hardwaremonitor/PresentMonReading.kt
diff --git a/app/core/common/src/main/kotlin/app/cleanmeter/core/common/process/SingleInstance.kt b/app/core/common/src/main/kotlin/app/cleanmeter/core/common/process/SingleInstance.kt
new file mode 100644
index 0000000..7cfab51
--- /dev/null
+++ b/app/core/common/src/main/kotlin/app/cleanmeter/core/common/process/SingleInstance.kt
@@ -0,0 +1,42 @@
+package app.cleanmeter.core.common.process
+
+import app.cleanmeter.core.common.reporting.ApplicationParams
+import app.cleanmeter.core.common.reporting.setDefaultUncaughtExceptionHandler
+import kotlin.system.exitProcess
+
+fun singleInstance(args: Array, block: () -> Unit) {
+ if(isAppAlreadyRunning()) {
+ exitProcess(0)
+ }
+
+ ApplicationParams.parse(args)
+
+ setDefaultUncaughtExceptionHandler()
+
+ block()
+}
+
+private fun isAppAlreadyRunning(): Boolean {
+ val os = System.getProperty("os.name").lowercase()
+ val processName = "Clean Meter" // or jar name
+
+ return try {
+ val process = when {
+ os.contains("win") -> {
+ ProcessBuilder("tasklist", "/FI", "IMAGENAME eq $processName.exe").start()
+ }
+ os.contains("mac") || os.contains("nix") || os.contains("nux") -> {
+ ProcessBuilder("pgrep", "-f", processName).start()
+ }
+ else -> return false
+ }
+
+ val output = process.inputStream.bufferedReader().readText()
+ process.waitFor()
+
+ // Parse output to check if process exists (beyond current instance)
+ output.lines().filterNot { it.isEmpty() }.size > 1
+ } catch (e: Exception) {
+ false
+ }
+}
diff --git a/core/common/src/main/kotlin/app/cleanmeter/core/common/reporting/ApplicationParams.kt b/app/core/common/src/main/kotlin/app/cleanmeter/core/common/reporting/ApplicationParams.kt
similarity index 100%
rename from core/common/src/main/kotlin/app/cleanmeter/core/common/reporting/ApplicationParams.kt
rename to app/core/common/src/main/kotlin/app/cleanmeter/core/common/reporting/ApplicationParams.kt
diff --git a/core/common/src/main/kotlin/app/cleanmeter/core/common/reporting/LoggerUtils.kt b/app/core/common/src/main/kotlin/app/cleanmeter/core/common/reporting/LoggerUtils.kt
similarity index 100%
rename from core/common/src/main/kotlin/app/cleanmeter/core/common/reporting/LoggerUtils.kt
rename to app/core/common/src/main/kotlin/app/cleanmeter/core/common/reporting/LoggerUtils.kt
diff --git a/core/design-system/build.gradle.kts b/app/core/design-system/build.gradle.kts
similarity index 84%
rename from core/design-system/build.gradle.kts
rename to app/core/design-system/build.gradle.kts
index 38c4671..9b28875 100644
--- a/core/design-system/build.gradle.kts
+++ b/app/core/design-system/build.gradle.kts
@@ -4,6 +4,10 @@ plugins {
alias(libs.plugins.compose.compiler)
}
+kotlin {
+ jvmToolchain(17)
+}
+
dependencies {
implementation(compose.desktop.currentOs)
}
\ No newline at end of file
diff --git a/core/design-system/src/main/kotlin/app/cleanmeter/core/designsystem/ColorScheme.kt b/app/core/design-system/src/main/kotlin/app/cleanmeter/core/designsystem/ColorScheme.kt
similarity index 100%
rename from core/design-system/src/main/kotlin/app/cleanmeter/core/designsystem/ColorScheme.kt
rename to app/core/design-system/src/main/kotlin/app/cleanmeter/core/designsystem/ColorScheme.kt
diff --git a/core/design-system/src/main/kotlin/app/cleanmeter/core/designsystem/Primitives.kt b/app/core/design-system/src/main/kotlin/app/cleanmeter/core/designsystem/Primitives.kt
similarity index 100%
rename from core/design-system/src/main/kotlin/app/cleanmeter/core/designsystem/Primitives.kt
rename to app/core/design-system/src/main/kotlin/app/cleanmeter/core/designsystem/Primitives.kt
diff --git a/core/design-system/src/main/kotlin/app/cleanmeter/core/designsystem/Theme.kt b/app/core/design-system/src/main/kotlin/app/cleanmeter/core/designsystem/Theme.kt
similarity index 100%
rename from core/design-system/src/main/kotlin/app/cleanmeter/core/designsystem/Theme.kt
rename to app/core/design-system/src/main/kotlin/app/cleanmeter/core/designsystem/Theme.kt
diff --git a/core/design-system/src/main/kotlin/app/cleanmeter/core/designsystem/Typography.kt b/app/core/design-system/src/main/kotlin/app/cleanmeter/core/designsystem/Typography.kt
similarity index 100%
rename from core/design-system/src/main/kotlin/app/cleanmeter/core/designsystem/Typography.kt
rename to app/core/design-system/src/main/kotlin/app/cleanmeter/core/designsystem/Typography.kt
diff --git a/core/design-system/src/main/resources/font/inter_black.ttf b/app/core/design-system/src/main/resources/font/inter_black.ttf
similarity index 100%
rename from core/design-system/src/main/resources/font/inter_black.ttf
rename to app/core/design-system/src/main/resources/font/inter_black.ttf
diff --git a/core/design-system/src/main/resources/font/inter_bold.ttf b/app/core/design-system/src/main/resources/font/inter_bold.ttf
similarity index 100%
rename from core/design-system/src/main/resources/font/inter_bold.ttf
rename to app/core/design-system/src/main/resources/font/inter_bold.ttf
diff --git a/core/design-system/src/main/resources/font/inter_extrabold.ttf b/app/core/design-system/src/main/resources/font/inter_extrabold.ttf
similarity index 100%
rename from core/design-system/src/main/resources/font/inter_extrabold.ttf
rename to app/core/design-system/src/main/resources/font/inter_extrabold.ttf
diff --git a/core/design-system/src/main/resources/font/inter_extralight.ttf b/app/core/design-system/src/main/resources/font/inter_extralight.ttf
similarity index 100%
rename from core/design-system/src/main/resources/font/inter_extralight.ttf
rename to app/core/design-system/src/main/resources/font/inter_extralight.ttf
diff --git a/core/design-system/src/main/resources/font/inter_light.ttf b/app/core/design-system/src/main/resources/font/inter_light.ttf
similarity index 100%
rename from core/design-system/src/main/resources/font/inter_light.ttf
rename to app/core/design-system/src/main/resources/font/inter_light.ttf
diff --git a/core/design-system/src/main/resources/font/inter_medium.ttf b/app/core/design-system/src/main/resources/font/inter_medium.ttf
similarity index 100%
rename from core/design-system/src/main/resources/font/inter_medium.ttf
rename to app/core/design-system/src/main/resources/font/inter_medium.ttf
diff --git a/core/design-system/src/main/resources/font/inter_regular.ttf b/app/core/design-system/src/main/resources/font/inter_regular.ttf
similarity index 100%
rename from core/design-system/src/main/resources/font/inter_regular.ttf
rename to app/core/design-system/src/main/resources/font/inter_regular.ttf
diff --git a/core/design-system/src/main/resources/font/inter_semibold.ttf b/app/core/design-system/src/main/resources/font/inter_semibold.ttf
similarity index 100%
rename from core/design-system/src/main/resources/font/inter_semibold.ttf
rename to app/core/design-system/src/main/resources/font/inter_semibold.ttf
diff --git a/core/design-system/src/main/resources/font/inter_thin.ttf b/app/core/design-system/src/main/resources/font/inter_thin.ttf
similarity index 100%
rename from core/design-system/src/main/resources/font/inter_thin.ttf
rename to app/core/design-system/src/main/resources/font/inter_thin.ttf
diff --git a/app/core/native/build.gradle.kts b/app/core/native/build.gradle.kts
new file mode 100644
index 0000000..6911869
--- /dev/null
+++ b/app/core/native/build.gradle.kts
@@ -0,0 +1,35 @@
+@file:OptIn(ExperimentalKotlinGradlePluginApi::class)
+
+import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
+
+plugins {
+ kotlin("multiplatform")
+ kotlin("plugin.serialization")
+}
+
+kotlin {
+ jvmToolchain(17)
+
+ jvm()
+
+ compilerOptions {
+ freeCompilerArgs.add("-Xexpect-actual-classes")
+ }
+
+ sourceSets {
+ commonMain {
+ dependencies {
+ implementation(libs.kotlinx.coroutines.core)
+ implementation(libs.kotlinx.serialization)
+ implementation("io.github.z4kn4fein:semver:2.0.0")
+ implementation(projects.core.common)
+ }
+ }
+
+ jvmMain {
+ dependencies {
+ api(libs.jna)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/core/native/src/commonMain/kotlin/app/cleanmeter/core/os/PlatformService.kt b/app/core/native/src/commonMain/kotlin/app/cleanmeter/core/os/PlatformService.kt
new file mode 100644
index 0000000..eb4c45b
--- /dev/null
+++ b/app/core/native/src/commonMain/kotlin/app/cleanmeter/core/os/PlatformService.kt
@@ -0,0 +1,11 @@
+package app.cleanmeter.core.os
+
+import java.awt.Component
+
+/**
+ * Platform-specific service interface for OS-level operations
+ */
+expect object PlatformService {
+ fun changeWindowTransparency(w: Component, isTransparent: Boolean)
+ fun getForegroundProcessName(): String?
+}
diff --git a/app/core/native/src/commonMain/kotlin/app/cleanmeter/core/os/PreferencesRepository.kt b/app/core/native/src/commonMain/kotlin/app/cleanmeter/core/os/PreferencesRepository.kt
new file mode 100644
index 0000000..1050fd7
--- /dev/null
+++ b/app/core/native/src/commonMain/kotlin/app/cleanmeter/core/os/PreferencesRepository.kt
@@ -0,0 +1,14 @@
+package app.cleanmeter.core.os
+
+const val OVERLAY_SETTINGS_PREFERENCE_KEY = "OVERLAY_SETTINGS_PREFERENCE_KEY"
+const val PREFERENCE_START_MINIMIZED = "PREFERENCE_START_MINIMIZED"
+const val PREFERENCE_PERMISSION_CONSENT = "PREFERENCE_PERMISSION_CONSENT"
+
+expect object PreferencesRepository {
+ fun getPreferenceString(key: String): String?
+ fun getPreferenceBoolean(key: String, defaultValue: Boolean = false): Boolean
+ fun getPreferenceBooleanNullable(key: String): Boolean?
+ fun setPreference(key: String, value: String)
+ fun setPreferenceBoolean(key: String, value: Boolean)
+ fun clear()
+}
diff --git a/app/core/native/src/commonMain/kotlin/app/cleanmeter/core/os/StartupManager.kt b/app/core/native/src/commonMain/kotlin/app/cleanmeter/core/os/StartupManager.kt
new file mode 100644
index 0000000..6ae866b
--- /dev/null
+++ b/app/core/native/src/commonMain/kotlin/app/cleanmeter/core/os/StartupManager.kt
@@ -0,0 +1,7 @@
+package app.cleanmeter.core.os
+
+expect object StartupManager {
+ fun isAppRegisteredToStartWithSystem(): Boolean
+ fun registerAppToStartWithSystem()
+ fun removeAppFromStartWithSystem()
+}
diff --git a/app/core/native/src/commonMain/kotlin/app/cleanmeter/core/os/hardwaremonitor/HardwareMonitorProcessManager.kt b/app/core/native/src/commonMain/kotlin/app/cleanmeter/core/os/hardwaremonitor/HardwareMonitorProcessManager.kt
new file mode 100644
index 0000000..2a69b61
--- /dev/null
+++ b/app/core/native/src/commonMain/kotlin/app/cleanmeter/core/os/hardwaremonitor/HardwareMonitorProcessManager.kt
@@ -0,0 +1,10 @@
+package app.cleanmeter.core.os.hardwaremonitor
+
+expect object HardwareMonitorProcessManager {
+ fun start()
+ fun stop()
+ fun isServiceCreated(): Boolean
+ fun createService()
+ fun stopService()
+ fun deleteService()
+}
diff --git a/app/core/native/src/commonMain/kotlin/app/cleanmeter/core/os/resource/NativeResourceLoader.kt b/app/core/native/src/commonMain/kotlin/app/cleanmeter/core/os/resource/NativeResourceLoader.kt
new file mode 100644
index 0000000..685204b
--- /dev/null
+++ b/app/core/native/src/commonMain/kotlin/app/cleanmeter/core/os/resource/NativeResourceLoader.kt
@@ -0,0 +1,5 @@
+package app.cleanmeter.core.os.resource
+
+expect object NativeResourceLoader {
+ fun load(path: String): String
+}
diff --git a/app/core/native/src/commonMain/kotlin/app/cleanmeter/core/os/util/MemoryUtils.kt b/app/core/native/src/commonMain/kotlin/app/cleanmeter/core/os/util/MemoryUtils.kt
new file mode 100644
index 0000000..db18057
--- /dev/null
+++ b/app/core/native/src/commonMain/kotlin/app/cleanmeter/core/os/util/MemoryUtils.kt
@@ -0,0 +1,14 @@
+package app.cleanmeter.core.os.util
+
+import java.io.InputStream
+import java.nio.ByteBuffer
+import java.nio.charset.Charset
+
+expect fun getByteBuffer(input: InputStream, length: Int): ByteBuffer
+expect fun getByteBuffer(input: ByteArray, length: Int, offset: Int): ByteBuffer
+expect fun getByteBuffer(pointer: Any, size: Int, offset: Int = 0): ByteBuffer
+
+expect val systemCharset: Charset
+
+expect fun ByteBuffer.readString(maxLength: Int, charset: Charset = systemCharset): String
+expect fun trim(bytes: ByteArray): ByteArray
diff --git a/app/core/native/src/commonMain/kotlin/app/cleanmeter/core/os/util/env.kt b/app/core/native/src/commonMain/kotlin/app/cleanmeter/core/os/util/env.kt
new file mode 100644
index 0000000..f82e79d
--- /dev/null
+++ b/app/core/native/src/commonMain/kotlin/app/cleanmeter/core/os/util/env.kt
@@ -0,0 +1,3 @@
+package app.cleanmeter.core.os.util
+
+expect fun isDev(): Boolean
diff --git a/app/core/native/src/jvmMain/kotlin/app/cleanmeter/core/os/PlatformService.kt b/app/core/native/src/jvmMain/kotlin/app/cleanmeter/core/os/PlatformService.kt
new file mode 100644
index 0000000..4ad4685
--- /dev/null
+++ b/app/core/native/src/jvmMain/kotlin/app/cleanmeter/core/os/PlatformService.kt
@@ -0,0 +1,36 @@
+package app.cleanmeter.core.os
+
+import app.cleanmeter.core.os.win32.WindowsService
+import java.awt.Component
+
+actual object PlatformService {
+ actual fun changeWindowTransparency(w: Component, isTransparent: Boolean) {
+ when (getCurrentPlatform()) {
+ Platform.WINDOWS -> WindowsService.changeWindowTransparency(w, isTransparent)
+ Platform.MACOS -> {
+ // TODO: Implement macOS window transparency
+ println("macOS window transparency not yet implemented")
+ }
+ Platform.LINUX -> {
+ // TODO: Implement Linux window transparency
+ println("Linux window transparency not yet implemented")
+ }
+ }
+ }
+
+ actual fun getForegroundProcessName(): String? {
+ return when(getCurrentPlatform()) {
+ Platform.WINDOWS -> WindowsService.getForegroundProcessName()
+ Platform.MACOS -> {
+ // TODO: Implement macOS window transparency
+ println("macOS window transparency not yet implemented")
+ null
+ }
+ Platform.LINUX -> {
+ // TODO: Implement Linux window transparency
+ println("Linux window transparency not yet implemented")
+ null
+ }
+ }
+ }
+}
diff --git a/app/core/native/src/jvmMain/kotlin/app/cleanmeter/core/os/PreferencesRepository.kt b/app/core/native/src/jvmMain/kotlin/app/cleanmeter/core/os/PreferencesRepository.kt
new file mode 100644
index 0000000..f0b23ce
--- /dev/null
+++ b/app/core/native/src/jvmMain/kotlin/app/cleanmeter/core/os/PreferencesRepository.kt
@@ -0,0 +1,77 @@
+package app.cleanmeter.core.os
+
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.booleanOrNull
+import kotlinx.serialization.json.contentOrNull
+import java.io.File
+import java.nio.file.Path
+
+actual object PreferencesRepository {
+
+ private val json = Json {
+ prettyPrint = true
+ ignoreUnknownKeys = true
+ }
+
+ private val preferencesFile: File by lazy {
+ val currentDir = Path.of("").toAbsolutePath().toString()
+ File(currentDir, "preferences.json")
+ }
+
+ private fun loadPreferences(): Map {
+ return if (preferencesFile.exists()) {
+ try {
+ val jsonContent = preferencesFile.readText()
+ json.decodeFromString