Repository: cognitedata/dotnet-simulator-utils
Pull request #239: feat: add license controller + process termination support
Author: login: abdullah-cognite
Created: 2025-02-20T06:49:22Z | Merged: 2025-03-11T09:26:43Z

## Why is this PR needed?
This package (`dotnet-simulator-utils`) is used by the PROSPER connector to integrate the PROSPER simulator with CDF. Recently we were having issues with a customer where the PROSPER connector would not be able to run simulations (at random) because the connector was unable to acquire a license for starting the simulator, we came up with an approach where the connector would acquire a license at startup and keep holding on to it for a user defined period of time. We also noticed that occasionally when we shut down the PROSPER simulator, the license would still keep on being consumed unless the simulator process was killed.

**This PR adds** 

- `LicenseController` : To help PROSPER (or any other connector) keep track of holding the license and releasing it after a timeout
- `ProcessUtils` : To help PROSPER (or any other connector) find processes belonging to the current user and terminating them. This part also caused us to modify our Github action, since the tests for this only run on Windows. 

**This PR removes:**
- Previous process termination logic, which unfortunately didnt work as we wanted it to. Now termination is handled by each connector since there can be different error codes at which we want to terminate the process, depending upon the simulator.



Diff:
diff --git a/.github/workflows/buildandtest.yml b/.github/workflows/buildandtest.yml
index dbb48e77..21627285 100644
--- a/.github/workflows/buildandtest.yml
+++ b/.github/workflows/buildandtest.yml
@@ -8,28 +8,56 @@ on:
 
 jobs:
   build:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        os: [ubuntu-latest, windows-latest]
+    steps:
+      - uses: actions/checkout@v4
+      - name: Setup .NET
+        uses: actions/setup-dotnet@v4
+        with:
+          dotnet-version: 8
+      - name: Restore dependencies
+        run: dotnet restore
+      - name: Build
+        run: dotnet build --no-restore
+      - name: Test
+        run: dotnet test --no-restore --verbosity normal /p:Exclude="[*.Test]*" /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput=TestResults/ ./Cognite.Simulator.Tests/ --logger:"console"
+        env:
+          COGNITE_PROJECT: ${{ secrets.COGNITE_PROJECT }}
+          COGNITE_HOST: ${{ secrets.COGNITE_HOST }}
+          AZURE_TENANT: ${{ secrets.AZURE_TENANT }}
+          AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
+          AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
+      - name: Upload coverage to artifacts
+        uses: actions/upload-artifact@v4
+        with:
+          name: coverage-${{ matrix.os }}
+          path: ./Cognite.Simulator.Tests/TestResults/coverage.opencover.xml
+          retention-days: 1
 
+  upload-coverage:
+    needs: build
     runs-on: ubuntu-latest
-
     steps:
-    - uses: actions/checkout@v4
-    - name: Setup .NET
-      uses: actions/setup-dotnet@v4
-      with:
-        dotnet-version: 8
-    - name: Restore dependencies
-      run: dotnet restore
-    - name: Build
-      run: dotnet build --no-restore
-    - name: Test
-      run: dotnet test --no-restore --verbosity normal /p:Exclude="[*.Test]*" /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput=TestResults/ ./Cognite.Simulator.Tests/  --logger:"console"
-      env:
-        COGNITE_PROJECT: ${{ secrets.COGNITE_PROJECT }}
-        COGNITE_HOST: ${{ secrets.COGNITE_HOST }}
-        AZURE_TENANT: ${{ secrets.AZURE_TENANT }}
-        AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
-        AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
-    - name: Upload coverage to Codecov
-      uses: codecov/codecov-action@v5
-      with:
-        token: ${{ secrets.CODECOV_TOKEN }}
+      - uses: actions/checkout@v4
+      
+      - name: Download Ubuntu coverage
+        uses: actions/download-artifact@v4
+        with:
+          name: coverage-ubuntu-latest
+          path: ./coverage-ubuntu
+      
+      - name: Download Windows coverage
+        uses: actions/download-artifact@v4
+        with:
+          name: coverage-windows-latest
+          path: ./coverage-windows
+      
+      - name: Upload combined coverage to Codecov
+        uses: codecov/codecov-action@v5
+        with:
+          token: ${{ secrets.CODECOV_TOKEN }}
+          files: ./coverage-ubuntu/coverage.opencover.xml,./coverage-windows/coverage.opencover.xml
+          fail_ci_if_error: true
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 04ffbd2b..90d300e4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,15 @@
 
 All notable changes to this project will be documented in this file.
 
+## Release v1.0.0-beta-015 (2025-03-06)
+
+### Features
+
+* **Breaking** `TerminateProcess` flag under the `AutomationConfig` section was removed. Since termination logic is unique to each connector this can be implemented by each connector.
+* Fixed bug where invalid config file led to connector running in a loop displaying connection error.
+* Introduced support for license tracking and holding.
+* Introduced support for Process termination.
+
 ## Release v1.0.0-beta-014 (2025-03-05)
 
 ### Bug Fixes
diff --git a/Cognite.Simulator.Tests/Cognite.Simulator.Tests.csproj b/Cognite.Simulator.Tests/Cognite.Simulator.Tests.csproj
index 484afa41..43306bae 100644
--- a/Cognite.Simulator.Tests/Cognite.Simulator.Tests.csproj
+++ b/Cognite.Simulator.Tests/Cognite.Simulator.Tests.csproj
@@ -14,6 +14,7 @@
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>
     <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
+    <PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="9.2.0" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
     <PackageReference Include="Moq" Version="4.20.72" />
     <PackageReference Include="xunit" Version="2.9.3" />
diff --git a/Cognite.Simulator.Tests/UtilsTests/LicenseControllerTest.cs b/Cognite.Simulator.Tests/UtilsTests/LicenseControllerTest.cs
new file mode 100644
index 00000000..c7602779
--- /dev/null
+++ b/Cognite.Simulator.Tests/UtilsTests/LicenseControllerTest.cs
@@ -0,0 +1,216 @@
+using Xunit;
+using System;
+using System.Threading;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Cognite.Simulator.Utils;
+using Cognite.Extractor.Common;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Time.Testing;
+
+using static Cognite.Simulator.Tests.UtilsTests.TestUtilities;
+
+namespace Cognite.Simulator.Tests.UtilsTests{
+
+    public enum TestLicenseState
+    {
+        Released,
+        Held
+    }
+
+    public class LicenseControllerTests
+    {
+        private readonly Mock<ILogger<LicenseControllerTests>> _loggerMock = new Mock<ILogger<LicenseControllerTests>>();
+
+        private FakeTimeProvider fakeTimeProvider = new FakeTimeProvider();
+
+        private (LicenseController controller, StateHolder<TestLicenseState> stateHolder) CreateTracker(
+            Mock<Func<object>>? releaseMock = null,
+            Mock<Func<object>>? acquireMock = null,
+            TimeSpan? licenseLockTime = null,
+            TimeProvider timeProvider = null)
+        {
+            var stateHolder = new StateHolder<TestLicenseState> { State = TestLicenseState.Released };
+            
+            releaseMock ??= new Mock<Func<object>>();
+            acquireMock ??= new Mock<Func<object>>();
+            
+            // Set up the mocks to track the state changes
+            releaseMock.Setup(m => m()).Callback(() => stateHolder.State = TestLicenseState.Released);
+            acquireMock.Setup(m => m()).Callback(() => stateHolder.State = TestLicenseState.Held);
+            
+            var tracker = new LicenseController(
+                licenseLockTime ?? TimeSpan.FromMilliseconds(100),
+                (CancellationToken _token) => { releaseMock.Object(); },
+                (CancellationToken _token) => { acquireMock.Object(); },
+                _loggerMock.Object,
+                fakeTimeProvider
+            );
+            
+            return (tracker, stateHolder);
+        }
+
+        // Simple class to hold state that can be captured in lambdas
+        private class StateHolder<T>
+        {
+            public T State { get; set; }
+        }
+
+
+        [Fact]
+        public void LicenseTracker_ShouldReleaseLicense_WhenNotInUse()
+        {
+            // Arrange
+            Mock<Func<object>> _releaseLicenseFuncMock = new Mock<Func<object>>();
+            Mock<Func<object>> _acquireLicenseFuncMock = new Mock<Func<object>>();
+            var licenseLockTime = TimeSpan.FromMilliseconds(100);
+            var (tracker, license) = CreateTracker( _releaseLicenseFuncMock, _acquireLicenseFuncMock, licenseLockTime);
+
+            Assert.Equal(TestLicenseState.Released, license.State);
+
+            // Check if acquiring the license calls the acquire license function and changes the state
+            tracker.AcquireLicense(CancellationToken.None);
+            _acquireLicenseFuncMock.Verify(f => f(), Times.Once);
+            Assert.Equal(TestLicenseState.Held, license.State);
+            Assert.True(tracker.LicenseHeld);
+
+            using (tracker.BeginUsage()) { /* Use and immediately release */ } 
+            
+            fakeTimeProvider.Advance(TimeSpan.FromMilliseconds(100));
+            
+            _releaseLicenseFuncMock.Verify(f => f(), Times.Once);
+
+            Assert.Equal(TestLicenseState.Released, license.State);
+            Assert.False(tracker.LicenseHeld);
+        }
+
+        [Fact]
+        public void LicenseTracker_ShouldNotRelease_WhileInUse()
+        {
+            // Arrange
+            var licenseLockTime = TimeSpan.FromMilliseconds(100);
+            var (tracker, license) = CreateTracker(licenseLockTime: licenseLockTime);
+            
+            // Act
+            tracker.AcquireLicense(CancellationToken.None);
+            Assert.Equal(TestLicenseState.Held, license.State);
+            Assert.True(tracker.LicenseHeld);
+            using (var usage = tracker.BeginUsage())
+            {
+                fakeTimeProvider.Advance(TimeSpan.FromMilliseconds(200));
+                Assert.True(tracker.LicenseHeld); // Should not release while in use
+            }
+
+        }
+
+        [Fact]
+        public async Task LicenseTracker_ShouldResetTimer_OnNewUsage()
+        {
+            // Arrange
+            var licenseLockTime = TimeSpan.FromMilliseconds(100);
+            var (tracker, license) = CreateTracker(licenseLockTime: licenseLockTime);
+
+            // Act  
+            tracker.AcquireLicense(CancellationToken.None);
+            Assert.Equal(TestLicenseState.Held, license.State);
+            
+            using (tracker.BeginUsage()) { } 
+            fakeTimeProvider.Advance(TimeSpan.FromMilliseconds(50));
+            
+            using (tracker.BeginUsage()) { } // This usage should reset the timeout timer
+            fakeTimeProvider.Advance(TimeSpan.FromMilliseconds(50));
+            
+            // Assert
+            Assert.True(tracker.LicenseHeld); // Should not be released yet due to timer reset
+
+            fakeTimeProvider.Advance(TimeSpan.FromMilliseconds(100));
+
+            Assert.False(tracker.LicenseHeld);
+            Assert.Equal(TestLicenseState.Released, license.State);
+
+        }
+
+        [Fact]
+        public void LicenseTracker_ShouldPreventRelease_DuringContinuousUsage()
+        {
+            // Arrange
+            var (tracker, license) = CreateTracker();
+
+            // Act
+            tracker.AcquireLicense(CancellationToken.None);
+            using (var usage1 = tracker.BeginUsage())
+            {
+                fakeTimeProvider.Advance(TimeSpan.FromMilliseconds(50));
+                using (var usage2 = tracker.BeginUsage())
+                {
+                    fakeTimeProvider.Advance(TimeSpan.FromMilliseconds(100));
+                    Assert.True(tracker.LicenseHeld);
+                }
+            }
+
+            fakeTimeProvider.Advance(TimeSpan.FromMilliseconds(100)); // Wait for the lock time to pass
+            
+            // Assert
+            Assert.True(license.State == TestLicenseState.Released);
+        }
+
+        [Fact]
+        public void LicenseTracker_ShouldUpdateState_WhenLicenseReleasedManually()
+        {
+            // Arrange
+            var (tracker, license) = CreateTracker();
+            tracker.AcquireLicense(CancellationToken.None);
+            using (tracker.BeginUsage()) { }
+            Assert.True(tracker.LicenseHeld);
+
+            // Act
+            tracker.ClearLicenseState();
+            
+            // Assert
+            Assert.False(tracker.LicenseHeld);
+            // Should not change external license state (since this is a manual release)
+            Assert.True(license.State == TestLicenseState.Held);
+        }
+
+        [Fact]
+        public void ClearLicenseState_ShouldLogProperlyAfterReset()
+        {
+            // Arrange
+            Mock<Func<object>> _releaseLicenseFuncMock = new Mock<Func<object>>();
+            Mock<Func<object>> _acquireLicenseFuncMock = new Mock<Func<object>>();
+            var licenseLockTime = TimeSpan.FromMinutes(5);
+            var (tracker, license) = CreateTracker(_releaseLicenseFuncMock, _acquireLicenseFuncMock, licenseLockTime);
+
+            fakeTimeProvider.Advance(TimeSpan.FromMinutes(1)); 
+            VerifyLog(_loggerMock, LogLevel.Information, "License is currently not held", Times.AtLeastOnce(), true);
+
+            // Current time at this point is 2000-01-01 00:00:01
+            tracker.AcquireLicense(CancellationToken.None);
+            using (tracker.BeginUsage()) {  }
+            fakeTimeProvider.Advance(TimeSpan.FromMinutes(2)); 
+            // forcing logging for the Github action
+            tracker.LogLicenseStatus();
+            VerifyLog(_loggerMock, LogLevel.Information, "License is currently held", Times.AtLeastOnce(), true);
+            VerifyLog(_loggerMock, LogLevel.Information, "License release scheduled for ", Times.AtLeastOnce(), true);
+            VerifyLog(_loggerMock, LogLevel.Information, "License will be released in 3.0 minutes (at 2000-01-01 00:06:00)", Times.AtLeastOnce(), true);
+
+            _loggerMock.Invocations.Clear();
+            
+            // Act
+            tracker.ClearLicenseState();
+            
+            fakeTimeProvider.Advance(TimeSpan.FromMinutes(2)); 
+            tracker.LogLicenseStatus();
+            VerifyLog(_loggerMock, LogLevel.Information, "License released forcefully", Times.AtLeastOnce(), true);
+            VerifyLog(_loggerMock, LogLevel.Information, "License is currently not held", Times.AtLeastOnce(), true);
+
+            tracker.AcquireLicense(CancellationToken.None);
+            using (tracker.BeginUsage()) {  }
+            fakeTimeProvider.Advance(TimeSpan.FromMinutes(1)); 
+            tracker.LogLicenseStatus();
+            VerifyLog(_loggerMock, LogLevel.Information, "License is currently held", Times.AtLeastOnce(), true);
+            VerifyLog(_loggerMock, LogLevel.Information, "License has been held for 1.0 minutes", Times.AtLeastOnce(), true);
+            VerifyLog(_loggerMock, LogLevel.Information, "License will be released in 4.0 minutes (at 2000-01-01 00:10:00)", Times.AtLeastOnce(), true);
+        }
+    }
+}
\ No newline at end of file
diff --git a/Cognite.Simulator.Tests/UtilsTests/ModelLibraryTest.cs b/Cognite.Simulator.Tests/UtilsTests/ModelLibraryTest.cs
index d566228c..a555056c 100644
--- a/Cognite.Simulator.Tests/UtilsTests/ModelLibraryTest.cs
+++ b/Cognite.Simulator.Tests/UtilsTests/ModelLibraryTest.cs
@@ -23,6 +23,25 @@ namespace Cognite.Simulator.Tests.UtilsTests
     [Collection(nameof(SequentialTestCollection))]
     public class ModelLibraryTest
     {
+        public static void CleanUp(bool deleteDirectory, StateStoreConfig stateConfig)
+        {
+            try
+            {
+                if (deleteDirectory && Directory.Exists("./files"))
+                {
+                    Directory.Delete("./files", true);
+                }
+                if (stateConfig != null)
+                {
+                    StateUtils.DeleteLocalFile(stateConfig.Location);
+                }
+            }
+            catch (Exception ex)
+            {
+                Console.WriteLine($"Error cleaning up: {ex.Message}");
+            }
+        }
+
         [Fact]
         public async Task TestModelLibrary()
         {
@@ -124,14 +143,7 @@ await modelLibTasks
             }
             finally
             {
-                if (Directory.Exists("./files"))
-                {
-                    Directory.Delete("./files", true);
-                }
-                if (stateConfig != null)
-                {
-                    StateUtils.DeleteLocalFile(stateConfig.Location);
-                }
+                CleanUp(true, stateConfig);
                 await SeedData.DeleteSimulator(cdf, SeedData.SimulatorCreate.ExternalId);
             }
         }
@@ -257,14 +269,7 @@ await lib.GetRunTasks(linkedTokenSource2.Token)
             }
             finally
             {
-                if (Directory.Exists("./files"))
-                {
-                    Directory.Delete("./files", true);
-                }
-                if (stateConfig != null)
-                {
-                    StateUtils.DeleteLocalFile(stateConfig.Location);
-                }
+                CleanUp(true, stateConfig);
                 await SeedData.DeleteSimulator(cdf, SeedData.SimulatorCreate.ExternalId);
             }
         }
@@ -350,14 +355,7 @@ await modelLibTasks
             }
             finally
             {
-                if (Directory.Exists("./files"))
-                {
-                    Directory.Delete("./files", true);
-                }
-                if (stateConfig != null)
-                {
-                    StateUtils.DeleteLocalFile(stateConfig.Location);
-                }
+                CleanUp(true, stateConfig);
                 await SeedData.DeleteSimulator(cdf, SeedData.SimulatorCreate.ExternalId);
             }
         }
@@ -434,10 +432,7 @@ await routineLibTasks
             }
             finally
             {
-                if (stateConfig != null)
-                {
-                    StateUtils.DeleteLocalFile(stateConfig.Location);
-                }
+                CleanUp(false, stateConfig);
                 await SeedData.DeleteSimulator(cdf.CogniteClient, SeedData.SimulatorCreate.ExternalId);
             }
         }
@@ -504,10 +499,7 @@ await routineLibTasks
             }
             finally
             {
-                if (stateConfig != null)
-                {
-                    StateUtils.DeleteLocalFile(stateConfig.Location);
-                }
+                CleanUp(false, stateConfig);
                 await SeedData.DeleteSimulator(cdf.CogniteClient, SeedData.SimulatorCreate.ExternalId);
 
             }
diff --git a/Cognite.Simulator.Tests/UtilsTests/ProcessUtilsTest.cs b/Cognite.Simulator.Tests/UtilsTests/ProcessUtilsTest.cs
new file mode 100644
index 00000000..162dbbad
--- /dev/null
+++ b/Cognite.Simulator.Tests/UtilsTests/ProcessUtilsTest.cs
@@ -0,0 +1,119 @@
+using Microsoft.Extensions.Logging;
+using Moq;
+using System;
+using System.Diagnostics;
+using System.Management;
+using System.Runtime.InteropServices;
+using System.Security.Principal;
+using Xunit;
+using Cognite.Simulator.Utils;
+using static Cognite.Simulator.Tests.UtilsTests.TestUtilities;
+using System.Linq;
+
+namespace Cognite.Simulator.Tests.UtilsTests
+{
+    public class WindowsOnlyFactAttribute : FactAttribute
+    {
+        public WindowsOnlyFactAttribute()
+        {
+            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                Skip = "Test only runs on Windows";
+            }
+        }
+    }
+    public class ProcessUtilsTests
+    {
+        [Fact]
+        public void GetProcessOwnerWmi_ThrowsOnNonWindows()
+        {
+            // We can't easily mock this static method, so we'll only run this test on non-Windows platforms
+            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                Assert.Throws<PlatformNotSupportedException>(() => ProcessUtils.GetProcessOwnerWmi(1));
+            }
+        }
+
+        [Fact]
+        public void GetCurrentUsername_ThrowsOnNonWindows()
+        {
+            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                Assert.Throws<PlatformNotSupportedException>(() => ProcessUtils.GetCurrentUsername());
+            }
+        }
+        
+        private bool IsProcessRunning(string processName, Process process) {
+            try
+            {
+                return Process.GetProcessesByName(processName).Any(p => p.Id == process.Id);
+            }
+            catch
+            {
+                // Any other exception indicates the process is likely not running
+                return false;
+            }
+        }
+
+        [WindowsOnlyFact]
+        public void KillProcess_KillsOwnedProcessesOnly()
+        {
+
+            // Arrange
+            var mockLogger = new Mock<ILogger<ProcessUtilsTests>>();
+            var processName = "notepad";
+            
+            // Create test process to ensure at least one exists
+            Process testProcess = null;
+            testProcess = Process.Start(processName + ".exe");
+
+            // Act
+            ProcessUtils.KillProcess(processName, mockLogger.Object);
+
+            // Verify log messages were called with correct parameters
+            VerifyLog(mockLogger, LogLevel.Debug, "Searching for process : " + processName, Times.Once(), true);
+            VerifyLog(mockLogger, LogLevel.Information, "Killing process with PID", Times.Once(), true);
+            
+
+            bool processStillRunning = IsProcessRunning(processName, testProcess);
+            Assert.False(processStillRunning, "Process should have been terminated");
+        }
+
+        [Fact]
+        public void KillProcess_DoesThrow_WhenProcessNotFound()
+        {
+            // Arrange
+            var mockLogger = new Mock<ILogger<ProcessUtilsTests>>();
+            
+            // Act 
+            Assert.Throws<Exception>(() => ProcessUtils.KillProcess("ThisProcessDoesNotExist_12345", mockLogger.Object));
+            
+            VerifyLog(mockLogger, LogLevel.Error, "No processes found to kill for the current user", Times.Once(), true);
+        }
+
+        [WindowsOnlyFact]
+        public void GetProcessOwnerWmi_ReturnsOwnerString()
+        {
+
+            // Arrange - get current process ID
+            int processId = Process.GetCurrentProcess().Id;
+            
+            // Act
+            string owner = ProcessUtils.GetProcessOwnerWmi(processId);
+            
+            // Assert
+            Assert.Contains("\\", owner); // Owner format should be "domain\user"
+        }
+
+        [WindowsOnlyFact]
+        public void GetProcessOwnerWmi_ReturnsNoOwnerFound_WhenProcessDoesNotExist()
+        {
+
+            // Arrange - unlikely that process ID 999999 exists
+            int nonExistentProcessId = 999999;
+
+            var exception = Assert.Throws<Exception>(() => ProcessUtils.GetProcessOwnerWmi(nonExistentProcessId));
+            Assert.Equal("Process not found", exception.Message);
+        }
+    }
+}
\ No newline at end of file
diff --git a/Cognite.Simulator.Utils/Automation/AutomationUtils.cs b/Cognite.Simulator.Utils/Automation/AutomationUtils.cs
index 77d6888b..a5244afa 100644
--- a/Cognite.Simulator.Utils/Automation/AutomationUtils.cs
+++ b/Cognite.Simulator.Utils/Automation/AutomationUtils.cs
@@ -21,7 +21,6 @@ public abstract class AutomationClient
         protected dynamic Server { get; private set; }
         private readonly ILogger _logger;
         private readonly AutomationConfig _config;
-        private IEnumerable<Process> _processes;
 
         /// <summary>
         /// Creates an instance of the client that instantiates a connection
@@ -40,7 +39,7 @@ public AutomationClient(ILogger logger, AutomationConfig config)
         /// be deactivated after use with <see cref="Shutdown"/>, else resources may not be deallocated
         /// </summary>
         /// <exception cref="SimulatorConnectionException">Thrown if the connection cannot be established</exception>
-        public void Initialize()
+        public virtual void Initialize()
         {
             if (Server != null)
             {
@@ -80,17 +79,6 @@ public virtual void Shutdown()
                     _logger.LogDebug("Released COM Object");
                     Server = null;
                 }
-                if (_processes != null && _processes.Any())
-                {
-                    // This is not ideal but in some cases, activating a simulator instance creates
-                    // a background process that is not terminated when the instance is removed.
-                    // To avoid locking simulator licenses, we have to kill the process
-                    foreach (var prc in _processes)
-                    {
-                        prc.Kill();
-                    }
-                    _processes = null;
-                }
             }
             _logger.LogDebug("Automation server instance removed");
         }
@@ -103,33 +91,26 @@ public virtual void Shutdown()
 
         private dynamic ActivateAutomationServer()
         {
-            var serverType = Type.GetTypeFromProgID(_config.ProgramId);
-            if (serverType == null)
-            {
-                _logger.LogError("Could not find automation server using the id: {ProgId}", _config.ProgramId);
-                throw new SimulatorConnectionException("Cannot connect to simulator");
-            }
-
-            var prcs = new List<int>();
-            if (_config.CanTerminateProcess())
-            {
-                prcs = Process.GetProcessesByName(_config.ProcessId)
-                    .Select(p => p.Id)
-                    .ToList();
-            }
-            dynamic server = Activator.CreateInstance(serverType);
-            if (server == null)
-            {
-                _logger.LogError("Could not activate automation server instance");
-                throw new SimulatorConnectionException("Cannot connect to simulator");
+            try {
+                var serverType = Type.GetTypeFromProgID(_config.ProgramId);
+                if (serverType == null)
+                {
+                    _logger.LogError("Could not find automation server using the id: {ProgId}", _config.ProgramId);
+                    throw new SimulatorConnectionException("Cannot connect to get automation server type");
+                }            
+                dynamic server = Activator.CreateInstance(serverType);
+                if (server == null)
+                {
+                    _logger.LogError("Could not activate automation server instance");
+                    throw new SimulatorConnectionException("Unable to create automation server instance");
+                }
+                return server;
             }
-            if (_config.CanTerminateProcess())
-            {
-                _processes = Process.GetProcessesByName(_config.ProcessId)
-                    .Where(p => !prcs.Contains(p.Id))
-                    .ToList();
+            catch(Exception e) {
+                // wrap every exception in a SimulatorConnectionException
+                throw new SimulatorConnectionException("Cannot connect to automation server", e);
             }
-            return server;
+
         }
     }
     
@@ -179,17 +160,5 @@ public class AutomationConfig
         /// Identifier of the process that should be terminated on shutdown, if any
         /// </summary>
         public string ProcessId { get; set; }
-        
-        /// <summary>
-        /// Whether or not to terminate (kill) the application process after shutdown.
-        /// This is not ideal and should be avoided, unless there is risk of a process
-        /// locking simulator resources
-        /// </summary>
-        public bool TerminateProcess { get; set; }
-
-        internal bool CanTerminateProcess()
-        {
-            return TerminateProcess && !string.IsNullOrEmpty(ProgramId);
-        }
     }
 }
diff --git a/Cognite.Simulator.Utils/Cognite.Simulator.Utils.csproj b/Cognite.Simulator.Utils/Cognite.Simulator.Utils.csproj
index 84b68c70..cacda208 100644
--- a/Cognite.Simulator.Utils/Cognite.Simulator.Utils.csproj
+++ b/Cognite.Simulator.Utils/Cognite.Simulator.Utils.csproj
@@ -23,8 +23,10 @@
 
   <ItemGroup>
     <PackageReference Include="Cognite.ExtractorUtils" Version="1.29.0" />
+    <PackageReference Include="Microsoft.Bcl.TimeProvider" Version="9.0.2" />
     <!-- <ProjectReference Include="..\..\dotnet-extractor-utils\ExtractorUtils\ExtractorUtils.csproj" /> -->
     <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
+    <PackageReference Include="System.Management" Version="9.0.2" />
   </ItemGroup>
 
   <ItemGroup>
diff --git a/Cognite.Simulator.Utils/CommonUtils.cs b/Cognite.Simulator.Utils/CommonUtils.cs
index b8cb8244..f8220372 100644
--- a/Cognite.Simulator.Utils/CommonUtils.cs
+++ b/Cognite.Simulator.Utils/CommonUtils.cs
@@ -31,6 +31,23 @@ public static string GetAssemblyVersion()
                     "0.0.1");
         }
 
+
+        /// <summary>
+        /// Format a duration of a TimeSpan in a human readable format
+        /// </summary>
+        /// <param name="duration">Duration to format</param>
+        /// <returns>Formatted duration</returns>
+        public static string FormatDuration(TimeSpan duration)
+        {
+            if (duration.TotalDays >= 1)
+                return $"{duration.TotalDays:N1} days";
+            if (duration.TotalHours >= 1)
+                return $"{duration.TotalHours:N1} hours";
+            if (duration.TotalMinutes >= 1)
+                return $"{duration.TotalMinutes:N1} minutes";
+            return $"{duration.TotalSeconds:N1} seconds";
+        }
+
         /// <summary>
         /// Run all of the tasks in this enumeration. If any fail or is canceled, cancel the
         /// remaining tasks and return. The first found exception is thrown
diff --git a/Cognite.Simulator.Utils/Configuration.cs b/Cognite.Simulator.Utils/Configuration.cs
index 55722144..6ae51b84 100644
--- a/Cognite.Simulator.Utils/Configuration.cs
+++ b/Cognite.Simulator.Utils/Configuration.cs
@@ -5,6 +5,7 @@
 using Cognite.Extractor.StateStorage;
 using Cognite.Extractor.Utils;
 using Cognite.Simulator.Utils.Automation;
+using Cognite.Extractor.Common;
 
 namespace Cognite.Simulator.Utils
 {
@@ -74,11 +75,6 @@ public override void GenerateDefaults()
                 Cognite = new CogniteConfig();
             }
 
-            if (Cognite.CdfRetries == null) {
-                Cognite.CdfRetries.MaxRetries = 11;
-                Cognite.CdfRetries.MaxDelay = 60 * 1000; //ms
-            }
-
             if (Metrics == null)
             {
                 Metrics = new MetricsConfig();
diff --git a/Cognite.Simulator.Utils/DefaultClasses/DefaultConnector.cs b/Cognite.Simulator.Utils/DefaultClasses/DefaultConnector.cs
index a71a78aa..931db0de 100644
--- a/Cognite.Simulator.Utils/DefaultClasses/DefaultConnector.cs
+++ b/Cognite.Simulator.Utils/DefaultClasses/DefaultConnector.cs
@@ -73,7 +73,7 @@ public override string GetSimulatorVersion(string simulator, CancellationToken t
 
         public override async Task Init(CancellationToken token)
         {
-
+            await _simulatorClient.TestConnection(token).ConfigureAwait(false);
             await _pipeline.Init(_config.Connector, token).ConfigureAwait(false);
             await InitRemoteSimulatorIntegration(token).ConfigureAwait(false);
             if(RemoteSimulatorIntegration != null){
@@ -110,8 +110,6 @@ public override async Task Run(CancellationToken token)
             {
                 using (var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token))
                 {
-                    
-                    await _simulatorClient.TestConnection(linkedTokenSource.Token).ConfigureAwait(false);
                     await RunAllTasks(linkedTokenSource).ConfigureAwait(false);
                 }
             }
@@ -135,6 +133,7 @@ public override async Task Run(CancellationToken token)
             catch (Exception ex)
             {
                 _logger.LogError(ex, "An error occurred during task execution");
+                throw;
             }
         }   
     }
diff --git a/Cognite.Simulator.Utils/DefaultClasses/DefaultConnectorRuntime.cs b/Cognite.Simulator.Utils/DefaultClasses/DefaultConnectorRuntime.cs
index 7fa56d80..c59e51dd 100644
--- a/Cognite.Simulator.Utils/DefaultClasses/DefaultConnectorRuntime.cs
+++ b/Cognite.Simulator.Utils/DefaultClasses/DefaultConnectorRuntime.cs
@@ -70,6 +70,7 @@ public static async Task RunStandalone()
                 catch (Exception e)
                 {
                     logger.LogError(e, "Unhandled exception: {message}", e.Message);
+                    throw;
                 }
             }
         }
diff --git a/Cognite.Simulator.Utils/LicenseController.cs b/Cognite.Simulator.Utils/LicenseController.cs
new file mode 100644
index 00000000..d7b098bb
--- /dev/null
+++ b/Cognite.Simulator.Utils/LicenseController.cs
@@ -0,0 +1,257 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Cognite.Simulator.Utils
+{
+
+    /// <summary>
+    /// Class to handle license acquisition and release.
+    /// With this a license can be acquired and released after a certain period of time.
+    /// </summary>
+    public class LicenseController : IDisposable
+    {
+        private string _controllerName;
+        private readonly TimeSpan _licenseLockTime;
+        private readonly object _lock = new object();
+        private ITimer _releaseTimer;
+        private readonly Action<CancellationToken> _releaseLicenseFunc;
+        private readonly Action<CancellationToken> _acquireLicenseFunc;
+        private bool _licenseHeld;
+        private DateTimeOffset _lastUsageTime;
+        private DateTimeOffset _scheduledReleaseTime;
+        private DateTimeOffset _acquisitionTime;
+        private readonly ILogger _logger;
+        private readonly TimeProvider _timeProvider;
+        private const int LoggingIntervalMs = 60000; // 1 minute
+        private readonly CancellationTokenSource _cts = new CancellationTokenSource();
+        private bool _timerPaused;
+
+        /// <summary>
+        /// Class to handle license acquisition and release.
+        /// When this is used in a using block, the license is released based on the time span provided after the block ends.
+        /// When the block ends a timer is started to release the license after the time span has elapsed.
+        /// </summary>
+        /// <param name="name">The name of the license controller instance.</param>
+        /// <param name="licenseLockTime">The time span for which the license is locked.</param>
+        /// <param name="releaseLicenseFunc">The function to release the license.</param>
+        /// <param name="acquireLicenseFunc">The function to acquire the license.</param>
+        /// <param name="logger">The logger instance for logging information.</param>
+        /// <param name="timeProvider">The time provider to use for all time-related operations.</param>
+        /// <param name="cancellationToken">The cancellation token to observe while waiting for the task to complete. Optional.</param>
+        /// <exception cref="ArgumentNullException">Thrown when <paramref name="releaseLicenseFunc"/> is null.</exception>
+        public LicenseController(
+            TimeSpan licenseLockTime,
+            Action<CancellationToken> releaseLicenseFunc,
+            Action<CancellationToken> acquireLicenseFunc,
+            ILogger logger,
+            TimeProvider timeProvider = null,
+            CancellationToken cancellationToken = new CancellationToken(),
+            string name = "DefaultLicenseController"
+        )
+        {
+            _controllerName = name;
+            _licenseLockTime = licenseLockTime;
+            _licenseHeld = false;
+            _releaseLicenseFunc = releaseLicenseFunc ?? throw new ArgumentNullException(nameof(releaseLicenseFunc));
+            _acquireLicenseFunc = acquireLicenseFunc ?? throw new ArgumentNullException(nameof(acquireLicenseFunc));
+            _logger = logger;
+            _timeProvider = timeProvider == null? TimeProvider.System : timeProvider;
+            _releaseTimer = _timeProvider.CreateTimer(ReleaseLicenseAfterTimeout, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
+            _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+            
+            // Dispose the timer on cancellation
+            _cts.Token.Register(() => {
+                _releaseTimer?.Dispose();
+                _releaseTimer = null;
+            }); 
+            _ = StartLoggingTask(_cts.Token);
+        }
+
+        /// <summary>
+        /// Gets a boolean indicating whether the license is currently held or not.
+        /// </summary>
+        public bool LicenseHeld => _licenseHeld;
+
+        private async Task StartLoggingTask(CancellationToken cancellationToken)
+        {
+            try
+            {
+                while (!cancellationToken.IsCancellationRequested)
+                {
+                    LogLicenseStatus();
+                    
+                    var tcs = new TaskCompletionSource<bool>();
+                    
+                    using var delayTimer = _timeProvider.CreateTimer(_ => tcs.TrySetResult(true), null,
+                        TimeSpan.FromMilliseconds(LoggingIntervalMs), Timeout.InfiniteTimeSpan);
+                    
+                    using var registration = cancellationToken.Register(() => tcs.TrySetCanceled());
+                    
+                    await tcs.Task.ConfigureAwait(false);
+                }
+            }
+            catch (OperationCanceledException)
+            {
+                // Task was cancelled, log final status
+                LogLicenseStatus();
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, $"{_controllerName}: Error in license status logging task");
+            }
+        }
+        
+        /// <summary>
+        /// Logs the current license status.
+        /// </summary>
+        public void LogLicenseStatus() {
+            _logger.LogInformation("{ControllerName}: License is currently {Status}", _controllerName, _licenseHeld ? "held" : "not held");
+            if (_licenseHeld) {
+                var duration = _timeProvider.GetUtcNow() - _lastUsageTime;
+                _logger.LogInformation("{ControllerName}: Duration of last license usage : {Duration} ", _controllerName, CommonUtils.FormatDuration(duration));
+                duration = _timeProvider.GetUtcNow() - _acquisitionTime;
+                if (_timerPaused) {
+                    _logger.LogInformation("{ControllerName}: License release timer paused", _controllerName);
+                }
+                else {
+                    _logger.LogInformation("{ControllerName}: License has been held for {Duration} ", _controllerName, CommonUtils.FormatDuration(duration));
+                    var timeUntilRelease = _scheduledReleaseTime - _timeProvider.GetUtcNow();
+                    var timeUntilReleaseFormatted = CommonUtils.FormatDuration(timeUntilRelease);
+                    var scheduledReleaseTimeFormatted = _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss");
+                    _logger.LogInformation("{ControllerName}: License will be released in {TimeUntilRelease} (at {ReleaseTime})",
+                        _controllerName, timeUntilReleaseFormatted, scheduledReleaseTimeFormatted);
+                }
+            }
+        }
+        
+        /// <summary>
+        /// Acquires a license by invoking the license acquisition function and sets the license status to held.
+        /// Also updates the release timer to ensure the license is released after a certain period.
+        /// </summary>
+        /// <exception cref="Exception">Thrown when an error occurs while acquiring the license.</exception>
+        public void AcquireLicense(CancellationToken cancellationToken) {
+            _logger.LogTrace($"{_controllerName}: Acquire license called, acquiring lock");
+            lock (_lock){
+                try
+                {
+                    if (_licenseHeld)
+                    {
+                        _logger.LogInformation($"{_controllerName}: License already held, skipping acquisition");
+                        return;
+                    }
+                    _logger.LogInformation($"{_controllerName}: Attempting to acquire license");
+                    _acquireLicenseFunc(cancellationToken);
+                    _licenseHeld = true;
+                    _acquisitionTime = _timeProvider.GetUtcNow();
+                    PauseTimer();
+                    _logger.LogInformation($"{_controllerName}: License acquired successfully");
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, $"{_controllerName}: Error acquiring license");
+                    throw;
+                }
+            }
+            _logger.LogTrace($"{_controllerName}: Acquire license completed, releasing lock");
+        }
+
+        /// <summary>
+        /// Releases the license after the timeout has elapsed. This is called by the timer.
+        /// </summary>
+        private void ReleaseLicenseAfterTimeout(object state)
+        {
+            _logger.LogTrace("{ControllerName}: Release license called by timer, acquiring lock", _controllerName);
+            lock (_lock)
+            {
+                _logger.LogInformation("{ControllerName}: Attempting to release license at scheduled time {ScheduledTime}", 
+                    _controllerName, _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss.fff"));
+                _logger.LogInformation("{ControllerName}: Current time {ScheduledTime}", 
+                    _controllerName, _timeProvider.GetUtcNow().ToString("yyyy-MM-dd HH:mm:ss.fff"));
+                _releaseLicenseFunc(_cts.Token);
+                _licenseHeld = false;  
+                _acquisitionTime = DateTimeOffset.MinValue;
+                _logger.LogInformation("{ControllerName}: License released successfully", _controllerName);
+            }
+            _logger.LogTrace("{ControllerName}: Release license completed, releasing lock", _controllerName);
+        }
+
+        /// <summary>
+        /// Releases the license immediately. This is used externally to reset the internal state when an external process finds out
+        /// that the license is no longer held.
+        /// </summary>
+        public void ClearLicenseState() {
+            _licenseHeld = false;
+            _lastUsageTime = _timeProvider.GetUtcNow();
+            _scheduledReleaseTime = DateTimeOffset.MaxValue;
+            _acquisitionTime = DateTimeOffset.MinValue;
+            _releaseTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
+            _logger.LogInformation($"{_controllerName}: License released forcefully");
+        }
+
+        private void PauseTimer() {
+            _logger.LogInformation($"{_controllerName}: License usage started: pausing release timer");
+            _releaseTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
+            _timerPaused = true;
+        }
+
+        /// <summary>
+        /// Begins usage of the license.
+        /// </summary>
+        /// <returns>An <see cref="IDisposable"/> object that can be used to end the license usage.</returns>
+        public IDisposable BeginUsage()
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation($"{_controllerName}: Starting license usage");
+                PauseTimer();
+                return new LicenseUsageScope(this);
+            }
+        }
+
+        private void EndUsage()
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("{ControllerName}: Ending license usage, scheduling release", _controllerName);
+                _lastUsageTime = _timeProvider.GetUtcNow();
+                _scheduledReleaseTime = _lastUsageTime.Add(_licenseLockTime);
+                _logger.LogInformation("{ControllerName}: License release scheduled for {ReleaseTime}", 
+                    _controllerName, _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+                _releaseTimer.Change(_licenseLockTime, Timeout.InfiniteTimeSpan);
+                _timerPaused = false;
+            }
+        }
+
+
+        /// <summary>
+        /// Disposes the license controller and releases the timer.
+        /// </summary>
+        public void Dispose()
+        {
+            _releaseTimer?.Dispose();
+            _releaseTimer = null;
+        }
+
+        private class LicenseUsageScope : IDisposable
+        {
+            private readonly LicenseController _tracker;
+            private bool _disposed;
+
+            public LicenseUsageScope(LicenseController tracker)
+            {
+                _tracker = tracker;
+            }
+
+            public void Dispose()
+            {
+                if (!_disposed)
+                {
+                    _tracker.EndUsage();
+                    _disposed = true;
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/Cognite.Simulator.Utils/ProcessUtils.cs b/Cognite.Simulator.Utils/ProcessUtils.cs
new file mode 100644
index 00000000..aa9b6d57
--- /dev/null
+++ b/Cognite.Simulator.Utils/ProcessUtils.cs
@@ -0,0 +1,100 @@
+using System.Linq;
+using System;
+using Microsoft.Extensions.Logging;
+using System.Diagnostics;
+using System.Collections.Generic;
+using System.Runtime.Versioning;
+using Cognite.Simulator.Utils.Automation;
+using Cognite.Simulator.Utils;
+using System.Threading;
+using System.Globalization;
+using System.Management;
+using System.Security.Principal;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+
+namespace Cognite.Simulator.Utils
+{
+     public static class ProcessUtils {
+         
+
+        /// <summary>
+        /// Get the owner of a process using WMI
+        /// </summary>
+        /// <param name="processId">The process ID</param>
+        /// <returns>The owner of the process</returns>
+        /// <exception cref="PlatformNotSupportedException">Thrown when the method is called on a non-Windows platform</exception>
+        /// <exception cref="Exception">Thrown when the process is not found or access is denied</exception>
+        public static string GetProcessOwnerWmi(int processId)
+        {
+            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
+                throw new PlatformNotSupportedException("This method is only supported on Windows");
+            }
+
+            string query = $"SELECT * FROM Win32_Process WHERE ProcessId = {processId}";
+            using (var searcher = new ManagementObjectSearcher(query))
+            using (var results = searcher.Get())
+            {
+                foreach (ManagementObject process in results)
+                {
+                    object[] args = new object[] { "", "" };
+                    if (Convert.ToInt32(process.InvokeMethod("GetOwner", args)) == 0)
+                    {
+                        string user = (string)args[0];
+                        string domain = (string)args[1];
+                        return $"{domain}\\{user}";
+                    }
+                }
+            }
+
+            throw new Exception("Process not found");
+        }
+        
+        private static void KillProcessUnsafe(string processId, ILogger logger) {
+            Process[] processes = Process.GetProcessesByName(processId);
+            logger.LogDebug("Searching for process : " + processId);
+            logger.LogDebug("Found {Count} matching processes", processes.Length);
+            bool found = false;
+    
+            foreach (Process process in processes) {
+                string owner = GetProcessOwnerWmi(process.Id);
+                logger.LogDebug($"Found process . Process owner is : {owner.ToLower()} . Current user is : {GetCurrentUsername().ToLower()}");
+        
+                if (owner.ToLower() == GetCurrentUsername().ToLower()) {
+                    logger.LogInformation("Killing process with PID {PID}", process.Id);
+                    process.Kill();
+                    process.WaitForExit();
+                    found = true;
+                } else {
+                    logger.LogWarning("Process with PID {PID} is owned by a different user ({Owner}). Skipping.", process.Id, owner);
+                }
+            }
+    
+            if (!found) {
+                throw new Exception("No processes found to kill for the current user");
+            }
+        }
+
+        /// <summary>
+        /// Kill a process by ID. Can throw exceptions if it is unable to kill the process or if it cannot find the process owner.
+        /// </summary>
+        /// <param name="processId"></param>
+        /// <param name="logger"></param>
+        public static void KillProcess(string processId, ILogger logger) {
+            try {
+                KillProcessUnsafe(processId, logger);
+            } catch (Exception e) {
+                logger.LogError("Failed to kill process: {Message}", e.Message);
+                throw;
+            }
+        }
+
+        public static string GetCurrentUsername()
+        {
+            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
+                throw new PlatformNotSupportedException("This method is only supported on Windows");
+            }
+            return WindowsIdentity.GetCurrent().Name;
+        }
+    }
+}
diff --git a/version b/version
index 0e0b079c..99617105 100644
--- a/version
+++ b/version
@@ -1 +1 @@
-1.0.0-beta-014
+1.0.0-beta-015


Reviews:
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-21T09:44:11Z
user: login: polomani
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-24T12:46:47Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-25T07:44:05Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-27T09:16:32Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-27T09:22:41Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-27T09:30:23Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-27T09:30:50Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-27T09:32:29Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-27T09:36:23Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-27T09:40:18Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-27T09:40:56Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-27T09:51:37Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-27T09:52:50Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-27T09:54:33Z
user: login: polomani
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-27T13:17:45Z
user: login: polomani
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-27T13:18:35Z
user: login: polomani
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-27T13:24:58Z
user: login: polomani
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-27T13:28:07Z
user: login: polomani
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-27T14:07:22Z
user: login: polomani
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-27T14:08:50Z
user: login: polomani
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-27T14:13:09Z
user: login: polomani
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-27T14:15:21Z
user: login: polomani
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-27T14:18:29Z
user: login: polomani
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-27T14:30:49Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-28T08:44:15Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-28T11:03:33Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-28T11:03:49Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-28T11:04:07Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-28T11:06:35Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-28T11:06:41Z
user: login: sagar-thalwar
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-28T11:39:40Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-28T11:46:09Z
user: login: polomani
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T09:31:06Z
user: login: polomani
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T09:33:36Z
user: login: polomani
body: we can't merge this without code coverage

<img width="603" alt="Screenshot 2025-03-03 at 10 41 38" src="https://github.com/user-attachments/assets/331791de-e7b0-4c8c-aee5-037ea3e4c582" />

state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T09:34:06Z
user: login: polomani
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T09:45:13Z
user: login: polomani
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T09:50:01Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T10:14:32Z
user: login: polomani
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T13:35:21Z
user: login: polomani
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T13:38:18Z
user: login: polomani
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T13:46:15Z
user: login: polomani
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T13:48:04Z
user: login: polomani
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T13:49:39Z
user: login: polomani
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T13:53:49Z
user: login: polomani
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T13:54:58Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T14:10:00Z
user: login: polomani
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T14:13:04Z
user: login: polomani
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T14:13:43Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T14:46:55Z
user: login: polomani
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-03-03T15:00:18Z
user: login: polomani
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T12:32:53Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T09:38:21Z
user: login: polomani
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T10:17:42Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T10:20:05Z
user: login: polomani
body: looks better than before
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-03-06T11:02:20Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T12:32:09Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T12:34:05Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-07T09:43:11Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-07T09:43:14Z
user: login: dbrattli
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-07T14:04:13Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-07T14:08:08Z
user: login: finnag
body: NT - just leaving this here to make the PR disappear from my risk review dashboard.
Dag is doing the risk review.
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-07T15:31:06Z
user: login: dbrattli
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-07T15:48:39Z
user: login: dbrattli
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-07T15:51:50Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T10:05:46Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T10:06:16Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T10:08:26Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T10:08:32Z
user: login: polomani
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-10T10:38:35Z
user: login: dbrattli
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-10T13:41:19Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-10T15:08:24Z
user: login: dbrattli
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-10T17:37:49Z
user: login: dbrattli
body: :unicorn:
state: APPROVED
author_association: NONE
submitted_at: 2025-03-10T17:53:40Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-11T09:07:07Z
user: login: abdullah-cognite
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-11T09:26:24Z

Review comments:
diff_hunk: @@ -70,6 +70,7 @@ public static async Task RunStandalone()
                 catch (Exception e)
                 {
                     logger.LogError(e, "Unhandled exception: {message}", e.Message);
+                    throw;
path: Cognite.Simulator.Utils/DefaultClasses/DefaultConnectorRuntime.cs
user: login: abdullah-cognite
body: this was the bug, the exception was being eaten up
created_at: 2025-02-21T09:44:11Z
author_association: COLLABORATOR
diff_hunk: @@ -70,6 +70,7 @@ public static async Task RunStandalone()
                 catch (Exception e)
                 {
                     logger.LogError(e, "Unhandled exception: {message}", e.Message);
+                    throw;
path: Cognite.Simulator.Utils/DefaultClasses/DefaultConnectorRuntime.cs
user: login: polomani
body: let's do the same with the other place https://github.com/cognitedata/dotnet-simulator-utils/commit/ffbd236a18d849da5ee2762c34de3eb92d01ac12#diff-d3f097be5eb10746d715e992e40f702614c001eda5855955464f6e236a2fcfe1R137
created_at: 2025-02-24T12:46:47Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,177 @@
+using Xunit;
+using System;
+using System.Threading;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Cognite.Simulator.Utils;
+
+namespace Cognite.Simulator.Tests.UtilsTests{
+
+    public enum TestLicenseState
+    {
+        Released,
+        Held
+    }
+
+    public class LicenseTrackerTests
path: Cognite.Simulator.Tests/UtilsTests/LicenseControllerTest.cs
user: login: abdullah-cognite
body: Not satisfied with the quality of this test yet, need to improve it
created_at: 2025-02-25T07:44:05Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,226 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Cognite.Simulator.Utils
+{
+    /// <summary>
+    /// Class to handle license acquisition and release.
+    /// With this a license can be acquired and released after a certain period of time.
+    /// </summary>
+   public class LicenseController : IDisposable
+    {
+        private readonly TimeSpan _licenseLockTime;
+        private readonly object _lock = new object();
+        private Timer _releaseTimer;
+        private readonly Func<object> _releaseLicenseFunc;
+        private readonly Func<object> _acquireLicenseFunc;
+        private bool _licenseHeld;
+        private DateTime _lastUsageTime;
+        private DateTime _scheduledReleaseTime;
+        private readonly ILogger _logger;
+        private const int LoggingIntervalMs = 60000; // 1 minute
+        private readonly CancellationTokenSource _cts = new CancellationTokenSource();
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="LicenseController"/> class.
+        /// </summary>
+        /// <param name="licenseLockTime">The time span for which the license is locked.</param>
+        /// <param name="releaseLicenseFunc">The function to release the license.</param>
+        /// <param name="acquireLicenseFunc">The function to acquire the license.</param>
+        /// <param name="logger">The logger instance for logging information.</param>
+        /// <param name="cancellationToken">The cancellation token to observe while waiting for the task to complete. Optional.</param>
+        /// <exception cref="ArgumentNullException">Thrown when <paramref name="releaseLicenseFunc"/> is null.</exception>
+        public LicenseController(
+            TimeSpan licenseLockTime,
+            Func<object> releaseLicenseFunc,
+            Func<object> acquireLicenseFunc,
+            ILogger logger,
+            CancellationToken cancellationToken = new CancellationToken()
+        )
+        {
+            _licenseLockTime = licenseLockTime;
+            _licenseHeld = false;
+            _releaseLicenseFunc = releaseLicenseFunc ?? throw new ArgumentNullException(nameof(releaseLicenseFunc));
+            _acquireLicenseFunc = acquireLicenseFunc ?? throw new ArgumentNullException(nameof(acquireLicenseFunc));
+            _releaseTimer = new Timer(ReleaseLicenseAfterTimeout, null, Timeout.Infinite, Timeout.Infinite);
+            _logger = logger;
+            _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+            _ = StartLoggingTask(_cts.Token);
+        }
+
+        /// <summary>
+        /// Gets a boolean indicating whether the license is currently held or not.
+        /// </summary>
+        public bool LicenseHeld => _licenseHeld;
+
+        private async Task StartLoggingTask(CancellationToken cancellationToken)
+        {
+            try
+            {
+                while (!cancellationToken.IsCancellationRequested)
+                {
+                    LogLicenseStatus();
+                    await Task.Delay(LoggingIntervalMs, cancellationToken).ConfigureAwait(false);
+                }
+            }
+            catch (OperationCanceledException)
+            {
+                // Task was cancelled, log final status
+                LogLicenseStatus();
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error in license status logging task");
+            }
+        }
+
+        private void LogLicenseStatus()
+        {
+            _logger.LogInformation("License is currently {Status}", _licenseHeld ? "held" : "not held");
+            if (_licenseHeld)
+            {
+                var duration = DateTime.Now - _lastUsageTime;
+                _logger.LogInformation("License has been held for {Duration}", CommonUtils.FormatDuration(duration));
+                var timeUntilRelease = _scheduledReleaseTime - DateTime.Now;
+                _logger.LogInformation("License will be released in {TimeUntilRelease} (at {ReleaseTime})", 
+                    CommonUtils.FormatDuration(timeUntilRelease),
+                    _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+            }
+        }
+        
+        /// <summary>
+        /// Acquires a license by invoking the license acquisition function and sets the license status to held.
+        /// Also updates the release timer to ensure the license is released after a certain period.
+        /// </summary>
+        /// <exception cref="Exception">Thrown when an error occurs while acquiring the license.</exception>
+        public void AcquireLicense() {
+            lock (_lock)
+            {
+                try
+                {
+                    if (_licenseHeld)
+                    {
+                        _logger.LogInformation("License already held, skipping acquisition");
+                        return;
+                    }
+                    _logger.LogInformation("Attempting to acquire license");
+                    _acquireLicenseFunc();
+                    _licenseHeld = true;
+                    UpdateReleaseTimer();
+                    _logger.LogInformation("License acquired successfully");
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, "Error acquiring license");
+                    throw;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license after the timeout has elapsed. This is called by the timer.
+        /// </summary>
+        private void ReleaseLicenseAfterTimeout(object state)
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Attempting to release license at scheduled time {ScheduledTime}", _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+                if (DateTime.Now >= _scheduledReleaseTime)
+                {
+                    _releaseLicenseFunc();
+                    _licenseHeld = false;  
+                    _logger.LogInformation("License released successfully");
+                }
+                else
+                {
+                    _logger.LogInformation("License not released - current time {CurrentTime} is before scheduled release time", 
+                        DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license immediately. This is used externally to reset the internal state when an external process finds out
+        /// that the license is no longer held.
+        /// </summary>
+        public void ReleaseLicenseWithoutCallback() {
+            lock (_lock)
+            {
+                _licenseHeld = false;
+                _lastUsageTime = DateTime.Now;
+                _scheduledReleaseTime = DateTime.Now;
+                _releaseTimer.Change(Timeout.Infinite, Timeout.Infinite);
+                _logger.LogInformation("License released without callback");
+            }
+        }
+
+        private void UpdateReleaseTimer() {
+            _lastUsageTime = DateTime.Now;
+            // Extend release time
+            _scheduledReleaseTime = DateTime.Now.Add(_licenseLockTime);
+            _logger.LogInformation("License release scheduled for {ReleaseTime}", 
+            _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+            _releaseTimer.Change(Timeout.Infinite, Timeout.Infinite);
+        }
+
+        /// <summary>
+        /// Begins usage of the license.
+        /// </summary>
+        /// <returns>An <see cref="IDisposable"/> object that can be used to end the license usage.</returns>
+        public IDisposable BeginUsage()
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Starting license usage");
+                UpdateReleaseTimer();
+                return new LicenseUsageScope(this);
+            }
+        }
+
+        private void EndUsage()
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Ending license usage, scheduling release");
+                _lastUsageTime = DateTime.Now;
+                _scheduledReleaseTime = _lastUsageTime.Add(_licenseLockTime);
+                _logger.LogInformation("License release scheduled for {ReleaseTime}", 
+                    _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+                _releaseTimer.Change(_licenseLockTime, Timeout.InfiniteTimeSpan);
path: Cognite.Simulator.Utils/LicenseController.cs
user: login: abdullah-cognite
body: It can just call UpdateReleaseTimer(); here
created_at: 2025-02-27T09:16:32Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,226 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Cognite.Simulator.Utils
+{
+    /// <summary>
+    /// Class to handle license acquisition and release.
+    /// With this a license can be acquired and released after a certain period of time.
+    /// </summary>
+   public class LicenseController : IDisposable
+    {
+        private readonly TimeSpan _licenseLockTime;
+        private readonly object _lock = new object();
+        private Timer _releaseTimer;
+        private readonly Func<object> _releaseLicenseFunc;
+        private readonly Func<object> _acquireLicenseFunc;
+        private bool _licenseHeld;
+        private DateTime _lastUsageTime;
+        private DateTime _scheduledReleaseTime;
+        private readonly ILogger _logger;
+        private const int LoggingIntervalMs = 60000; // 1 minute
+        private readonly CancellationTokenSource _cts = new CancellationTokenSource();
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="LicenseController"/> class.
+        /// </summary>
+        /// <param name="licenseLockTime">The time span for which the license is locked.</param>
+        /// <param name="releaseLicenseFunc">The function to release the license.</param>
+        /// <param name="acquireLicenseFunc">The function to acquire the license.</param>
+        /// <param name="logger">The logger instance for logging information.</param>
+        /// <param name="cancellationToken">The cancellation token to observe while waiting for the task to complete. Optional.</param>
+        /// <exception cref="ArgumentNullException">Thrown when <paramref name="releaseLicenseFunc"/> is null.</exception>
+        public LicenseController(
+            TimeSpan licenseLockTime,
+            Func<object> releaseLicenseFunc,
+            Func<object> acquireLicenseFunc,
+            ILogger logger,
+            CancellationToken cancellationToken = new CancellationToken()
+        )
+        {
+            _licenseLockTime = licenseLockTime;
+            _licenseHeld = false;
+            _releaseLicenseFunc = releaseLicenseFunc ?? throw new ArgumentNullException(nameof(releaseLicenseFunc));
+            _acquireLicenseFunc = acquireLicenseFunc ?? throw new ArgumentNullException(nameof(acquireLicenseFunc));
+            _releaseTimer = new Timer(ReleaseLicenseAfterTimeout, null, Timeout.Infinite, Timeout.Infinite);
+            _logger = logger;
+            _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+            _ = StartLoggingTask(_cts.Token);
+        }
+
+        /// <summary>
+        /// Gets a boolean indicating whether the license is currently held or not.
+        /// </summary>
+        public bool LicenseHeld => _licenseHeld;
+
+        private async Task StartLoggingTask(CancellationToken cancellationToken)
+        {
+            try
+            {
+                while (!cancellationToken.IsCancellationRequested)
+                {
+                    LogLicenseStatus();
+                    await Task.Delay(LoggingIntervalMs, cancellationToken).ConfigureAwait(false);
+                }
+            }
+            catch (OperationCanceledException)
+            {
+                // Task was cancelled, log final status
+                LogLicenseStatus();
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error in license status logging task");
+            }
+        }
+
+        private void LogLicenseStatus()
+        {
+            _logger.LogInformation("License is currently {Status}", _licenseHeld ? "held" : "not held");
+            if (_licenseHeld)
+            {
+                var duration = DateTime.Now - _lastUsageTime;
+                _logger.LogInformation("License has been held for {Duration}", CommonUtils.FormatDuration(duration));
+                var timeUntilRelease = _scheduledReleaseTime - DateTime.Now;
+                _logger.LogInformation("License will be released in {TimeUntilRelease} (at {ReleaseTime})", 
+                    CommonUtils.FormatDuration(timeUntilRelease),
+                    _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+            }
+        }
+        
+        /// <summary>
+        /// Acquires a license by invoking the license acquisition function and sets the license status to held.
+        /// Also updates the release timer to ensure the license is released after a certain period.
+        /// </summary>
+        /// <exception cref="Exception">Thrown when an error occurs while acquiring the license.</exception>
+        public void AcquireLicense() {
+            lock (_lock)
+            {
+                try
+                {
+                    if (_licenseHeld)
+                    {
+                        _logger.LogInformation("License already held, skipping acquisition");
+                        return;
+                    }
+                    _logger.LogInformation("Attempting to acquire license");
+                    _acquireLicenseFunc();
+                    _licenseHeld = true;
+                    UpdateReleaseTimer();
+                    _logger.LogInformation("License acquired successfully");
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, "Error acquiring license");
+                    throw;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license after the timeout has elapsed. This is called by the timer.
+        /// </summary>
+        private void ReleaseLicenseAfterTimeout(object state)
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Attempting to release license at scheduled time {ScheduledTime}", _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+                if (DateTime.Now >= _scheduledReleaseTime)
+                {
+                    _releaseLicenseFunc();
+                    _licenseHeld = false;  
+                    _logger.LogInformation("License released successfully");
+                }
+                else
+                {
+                    _logger.LogInformation("License not released - current time {CurrentTime} is before scheduled release time", 
+                        DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license immediately. This is used externally to reset the internal state when an external process finds out
+        /// that the license is no longer held.
+        /// </summary>
+        public void ReleaseLicenseWithoutCallback() {
+            lock (_lock)
+            {
+                _licenseHeld = false;
+                _lastUsageTime = DateTime.Now;
+                _scheduledReleaseTime = DateTime.Now;
+                _releaseTimer.Change(Timeout.Infinite, Timeout.Infinite);
+                _logger.LogInformation("License released without callback");
+            }
+        }
+
+        private void UpdateReleaseTimer() {
+            _lastUsageTime = DateTime.Now;
+            // Extend release time
+            _scheduledReleaseTime = DateTime.Now.Add(_licenseLockTime);
+            _logger.LogInformation("License release scheduled for {ReleaseTime}", 
+            _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+            _releaseTimer.Change(Timeout.Infinite, Timeout.Infinite);
+        }
+
+        /// <summary>
+        /// Begins usage of the license.
+        /// </summary>
+        /// <returns>An <see cref="IDisposable"/> object that can be used to end the license usage.</returns>
+        public IDisposable BeginUsage()
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Starting license usage");
+                UpdateReleaseTimer();
+                return new LicenseUsageScope(this);
+            }
+        }
+
+        private void EndUsage()
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Ending license usage, scheduling release");
+                _lastUsageTime = DateTime.Now;
+                _scheduledReleaseTime = _lastUsageTime.Add(_licenseLockTime);
+                _logger.LogInformation("License release scheduled for {ReleaseTime}", 
+                    _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+                _releaseTimer.Change(_licenseLockTime, Timeout.InfiniteTimeSpan);
+            }
+        }
+
+        /// <summary>
+        /// Releases the license immediately.
+        /// </summary>
+        public void Dispose()
+        {
+            _releaseTimer?.Dispose();
+            _releaseTimer = null;
+            GC.SuppressFinalize(this);
path: Cognite.Simulator.Utils/LicenseController.cs
user: login: abdullah-cognite
body: Investigate if this is actually needed or not
created_at: 2025-02-27T09:22:40Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,75 @@
+using System.Linq;
+using System;
+using Microsoft.Extensions.Logging;
+using System.Diagnostics;
+using System.Collections.Generic;
+using System.Runtime.Versioning;
+using Cognite.Simulator.Utils.Automation;
+using Cognite.Simulator.Utils;
+using System.Threading;
+using System.Globalization;
+using System.Management;
+using System.Security.Principal;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Cognite.Simulator.Utils
+{
+     public static class ProcessUtils {
+        public static string GetProcessOwnerWmi(int processId)
+        {
+            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
+                throw new PlatformNotSupportedException("This method is only supported on Windows");
+            }
+            string query = $"SELECT * FROM Win32_Process WHERE ProcessId = {processId}";
+            using (var searcher = new ManagementObjectSearcher(query))
+            using (var results = searcher.Get())
+            {
+                foreach (ManagementObject process in results)
+                {
+                    try
+                    {
+                        object[] args = new object[] { "", "" };
+                        if (Convert.ToInt32(process.InvokeMethod("GetOwner", args)) == 0)
+                        {
+                            string user = (string)args[0];
+                            string domain = (string)args[1];
+                            return $"{domain}\\{user}";
+                        }
+                    }
+                    catch
+                    {
+                        return "Access Denied or Process Exited";
+                    }
+                }
+            }
+            return "No Owner Found";
path: Cognite.Simulator.Utils/ProcessUtils.cs
user: login: abdullah-cognite
body: return N/A
created_at: 2025-02-27T09:30:23Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,75 @@
+using System.Linq;
+using System;
+using Microsoft.Extensions.Logging;
+using System.Diagnostics;
+using System.Collections.Generic;
+using System.Runtime.Versioning;
+using Cognite.Simulator.Utils.Automation;
+using Cognite.Simulator.Utils;
+using System.Threading;
+using System.Globalization;
+using System.Management;
+using System.Security.Principal;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Cognite.Simulator.Utils
+{
+     public static class ProcessUtils {
+        public static string GetProcessOwnerWmi(int processId)
+        {
+            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
+                throw new PlatformNotSupportedException("This method is only supported on Windows");
+            }
+            string query = $"SELECT * FROM Win32_Process WHERE ProcessId = {processId}";
+            using (var searcher = new ManagementObjectSearcher(query))
+            using (var results = searcher.Get())
+            {
+                foreach (ManagementObject process in results)
+                {
+                    try
+                    {
+                        object[] args = new object[] { "", "" };
+                        if (Convert.ToInt32(process.InvokeMethod("GetOwner", args)) == 0)
+                        {
+                            string user = (string)args[0];
+                            string domain = (string)args[1];
+                            return $"{domain}\\{user}";
+                        }
+                    }
+                    catch
+                    {
+                        return "Access Denied or Process Exited";
path: Cognite.Simulator.Utils/ProcessUtils.cs
user: login: abdullah-cognite
body: return N/A
created_at: 2025-02-27T09:30:50Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,75 @@
+using System.Linq;
+using System;
+using Microsoft.Extensions.Logging;
+using System.Diagnostics;
+using System.Collections.Generic;
+using System.Runtime.Versioning;
+using Cognite.Simulator.Utils.Automation;
+using Cognite.Simulator.Utils;
+using System.Threading;
+using System.Globalization;
+using System.Management;
+using System.Security.Principal;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Cognite.Simulator.Utils
+{
+     public static class ProcessUtils {
+        public static string GetProcessOwnerWmi(int processId)
path: Cognite.Simulator.Utils/ProcessUtils.cs
user: login: abdullah-cognite
body: ```suggestion
<exception>No owner found </exception>
        public static string GetProcessOwnerWmi(int processId)
```
created_at: 2025-02-27T09:32:29Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,176 @@
+using Xunit;
+using System;
+using System.Threading;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Cognite.Simulator.Utils;
+
+namespace Cognite.Simulator.Tests.UtilsTests{
+
+    public enum TestLicenseState
+    {
+        Released,
+        Held
+    }
+
+    public class LicenseTrackerTests
+    {
+        private readonly Mock<ILogger> _loggerMock = new Mock<ILogger>();
+
+        private (LicenseController controller, StateHolder<TestLicenseState> stateHolder) CreateTracker(
+            Mock<Func<object>>? releaseMock = null,
+            Mock<Func<object>>? acquireMock = null,
+            TimeSpan? licenseLockTime = null)
+        {
+            var stateHolder = new StateHolder<TestLicenseState> { State = TestLicenseState.Released };
+            
+            releaseMock ??= new Mock<Func<object>>();
+            acquireMock ??= new Mock<Func<object>>();
+            
+            // Set up the mocks to track the state changes
+            releaseMock.Setup(m => m()).Callback(() => stateHolder.State = TestLicenseState.Released);
+            acquireMock.Setup(m => m()).Callback(() => stateHolder.State = TestLicenseState.Held);
+            
+            var tracker = new LicenseController(
+                licenseLockTime ?? TimeSpan.FromMilliseconds(100),
+                () => { releaseMock.Object(); return null; },
+                () => { acquireMock.Object(); return null; },
+                _loggerMock.Object
+            );
+            
+            return (tracker, stateHolder);
+        }
+
+        // Simple class to hold state that can be captured in lambdas
+        private class StateHolder<T>
+        {
+            public T State { get; set; }
+        }
+
+
+        [Fact]
+        public void LicenseTracker_ShouldReleaseLicense_WhenNotInUse()
+        {
+
+            // Arrange
+            Mock<Func<object>> _releaseLicenseFuncMock = new Mock<Func<object>>();
+            Mock<Func<object>> _acquireLicenseFuncMock = new Mock<Func<object>>();
+            var licenseLockTime = TimeSpan.FromMilliseconds(100);
+            var (tracker, license) = CreateTracker( _releaseLicenseFuncMock, _acquireLicenseFuncMock, licenseLockTime);
+
+            // Check if the default state of the license is released
+            Assert.Equal(TestLicenseState.Released, license.State);
+
+            // Check if acquiring the license calls the acquire license function and changes the state
+            tracker.AcquireLicense();
+            _acquireLicenseFuncMock.Verify(f => f(), Times.Once);
+            Assert.Equal(TestLicenseState.Held, license.State);
+            Assert.True(tracker.LicenseHeld);
+
+            using (tracker.BeginUsage()) { /* Use and immediately release */ } 
+            
+            // Add buffer time to account for timer inconsistencies
+            Thread.Sleep(250);
+            
+            // Check if the release license callback was called
+            _releaseLicenseFuncMock.Verify(f => f(), Times.Once);
+
+            // Check if the license is released
+            Assert.Equal(TestLicenseState.Released, license.State);
+            Assert.False(tracker.LicenseHeld);
+            tracker.Dispose();
+        }
+
+        [Fact]
+        public void LicenseTracker_ShouldNotRelease_WhileInUse()
+        {
+            // Arrange
+            var licenseLockTime = TimeSpan.FromMilliseconds(100);
+            var (tracker, license) = CreateTracker();
path: Cognite.Simulator.Tests/UtilsTests/LicenseControllerTest.cs
user: login: abdullah-cognite
body: licenseLockTime is not passed to tracker
created_at: 2025-02-27T09:36:23Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,226 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Cognite.Simulator.Utils
+{
+    /// <summary>
+    /// Class to handle license acquisition and release.
+    /// With this a license can be acquired and released after a certain period of time.
+    /// </summary>
+   public class LicenseController : IDisposable
+    {
+        private readonly TimeSpan _licenseLockTime;
+        private readonly object _lock = new object();
+        private Timer _releaseTimer;
+        private readonly Func<object> _releaseLicenseFunc;
+        private readonly Func<object> _acquireLicenseFunc;
+        private bool _licenseHeld;
+        private DateTime _lastUsageTime;
+        private DateTime _scheduledReleaseTime;
+        private readonly ILogger _logger;
+        private const int LoggingIntervalMs = 60000; // 1 minute
+        private readonly CancellationTokenSource _cts = new CancellationTokenSource();
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="LicenseController"/> class.
+        /// </summary>
+        /// <param name="licenseLockTime">The time span for which the license is locked.</param>
+        /// <param name="releaseLicenseFunc">The function to release the license.</param>
+        /// <param name="acquireLicenseFunc">The function to acquire the license.</param>
+        /// <param name="logger">The logger instance for logging information.</param>
+        /// <param name="cancellationToken">The cancellation token to observe while waiting for the task to complete. Optional.</param>
+        /// <exception cref="ArgumentNullException">Thrown when <paramref name="releaseLicenseFunc"/> is null.</exception>
+        public LicenseController(
+            TimeSpan licenseLockTime,
+            Func<object> releaseLicenseFunc,
+            Func<object> acquireLicenseFunc,
+            ILogger logger,
+            CancellationToken cancellationToken = new CancellationToken()
+        )
+        {
+            _licenseLockTime = licenseLockTime;
+            _licenseHeld = false;
+            _releaseLicenseFunc = releaseLicenseFunc ?? throw new ArgumentNullException(nameof(releaseLicenseFunc));
+            _acquireLicenseFunc = acquireLicenseFunc ?? throw new ArgumentNullException(nameof(acquireLicenseFunc));
+            _releaseTimer = new Timer(ReleaseLicenseAfterTimeout, null, Timeout.Infinite, Timeout.Infinite);
+            _logger = logger;
+            _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+            _ = StartLoggingTask(_cts.Token);
+        }
+
+        /// <summary>
+        /// Gets a boolean indicating whether the license is currently held or not.
+        /// </summary>
+        public bool LicenseHeld => _licenseHeld;
+
+        private async Task StartLoggingTask(CancellationToken cancellationToken)
+        {
+            try
+            {
+                while (!cancellationToken.IsCancellationRequested)
+                {
+                    LogLicenseStatus();
+                    await Task.Delay(LoggingIntervalMs, cancellationToken).ConfigureAwait(false);
+                }
+            }
+            catch (OperationCanceledException)
+            {
+                // Task was cancelled, log final status
+                LogLicenseStatus();
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error in license status logging task");
+            }
+        }
+
+        private void LogLicenseStatus()
+        {
+            _logger.LogInformation("License is currently {Status}", _licenseHeld ? "held" : "not held");
+            if (_licenseHeld)
+            {
+                var duration = DateTime.Now - _lastUsageTime;
+                _logger.LogInformation("License has been held for {Duration}", CommonUtils.FormatDuration(duration));
+                var timeUntilRelease = _scheduledReleaseTime - DateTime.Now;
+                _logger.LogInformation("License will be released in {TimeUntilRelease} (at {ReleaseTime})", 
+                    CommonUtils.FormatDuration(timeUntilRelease),
+                    _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+            }
+        }
+        
+        /// <summary>
+        /// Acquires a license by invoking the license acquisition function and sets the license status to held.
+        /// Also updates the release timer to ensure the license is released after a certain period.
+        /// </summary>
+        /// <exception cref="Exception">Thrown when an error occurs while acquiring the license.</exception>
+        public void AcquireLicense() {
+            lock (_lock)
+            {
+                try
+                {
+                    if (_licenseHeld)
+                    {
+                        _logger.LogInformation("License already held, skipping acquisition");
+                        return;
+                    }
+                    _logger.LogInformation("Attempting to acquire license");
+                    _acquireLicenseFunc();
+                    _licenseHeld = true;
+                    UpdateReleaseTimer();
+                    _logger.LogInformation("License acquired successfully");
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, "Error acquiring license");
+                    throw;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license after the timeout has elapsed. This is called by the timer.
+        /// </summary>
+        private void ReleaseLicenseAfterTimeout(object state)
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Attempting to release license at scheduled time {ScheduledTime}", _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+                if (DateTime.Now >= _scheduledReleaseTime)
+                {
+                    _releaseLicenseFunc();
+                    _licenseHeld = false;  
+                    _logger.LogInformation("License released successfully");
+                }
+                else
+                {
+                    _logger.LogInformation("License not released - current time {CurrentTime} is before scheduled release time", 
+                        DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license immediately. This is used externally to reset the internal state when an external process finds out
+        /// that the license is no longer held.
+        /// </summary>
+        public void ReleaseLicenseWithoutCallback() {
+            lock (_lock)
+            {
+                _licenseHeld = false;
+                _lastUsageTime = DateTime.Now;
+                _scheduledReleaseTime = DateTime.Now;
+                _releaseTimer.Change(Timeout.Infinite, Timeout.Infinite);
+                _logger.LogInformation("License released without callback");
+            }
+        }
+
+        private void UpdateReleaseTimer() {
+            _lastUsageTime = DateTime.Now;
+            // Extend release time
+            _scheduledReleaseTime = DateTime.Now.Add(_licenseLockTime);
+            _logger.LogInformation("License release scheduled for {ReleaseTime}", 
+            _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+            _releaseTimer.Change(Timeout.Infinite, Timeout.Infinite);
path: Cognite.Simulator.Utils/LicenseController.cs
user: login: abdullah-cognite
body: Add scheduleReleaseTime to the change time: _scheduledReleaseTime
created_at: 2025-02-27T09:40:18Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,226 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Cognite.Simulator.Utils
+{
+    /// <summary>
+    /// Class to handle license acquisition and release.
+    /// With this a license can be acquired and released after a certain period of time.
+    /// </summary>
+   public class LicenseController : IDisposable
+    {
+        private readonly TimeSpan _licenseLockTime;
+        private readonly object _lock = new object();
+        private Timer _releaseTimer;
+        private readonly Func<object> _releaseLicenseFunc;
+        private readonly Func<object> _acquireLicenseFunc;
+        private bool _licenseHeld;
+        private DateTime _lastUsageTime;
+        private DateTime _scheduledReleaseTime;
+        private readonly ILogger _logger;
+        private const int LoggingIntervalMs = 60000; // 1 minute
+        private readonly CancellationTokenSource _cts = new CancellationTokenSource();
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="LicenseController"/> class.
path: Cognite.Simulator.Utils/LicenseController.cs
user: login: abdullah-cognite
body: Give a description of how it works
created_at: 2025-02-27T09:40:56Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,176 @@
+using Xunit;
+using System;
+using System.Threading;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Cognite.Simulator.Utils;
+
+namespace Cognite.Simulator.Tests.UtilsTests{
+
+    public enum TestLicenseState
+    {
+        Released,
+        Held
+    }
+
+    public class LicenseTrackerTests
+    {
+        private readonly Mock<ILogger> _loggerMock = new Mock<ILogger>();
+
+        private (LicenseController controller, StateHolder<TestLicenseState> stateHolder) CreateTracker(
+            Mock<Func<object>>? releaseMock = null,
+            Mock<Func<object>>? acquireMock = null,
+            TimeSpan? licenseLockTime = null)
+        {
+            var stateHolder = new StateHolder<TestLicenseState> { State = TestLicenseState.Released };
+            
+            releaseMock ??= new Mock<Func<object>>();
+            acquireMock ??= new Mock<Func<object>>();
+            
+            // Set up the mocks to track the state changes
+            releaseMock.Setup(m => m()).Callback(() => stateHolder.State = TestLicenseState.Released);
+            acquireMock.Setup(m => m()).Callback(() => stateHolder.State = TestLicenseState.Held);
+            
+            var tracker = new LicenseController(
+                licenseLockTime ?? TimeSpan.FromMilliseconds(100),
+                () => { releaseMock.Object(); return null; },
+                () => { acquireMock.Object(); return null; },
+                _loggerMock.Object
+            );
+            
+            return (tracker, stateHolder);
+        }
+
+        // Simple class to hold state that can be captured in lambdas
+        private class StateHolder<T>
+        {
+            public T State { get; set; }
+        }
+
+
+        [Fact]
+        public void LicenseTracker_ShouldReleaseLicense_WhenNotInUse()
+        {
+
+            // Arrange
+            Mock<Func<object>> _releaseLicenseFuncMock = new Mock<Func<object>>();
+            Mock<Func<object>> _acquireLicenseFuncMock = new Mock<Func<object>>();
+            var licenseLockTime = TimeSpan.FromMilliseconds(100);
+            var (tracker, license) = CreateTracker( _releaseLicenseFuncMock, _acquireLicenseFuncMock, licenseLockTime);
+
+            // Check if the default state of the license is released
+            Assert.Equal(TestLicenseState.Released, license.State);
+
+            // Check if acquiring the license calls the acquire license function and changes the state
+            tracker.AcquireLicense();
+            _acquireLicenseFuncMock.Verify(f => f(), Times.Once);
+            Assert.Equal(TestLicenseState.Held, license.State);
+            Assert.True(tracker.LicenseHeld);
+
+            using (tracker.BeginUsage()) { /* Use and immediately release */ } 
+            
+            // Add buffer time to account for timer inconsistencies
+            Thread.Sleep(250);
+            
+            // Check if the release license callback was called
+            _releaseLicenseFuncMock.Verify(f => f(), Times.Once);
+
+            // Check if the license is released
+            Assert.Equal(TestLicenseState.Released, license.State);
+            Assert.False(tracker.LicenseHeld);
+            tracker.Dispose();
+        }
+
+        [Fact]
+        public void LicenseTracker_ShouldNotRelease_WhileInUse()
+        {
+            // Arrange
+            var licenseLockTime = TimeSpan.FromMilliseconds(100);
+            var (tracker, license) = CreateTracker();
+            
+            // Act
+            tracker.AcquireLicense();
+            Assert.Equal(TestLicenseState.Held, license.State);
+            Assert.True(tracker.LicenseHeld);
+            using (var usage = tracker.BeginUsage())
+            {
+                Thread.Sleep(200); // Wait longer than lock time
+                Assert.True(tracker.LicenseHeld); // Should not release while in use
+            }
+
+            tracker.Dispose();
+        }
+
+        [Fact]
+        public void LicenseTracker_ShouldResetTimer_OnNewUsage()
+        {
+            // Arrange
+            var licenseLockTime = TimeSpan.FromMilliseconds(100);
+            var (tracker, license) = CreateTracker(licenseLockTime: licenseLockTime);
+
+            // Act  
+            tracker.AcquireLicense();
+            Assert.Equal(TestLicenseState.Held, license.State);
+            using (tracker.BeginUsage()) { } // First usage
+            Thread.Sleep(50);
+            
+            using (tracker.BeginUsage()) { } // Second usage should reset timer
+            Thread.Sleep(50);
+            
+            // Assert
+            Assert.True(tracker.LicenseHeld); // Should not be released yet due to timer reset
+
+            Thread.Sleep(150); // Wait for full lock time + buffer
+            Assert.True(license.State == TestLicenseState.Released);
+            Assert.False(tracker.LicenseHeld);
+
+            tracker.Dispose();
+        }
+
+        [Fact]
+        public void LicenseTracker_ShouldPreventRelease_DuringContinuousUsage()
+        {
+            // Arrange
+            var (tracker, license) = CreateTracker();
+
+            // Act
+            tracker.AcquireLicense();
+            using (var usage1 = tracker.BeginUsage())
+            {
+                Thread.Sleep(50);
+                using (var usage2 = tracker.BeginUsage())
+                {
+                    Thread.Sleep(100);
+                    // Should not release during overlapping usage
+                    Assert.True(tracker.LicenseHeld);
+                }
+            }
+
+            Thread.Sleep(200); // Wait longer than lock time after all usage ends
path: Cognite.Simulator.Tests/UtilsTests/LicenseControllerTest.cs
user: login: abdullah-cognite
body: increase this value
created_at: 2025-02-27T09:51:36Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,176 @@
+using Xunit;
+using System;
+using System.Threading;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Cognite.Simulator.Utils;
+
+namespace Cognite.Simulator.Tests.UtilsTests{
+
+    public enum TestLicenseState
+    {
+        Released,
+        Held
+    }
+
+    public class LicenseTrackerTests
+    {
+        private readonly Mock<ILogger> _loggerMock = new Mock<ILogger>();
+
+        private (LicenseController controller, StateHolder<TestLicenseState> stateHolder) CreateTracker(
+            Mock<Func<object>>? releaseMock = null,
+            Mock<Func<object>>? acquireMock = null,
+            TimeSpan? licenseLockTime = null)
+        {
+            var stateHolder = new StateHolder<TestLicenseState> { State = TestLicenseState.Released };
+            
+            releaseMock ??= new Mock<Func<object>>();
+            acquireMock ??= new Mock<Func<object>>();
+            
+            // Set up the mocks to track the state changes
+            releaseMock.Setup(m => m()).Callback(() => stateHolder.State = TestLicenseState.Released);
+            acquireMock.Setup(m => m()).Callback(() => stateHolder.State = TestLicenseState.Held);
+            
+            var tracker = new LicenseController(
+                licenseLockTime ?? TimeSpan.FromMilliseconds(100),
+                () => { releaseMock.Object(); return null; },
+                () => { acquireMock.Object(); return null; },
+                _loggerMock.Object
+            );
+            
+            return (tracker, stateHolder);
+        }
+
+        // Simple class to hold state that can be captured in lambdas
+        private class StateHolder<T>
+        {
+            public T State { get; set; }
+        }
+
+
+        [Fact]
+        public void LicenseTracker_ShouldReleaseLicense_WhenNotInUse()
+        {
+
+            // Arrange
+            Mock<Func<object>> _releaseLicenseFuncMock = new Mock<Func<object>>();
+            Mock<Func<object>> _acquireLicenseFuncMock = new Mock<Func<object>>();
+            var licenseLockTime = TimeSpan.FromMilliseconds(100);
+            var (tracker, license) = CreateTracker( _releaseLicenseFuncMock, _acquireLicenseFuncMock, licenseLockTime);
+
+            // Check if the default state of the license is released
+            Assert.Equal(TestLicenseState.Released, license.State);
+
+            // Check if acquiring the license calls the acquire license function and changes the state
+            tracker.AcquireLicense();
+            _acquireLicenseFuncMock.Verify(f => f(), Times.Once);
+            Assert.Equal(TestLicenseState.Held, license.State);
+            Assert.True(tracker.LicenseHeld);
+
+            using (tracker.BeginUsage()) { /* Use and immediately release */ } 
+            
+            // Add buffer time to account for timer inconsistencies
+            Thread.Sleep(250);
+            
+            // Check if the release license callback was called
+            _releaseLicenseFuncMock.Verify(f => f(), Times.Once);
+
+            // Check if the license is released
+            Assert.Equal(TestLicenseState.Released, license.State);
+            Assert.False(tracker.LicenseHeld);
+            tracker.Dispose();
+        }
+
+        [Fact]
+        public void LicenseTracker_ShouldNotRelease_WhileInUse()
+        {
+            // Arrange
+            var licenseLockTime = TimeSpan.FromMilliseconds(100);
+            var (tracker, license) = CreateTracker();
+            
+            // Act
+            tracker.AcquireLicense();
+            Assert.Equal(TestLicenseState.Held, license.State);
+            Assert.True(tracker.LicenseHeld);
+            using (var usage = tracker.BeginUsage())
+            {
+                Thread.Sleep(200); // Wait longer than lock time
+                Assert.True(tracker.LicenseHeld); // Should not release while in use
+            }
+
+            tracker.Dispose();
+        }
+
+        [Fact]
+        public void LicenseTracker_ShouldResetTimer_OnNewUsage()
+        {
+            // Arrange
+            var licenseLockTime = TimeSpan.FromMilliseconds(100);
+            var (tracker, license) = CreateTracker(licenseLockTime: licenseLockTime);
+
+            // Act  
+            tracker.AcquireLicense();
+            Assert.Equal(TestLicenseState.Held, license.State);
+            using (tracker.BeginUsage()) { } // First usage
+            Thread.Sleep(50);
+            
+            using (tracker.BeginUsage()) { } // Second usage should reset timer
+            Thread.Sleep(50);
+            
+            // Assert
+            Assert.True(tracker.LicenseHeld); // Should not be released yet due to timer reset
+
+            Thread.Sleep(150); // Wait for full lock time + buffer
+            Assert.True(license.State == TestLicenseState.Released);
+            Assert.False(tracker.LicenseHeld);
+
+            tracker.Dispose();
+        }
+
+        [Fact]
+        public void LicenseTracker_ShouldPreventRelease_DuringContinuousUsage()
+        {
+            // Arrange
+            var (tracker, license) = CreateTracker();
+
+            // Act
+            tracker.AcquireLicense();
+            using (var usage1 = tracker.BeginUsage())
+            {
+                Thread.Sleep(50);
+                using (var usage2 = tracker.BeginUsage())
+                {
+                    Thread.Sleep(100);
+                    // Should not release during overlapping usage
+                    Assert.True(tracker.LicenseHeld);
+                }
+            }
+
+            Thread.Sleep(200); // Wait longer than lock time after all usage ends
+            
+            // Assert
+            Assert.True(license.State == TestLicenseState.Released);
+            tracker.Dispose();
+        }
+
+        [Fact]
+        public void LicenseTracker_ShouldUpdateState_WhenLicenseReleasedManually()
+        {
+            // Arrange
+            var (tracker, license) = CreateTracker();
+            tracker.AcquireLicense();
+            using (tracker.BeginUsage()) { }
+            Assert.True(tracker.LicenseHeld);
+
+            // Act
+            tracker.ReleaseLicenseWithoutCallback();
+            
+            // Assert
+            Assert.False(tracker.LicenseHeld);
+            // Should not change external license state (since this is a manual release)
+            Assert.True(license.State == TestLicenseState.Held);
path: Cognite.Simulator.Tests/UtilsTests/LicenseControllerTest.cs
user: login: abdullah-cognite
body: check if there's timer precision thing we could use
created_at: 2025-02-27T09:52:50Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,176 @@
+using Xunit;
+using System;
+using System.Threading;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Cognite.Simulator.Utils;
+
+namespace Cognite.Simulator.Tests.UtilsTests{
+
+    public enum TestLicenseState
+    {
+        Released,
+        Held
+    }
+
+    public class LicenseTrackerTests
+    {
+        private readonly Mock<ILogger> _loggerMock = new Mock<ILogger>();
+
+        private (LicenseController controller, StateHolder<TestLicenseState> stateHolder) CreateTracker(
+            Mock<Func<object>>? releaseMock = null,
+            Mock<Func<object>>? acquireMock = null,
+            TimeSpan? licenseLockTime = null)
+        {
+            var stateHolder = new StateHolder<TestLicenseState> { State = TestLicenseState.Released };
+            
+            releaseMock ??= new Mock<Func<object>>();
+            acquireMock ??= new Mock<Func<object>>();
+            
+            // Set up the mocks to track the state changes
+            releaseMock.Setup(m => m()).Callback(() => stateHolder.State = TestLicenseState.Released);
+            acquireMock.Setup(m => m()).Callback(() => stateHolder.State = TestLicenseState.Held);
+            
+            var tracker = new LicenseController(
+                licenseLockTime ?? TimeSpan.FromMilliseconds(100),
+                () => { releaseMock.Object(); return null; },
+                () => { acquireMock.Object(); return null; },
+                _loggerMock.Object
+            );
+            
+            return (tracker, stateHolder);
+        }
+
+        // Simple class to hold state that can be captured in lambdas
+        private class StateHolder<T>
+        {
+            public T State { get; set; }
+        }
+
+
+        [Fact]
+        public void LicenseTracker_ShouldReleaseLicense_WhenNotInUse()
+        {
+
+            // Arrange
+            Mock<Func<object>> _releaseLicenseFuncMock = new Mock<Func<object>>();
+            Mock<Func<object>> _acquireLicenseFuncMock = new Mock<Func<object>>();
+            var licenseLockTime = TimeSpan.FromMilliseconds(100);
+            var (tracker, license) = CreateTracker( _releaseLicenseFuncMock, _acquireLicenseFuncMock, licenseLockTime);
+
+            // Check if the default state of the license is released
+            Assert.Equal(TestLicenseState.Released, license.State);
+
+            // Check if acquiring the license calls the acquire license function and changes the state
+            tracker.AcquireLicense();
+            _acquireLicenseFuncMock.Verify(f => f(), Times.Once);
+            Assert.Equal(TestLicenseState.Held, license.State);
+            Assert.True(tracker.LicenseHeld);
+
+            using (tracker.BeginUsage()) { /* Use and immediately release */ } 
+            
+            // Add buffer time to account for timer inconsistencies
+            Thread.Sleep(250);
+            
+            // Check if the release license callback was called
+            _releaseLicenseFuncMock.Verify(f => f(), Times.Once);
+
+            // Check if the license is released
+            Assert.Equal(TestLicenseState.Released, license.State);
+            Assert.False(tracker.LicenseHeld);
+            tracker.Dispose();
+        }
+
+        [Fact]
+        public void LicenseTracker_ShouldNotRelease_WhileInUse()
+        {
+            // Arrange
+            var licenseLockTime = TimeSpan.FromMilliseconds(100);
+            var (tracker, license) = CreateTracker();
+            
+            // Act
+            tracker.AcquireLicense();
+            Assert.Equal(TestLicenseState.Held, license.State);
+            Assert.True(tracker.LicenseHeld);
+            using (var usage = tracker.BeginUsage())
+            {
+                Thread.Sleep(200); // Wait longer than lock time
+                Assert.True(tracker.LicenseHeld); // Should not release while in use
+            }
+
+            tracker.Dispose();
+        }
+
+        [Fact]
+        public void LicenseTracker_ShouldResetTimer_OnNewUsage()
+        {
+            // Arrange
+            var licenseLockTime = TimeSpan.FromMilliseconds(100);
+            var (tracker, license) = CreateTracker(licenseLockTime: licenseLockTime);
+
+            // Act  
+            tracker.AcquireLicense();
+            Assert.Equal(TestLicenseState.Held, license.State);
+            using (tracker.BeginUsage()) { } // First usage
+            Thread.Sleep(50);
+            
+            using (tracker.BeginUsage()) { } // Second usage should reset timer
+            Thread.Sleep(50);
+            
+            // Assert
+            Assert.True(tracker.LicenseHeld); // Should not be released yet due to timer reset
+
+            Thread.Sleep(150); // Wait for full lock time + buffer
+            Assert.True(license.State == TestLicenseState.Released);
+            Assert.False(tracker.LicenseHeld);
+
+            tracker.Dispose();
+        }
+
+        [Fact]
+        public void LicenseTracker_ShouldPreventRelease_DuringContinuousUsage()
+        {
+            // Arrange
+            var (tracker, license) = CreateTracker();
+
+            // Act
+            tracker.AcquireLicense();
+            using (var usage1 = tracker.BeginUsage())
+            {
+                Thread.Sleep(50);
+                using (var usage2 = tracker.BeginUsage())
+                {
+                    Thread.Sleep(100);
+                    // Should not release during overlapping usage
+                    Assert.True(tracker.LicenseHeld);
+                }
+            }
+
+            Thread.Sleep(200); // Wait longer than lock time after all usage ends
+            
+            // Assert
+            Assert.True(license.State == TestLicenseState.Released);
+            tracker.Dispose();
+        }
+
+        [Fact]
+        public void LicenseTracker_ShouldUpdateState_WhenLicenseReleasedManually()
+        {
+            // Arrange
+            var (tracker, license) = CreateTracker();
+            tracker.AcquireLicense();
+            using (tracker.BeginUsage()) { }
+            Assert.True(tracker.LicenseHeld);
+
+            // Act
+            tracker.ReleaseLicenseWithoutCallback();
path: Cognite.Simulator.Tests/UtilsTests/LicenseControllerTest.cs
user: login: abdullah-cognite
body: Can be called ClearLicenseState
created_at: 2025-02-27T09:54:33Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,75 @@
+using System.Linq;
+using System;
+using Microsoft.Extensions.Logging;
+using System.Diagnostics;
+using System.Collections.Generic;
+using System.Runtime.Versioning;
+using Cognite.Simulator.Utils.Automation;
+using Cognite.Simulator.Utils;
+using System.Threading;
+using System.Globalization;
+using System.Management;
+using System.Security.Principal;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Cognite.Simulator.Utils
+{
+     public static class ProcessUtils {
+        public static string GetProcessOwnerWmi(int processId)
+        {
+            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
+                throw new PlatformNotSupportedException("This method is only supported on Windows");
+            }
+            string query = $"SELECT * FROM Win32_Process WHERE ProcessId = {processId}";
+            using (var searcher = new ManagementObjectSearcher(query))
+            using (var results = searcher.Get())
+            {
+                foreach (ManagementObject process in results)
+                {
+                    try
+                    {
+                        object[] args = new object[] { "", "" };
+                        if (Convert.ToInt32(process.InvokeMethod("GetOwner", args)) == 0)
+                        {
+                            string user = (string)args[0];
+                            string domain = (string)args[1];
+                            return $"{domain}\\{user}";
+                        }
+                    }
+                    catch
+                    {
+                        return "Access Denied or Process Exited";
path: Cognite.Simulator.Utils/ProcessUtils.cs
user: login: polomani
body: as we discussed, we can just throw an error and catch it in the caller
created_at: 2025-02-27T13:17:45Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,75 @@
+using System.Linq;
+using System;
+using Microsoft.Extensions.Logging;
+using System.Diagnostics;
+using System.Collections.Generic;
+using System.Runtime.Versioning;
+using Cognite.Simulator.Utils.Automation;
+using Cognite.Simulator.Utils;
+using System.Threading;
+using System.Globalization;
+using System.Management;
+using System.Security.Principal;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
path: Cognite.Simulator.Utils/ProcessUtils.cs
user: login: polomani
body: have a look into gihub `warning`s. they usually make sense
created_at: 2025-02-27T13:18:35Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,75 @@
+using System.Linq;
+using System;
+using Microsoft.Extensions.Logging;
+using System.Diagnostics;
+using System.Collections.Generic;
+using System.Runtime.Versioning;
+using Cognite.Simulator.Utils.Automation;
+using Cognite.Simulator.Utils;
+using System.Threading;
+using System.Globalization;
+using System.Management;
+using System.Security.Principal;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Cognite.Simulator.Utils
+{
+     public static class ProcessUtils {
+        public static string GetProcessOwnerWmi(int processId)
+        {
+            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
+                throw new PlatformNotSupportedException("This method is only supported on Windows");
path: Cognite.Simulator.Utils/ProcessUtils.cs
user: login: polomani
body: we need a github action that runs on windows as well as on linux
created_at: 2025-02-27T13:24:58Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,226 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Cognite.Simulator.Utils
+{
+    /// <summary>
+    /// Class to handle license acquisition and release.
+    /// With this a license can be acquired and released after a certain period of time.
+    /// </summary>
+   public class LicenseController : IDisposable
+    {
+        private readonly TimeSpan _licenseLockTime;
+        private readonly object _lock = new object();
+        private Timer _releaseTimer;
+        private readonly Func<object> _releaseLicenseFunc;
+        private readonly Func<object> _acquireLicenseFunc;
+        private bool _licenseHeld;
+        private DateTime _lastUsageTime;
+        private DateTime _scheduledReleaseTime;
+        private readonly ILogger _logger;
+        private const int LoggingIntervalMs = 60000; // 1 minute
+        private readonly CancellationTokenSource _cts = new CancellationTokenSource();
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="LicenseController"/> class.
+        /// </summary>
+        /// <param name="licenseLockTime">The time span for which the license is locked.</param>
+        /// <param name="releaseLicenseFunc">The function to release the license.</param>
+        /// <param name="acquireLicenseFunc">The function to acquire the license.</param>
+        /// <param name="logger">The logger instance for logging information.</param>
+        /// <param name="cancellationToken">The cancellation token to observe while waiting for the task to complete. Optional.</param>
+        /// <exception cref="ArgumentNullException">Thrown when <paramref name="releaseLicenseFunc"/> is null.</exception>
+        public LicenseController(
+            TimeSpan licenseLockTime,
+            Func<object> releaseLicenseFunc,
+            Func<object> acquireLicenseFunc,
path: Cognite.Simulator.Utils/LicenseController.cs
user: login: polomani
body: ```suggestion
            Action releaseLicenseFunc,
            Action acquireLicenseFunc,
```
how about this? (no return type)
created_at: 2025-02-27T13:28:07Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,226 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Cognite.Simulator.Utils
+{
+    /// <summary>
+    /// Class to handle license acquisition and release.
+    /// With this a license can be acquired and released after a certain period of time.
+    /// </summary>
+   public class LicenseController : IDisposable
+    {
+        private readonly TimeSpan _licenseLockTime;
+        private readonly object _lock = new object();
+        private Timer _releaseTimer;
+        private readonly Func<object> _releaseLicenseFunc;
+        private readonly Func<object> _acquireLicenseFunc;
+        private bool _licenseHeld;
+        private DateTime _lastUsageTime;
+        private DateTime _scheduledReleaseTime;
+        private readonly ILogger _logger;
+        private const int LoggingIntervalMs = 60000; // 1 minute
+        private readonly CancellationTokenSource _cts = new CancellationTokenSource();
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="LicenseController"/> class.
+        /// </summary>
+        /// <param name="licenseLockTime">The time span for which the license is locked.</param>
+        /// <param name="releaseLicenseFunc">The function to release the license.</param>
+        /// <param name="acquireLicenseFunc">The function to acquire the license.</param>
+        /// <param name="logger">The logger instance for logging information.</param>
+        /// <param name="cancellationToken">The cancellation token to observe while waiting for the task to complete. Optional.</param>
+        /// <exception cref="ArgumentNullException">Thrown when <paramref name="releaseLicenseFunc"/> is null.</exception>
+        public LicenseController(
+            TimeSpan licenseLockTime,
+            Func<object> releaseLicenseFunc,
+            Func<object> acquireLicenseFunc,
+            ILogger logger,
+            CancellationToken cancellationToken = new CancellationToken()
+        )
+        {
+            _licenseLockTime = licenseLockTime;
+            _licenseHeld = false;
+            _releaseLicenseFunc = releaseLicenseFunc ?? throw new ArgumentNullException(nameof(releaseLicenseFunc));
+            _acquireLicenseFunc = acquireLicenseFunc ?? throw new ArgumentNullException(nameof(acquireLicenseFunc));
+            _releaseTimer = new Timer(ReleaseLicenseAfterTimeout, null, Timeout.Infinite, Timeout.Infinite);
+            _logger = logger;
+            _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+            _ = StartLoggingTask(_cts.Token);
+        }
+
+        /// <summary>
+        /// Gets a boolean indicating whether the license is currently held or not.
+        /// </summary>
+        public bool LicenseHeld => _licenseHeld;
+
+        private async Task StartLoggingTask(CancellationToken cancellationToken)
+        {
+            try
+            {
+                while (!cancellationToken.IsCancellationRequested)
+                {
+                    LogLicenseStatus();
+                    await Task.Delay(LoggingIntervalMs, cancellationToken).ConfigureAwait(false);
+                }
+            }
+            catch (OperationCanceledException)
+            {
+                // Task was cancelled, log final status
+                LogLicenseStatus();
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error in license status logging task");
+            }
+        }
+
+        private void LogLicenseStatus()
+        {
+            _logger.LogInformation("License is currently {Status}", _licenseHeld ? "held" : "not held");
+            if (_licenseHeld)
+            {
+                var duration = DateTime.Now - _lastUsageTime;
+                _logger.LogInformation("License has been held for {Duration}", CommonUtils.FormatDuration(duration));
+                var timeUntilRelease = _scheduledReleaseTime - DateTime.Now;
+                _logger.LogInformation("License will be released in {TimeUntilRelease} (at {ReleaseTime})", 
+                    CommonUtils.FormatDuration(timeUntilRelease),
+                    _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+            }
+        }
+        
+        /// <summary>
+        /// Acquires a license by invoking the license acquisition function and sets the license status to held.
+        /// Also updates the release timer to ensure the license is released after a certain period.
+        /// </summary>
+        /// <exception cref="Exception">Thrown when an error occurs while acquiring the license.</exception>
+        public void AcquireLicense() {
+            lock (_lock)
+            {
+                try
+                {
+                    if (_licenseHeld)
+                    {
+                        _logger.LogInformation("License already held, skipping acquisition");
+                        return;
+                    }
+                    _logger.LogInformation("Attempting to acquire license");
+                    _acquireLicenseFunc();
+                    _licenseHeld = true;
+                    UpdateReleaseTimer();
+                    _logger.LogInformation("License acquired successfully");
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, "Error acquiring license");
+                    throw;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license after the timeout has elapsed. This is called by the timer.
+        /// </summary>
+        private void ReleaseLicenseAfterTimeout(object state)
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Attempting to release license at scheduled time {ScheduledTime}", _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+                if (DateTime.Now >= _scheduledReleaseTime)
+                {
+                    _releaseLicenseFunc();
+                    _licenseHeld = false;  
+                    _logger.LogInformation("License released successfully");
+                }
+                else
+                {
+                    _logger.LogInformation("License not released - current time {CurrentTime} is before scheduled release time", 
+                        DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license immediately. This is used externally to reset the internal state when an external process finds out
+        /// that the license is no longer held.
+        /// </summary>
+        public void ReleaseLicenseWithoutCallback() {
+            lock (_lock)
+            {
+                _licenseHeld = false;
+                _lastUsageTime = DateTime.Now;
+                _scheduledReleaseTime = DateTime.Now;
+                _releaseTimer.Change(Timeout.Infinite, Timeout.Infinite);
+                _logger.LogInformation("License released without callback");
+            }
+        }
+
+        private void UpdateReleaseTimer() {
+            _lastUsageTime = DateTime.Now;
+            // Extend release time
+            _scheduledReleaseTime = DateTime.Now.Add(_licenseLockTime);
path: Cognite.Simulator.Utils/LicenseController.cs
user: login: polomani
body: _scheduledReleaseTime is not correct here.
the actual is not know because it will be _lastUsageTime + _licenseLockTime.
maybe set it to infinite? or null idk
created_at: 2025-02-27T14:07:22Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,226 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Cognite.Simulator.Utils
+{
+    /// <summary>
+    /// Class to handle license acquisition and release.
+    /// With this a license can be acquired and released after a certain period of time.
+    /// </summary>
+   public class LicenseController : IDisposable
+    {
+        private readonly TimeSpan _licenseLockTime;
+        private readonly object _lock = new object();
+        private Timer _releaseTimer;
+        private readonly Func<object> _releaseLicenseFunc;
+        private readonly Func<object> _acquireLicenseFunc;
+        private bool _licenseHeld;
+        private DateTime _lastUsageTime;
+        private DateTime _scheduledReleaseTime;
+        private readonly ILogger _logger;
+        private const int LoggingIntervalMs = 60000; // 1 minute
+        private readonly CancellationTokenSource _cts = new CancellationTokenSource();
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="LicenseController"/> class.
+        /// </summary>
+        /// <param name="licenseLockTime">The time span for which the license is locked.</param>
+        /// <param name="releaseLicenseFunc">The function to release the license.</param>
+        /// <param name="acquireLicenseFunc">The function to acquire the license.</param>
+        /// <param name="logger">The logger instance for logging information.</param>
+        /// <param name="cancellationToken">The cancellation token to observe while waiting for the task to complete. Optional.</param>
+        /// <exception cref="ArgumentNullException">Thrown when <paramref name="releaseLicenseFunc"/> is null.</exception>
+        public LicenseController(
+            TimeSpan licenseLockTime,
+            Func<object> releaseLicenseFunc,
+            Func<object> acquireLicenseFunc,
+            ILogger logger,
+            CancellationToken cancellationToken = new CancellationToken()
+        )
+        {
+            _licenseLockTime = licenseLockTime;
+            _licenseHeld = false;
+            _releaseLicenseFunc = releaseLicenseFunc ?? throw new ArgumentNullException(nameof(releaseLicenseFunc));
+            _acquireLicenseFunc = acquireLicenseFunc ?? throw new ArgumentNullException(nameof(acquireLicenseFunc));
+            _releaseTimer = new Timer(ReleaseLicenseAfterTimeout, null, Timeout.Infinite, Timeout.Infinite);
+            _logger = logger;
+            _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+            _ = StartLoggingTask(_cts.Token);
+        }
+
+        /// <summary>
+        /// Gets a boolean indicating whether the license is currently held or not.
+        /// </summary>
+        public bool LicenseHeld => _licenseHeld;
+
+        private async Task StartLoggingTask(CancellationToken cancellationToken)
+        {
+            try
+            {
+                while (!cancellationToken.IsCancellationRequested)
+                {
+                    LogLicenseStatus();
+                    await Task.Delay(LoggingIntervalMs, cancellationToken).ConfigureAwait(false);
+                }
+            }
+            catch (OperationCanceledException)
+            {
+                // Task was cancelled, log final status
+                LogLicenseStatus();
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error in license status logging task");
+            }
+        }
+
+        private void LogLicenseStatus()
+        {
+            _logger.LogInformation("License is currently {Status}", _licenseHeld ? "held" : "not held");
+            if (_licenseHeld)
+            {
+                var duration = DateTime.Now - _lastUsageTime;
+                _logger.LogInformation("License has been held for {Duration}", CommonUtils.FormatDuration(duration));
+                var timeUntilRelease = _scheduledReleaseTime - DateTime.Now;
+                _logger.LogInformation("License will be released in {TimeUntilRelease} (at {ReleaseTime})", 
+                    CommonUtils.FormatDuration(timeUntilRelease),
+                    _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+            }
+        }
+        
+        /// <summary>
+        /// Acquires a license by invoking the license acquisition function and sets the license status to held.
+        /// Also updates the release timer to ensure the license is released after a certain period.
+        /// </summary>
+        /// <exception cref="Exception">Thrown when an error occurs while acquiring the license.</exception>
+        public void AcquireLicense() {
+            lock (_lock)
+            {
+                try
+                {
+                    if (_licenseHeld)
+                    {
+                        _logger.LogInformation("License already held, skipping acquisition");
+                        return;
+                    }
+                    _logger.LogInformation("Attempting to acquire license");
+                    _acquireLicenseFunc();
+                    _licenseHeld = true;
+                    UpdateReleaseTimer();
+                    _logger.LogInformation("License acquired successfully");
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, "Error acquiring license");
+                    throw;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license after the timeout has elapsed. This is called by the timer.
+        /// </summary>
+        private void ReleaseLicenseAfterTimeout(object state)
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Attempting to release license at scheduled time {ScheduledTime}", _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+                if (DateTime.Now >= _scheduledReleaseTime)
+                {
+                    _releaseLicenseFunc();
+                    _licenseHeld = false;  
+                    _logger.LogInformation("License released successfully");
+                }
+                else
+                {
+                    _logger.LogInformation("License not released - current time {CurrentTime} is before scheduled release time", 
+                        DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license immediately. This is used externally to reset the internal state when an external process finds out
+        /// that the license is no longer held.
+        /// </summary>
+        public void ReleaseLicenseWithoutCallback() {
+            lock (_lock)
+            {
+                _licenseHeld = false;
+                _lastUsageTime = DateTime.Now;
+                _scheduledReleaseTime = DateTime.Now;
+                _releaseTimer.Change(Timeout.Infinite, Timeout.Infinite);
+                _logger.LogInformation("License released without callback");
+            }
+        }
+
+        private void UpdateReleaseTimer() {
path: Cognite.Simulator.Utils/LicenseController.cs
user: login: polomani
body: ```suggestion
        private void PauseTimer() {
```
created_at: 2025-02-27T14:08:50Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,226 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Cognite.Simulator.Utils
+{
+    /// <summary>
+    /// Class to handle license acquisition and release.
+    /// With this a license can be acquired and released after a certain period of time.
+    /// </summary>
+   public class LicenseController : IDisposable
+    {
+        private readonly TimeSpan _licenseLockTime;
+        private readonly object _lock = new object();
+        private Timer _releaseTimer;
+        private readonly Func<object> _releaseLicenseFunc;
+        private readonly Func<object> _acquireLicenseFunc;
+        private bool _licenseHeld;
+        private DateTime _lastUsageTime;
+        private DateTime _scheduledReleaseTime;
+        private readonly ILogger _logger;
+        private const int LoggingIntervalMs = 60000; // 1 minute
+        private readonly CancellationTokenSource _cts = new CancellationTokenSource();
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="LicenseController"/> class.
+        /// </summary>
+        /// <param name="licenseLockTime">The time span for which the license is locked.</param>
+        /// <param name="releaseLicenseFunc">The function to release the license.</param>
+        /// <param name="acquireLicenseFunc">The function to acquire the license.</param>
+        /// <param name="logger">The logger instance for logging information.</param>
+        /// <param name="cancellationToken">The cancellation token to observe while waiting for the task to complete. Optional.</param>
+        /// <exception cref="ArgumentNullException">Thrown when <paramref name="releaseLicenseFunc"/> is null.</exception>
+        public LicenseController(
+            TimeSpan licenseLockTime,
+            Func<object> releaseLicenseFunc,
+            Func<object> acquireLicenseFunc,
+            ILogger logger,
+            CancellationToken cancellationToken = new CancellationToken()
+        )
+        {
+            _licenseLockTime = licenseLockTime;
+            _licenseHeld = false;
+            _releaseLicenseFunc = releaseLicenseFunc ?? throw new ArgumentNullException(nameof(releaseLicenseFunc));
+            _acquireLicenseFunc = acquireLicenseFunc ?? throw new ArgumentNullException(nameof(acquireLicenseFunc));
+            _releaseTimer = new Timer(ReleaseLicenseAfterTimeout, null, Timeout.Infinite, Timeout.Infinite);
+            _logger = logger;
+            _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+            _ = StartLoggingTask(_cts.Token);
+        }
+
+        /// <summary>
+        /// Gets a boolean indicating whether the license is currently held or not.
+        /// </summary>
+        public bool LicenseHeld => _licenseHeld;
+
+        private async Task StartLoggingTask(CancellationToken cancellationToken)
+        {
+            try
+            {
+                while (!cancellationToken.IsCancellationRequested)
+                {
+                    LogLicenseStatus();
+                    await Task.Delay(LoggingIntervalMs, cancellationToken).ConfigureAwait(false);
+                }
+            }
+            catch (OperationCanceledException)
+            {
+                // Task was cancelled, log final status
+                LogLicenseStatus();
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error in license status logging task");
+            }
+        }
+
+        private void LogLicenseStatus()
+        {
+            _logger.LogInformation("License is currently {Status}", _licenseHeld ? "held" : "not held");
+            if (_licenseHeld)
+            {
+                var duration = DateTime.Now - _lastUsageTime;
+                _logger.LogInformation("License has been held for {Duration}", CommonUtils.FormatDuration(duration));
+                var timeUntilRelease = _scheduledReleaseTime - DateTime.Now;
+                _logger.LogInformation("License will be released in {TimeUntilRelease} (at {ReleaseTime})", 
+                    CommonUtils.FormatDuration(timeUntilRelease),
+                    _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+            }
+        }
+        
+        /// <summary>
+        /// Acquires a license by invoking the license acquisition function and sets the license status to held.
+        /// Also updates the release timer to ensure the license is released after a certain period.
+        /// </summary>
+        /// <exception cref="Exception">Thrown when an error occurs while acquiring the license.</exception>
+        public void AcquireLicense() {
+            lock (_lock)
+            {
+                try
+                {
+                    if (_licenseHeld)
+                    {
+                        _logger.LogInformation("License already held, skipping acquisition");
+                        return;
+                    }
+                    _logger.LogInformation("Attempting to acquire license");
+                    _acquireLicenseFunc();
+                    _licenseHeld = true;
+                    UpdateReleaseTimer();
+                    _logger.LogInformation("License acquired successfully");
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, "Error acquiring license");
+                    throw;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license after the timeout has elapsed. This is called by the timer.
+        /// </summary>
+        private void ReleaseLicenseAfterTimeout(object state)
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Attempting to release license at scheduled time {ScheduledTime}", _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+                if (DateTime.Now >= _scheduledReleaseTime)
+                {
+                    _releaseLicenseFunc();
+                    _licenseHeld = false;  
+                    _logger.LogInformation("License released successfully");
+                }
+                else
+                {
+                    _logger.LogInformation("License not released - current time {CurrentTime} is before scheduled release time", 
+                        DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license immediately. This is used externally to reset the internal state when an external process finds out
+        /// that the license is no longer held.
+        /// </summary>
+        public void ReleaseLicenseWithoutCallback() {
+            lock (_lock)
+            {
+                _licenseHeld = false;
+                _lastUsageTime = DateTime.Now;
+                _scheduledReleaseTime = DateTime.Now;
+                _releaseTimer.Change(Timeout.Infinite, Timeout.Infinite);
+                _logger.LogInformation("License released without callback");
+            }
+        }
+
+        private void UpdateReleaseTimer() {
+            _lastUsageTime = DateTime.Now;
+            // Extend release time
+            _scheduledReleaseTime = DateTime.Now.Add(_licenseLockTime);
+            _logger.LogInformation("License release scheduled for {ReleaseTime}", 
+            _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+            _releaseTimer.Change(Timeout.Infinite, Timeout.Infinite);
+        }
+
+        /// <summary>
+        /// Begins usage of the license.
+        /// </summary>
+        /// <returns>An <see cref="IDisposable"/> object that can be used to end the license usage.</returns>
+        public IDisposable BeginUsage()
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Starting license usage");
+                UpdateReleaseTimer();
+                return new LicenseUsageScope(this);
+            }
+        }
+
+        private void EndUsage()
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Ending license usage, scheduling release");
+                _lastUsageTime = DateTime.Now;
+                _scheduledReleaseTime = _lastUsageTime.Add(_licenseLockTime);
+                _logger.LogInformation("License release scheduled for {ReleaseTime}", 
+                    _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+                _releaseTimer.Change(_licenseLockTime, Timeout.InfiniteTimeSpan);
+            }
+        }
+
+        /// <summary>
+        /// Releases the license immediately.
path: Cognite.Simulator.Utils/LicenseController.cs
user: login: polomani
body: ```suggestion
        /// Releases the license immediately.
```
the comment is misleading, no?
created_at: 2025-02-27T14:13:09Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,226 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Cognite.Simulator.Utils
+{
+    /// <summary>
+    /// Class to handle license acquisition and release.
+    /// With this a license can be acquired and released after a certain period of time.
+    /// </summary>
+   public class LicenseController : IDisposable
+    {
+        private readonly TimeSpan _licenseLockTime;
+        private readonly object _lock = new object();
+        private Timer _releaseTimer;
+        private readonly Func<object> _releaseLicenseFunc;
+        private readonly Func<object> _acquireLicenseFunc;
+        private bool _licenseHeld;
+        private DateTime _lastUsageTime;
+        private DateTime _scheduledReleaseTime;
+        private readonly ILogger _logger;
+        private const int LoggingIntervalMs = 60000; // 1 minute
+        private readonly CancellationTokenSource _cts = new CancellationTokenSource();
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="LicenseController"/> class.
+        /// </summary>
+        /// <param name="licenseLockTime">The time span for which the license is locked.</param>
+        /// <param name="releaseLicenseFunc">The function to release the license.</param>
+        /// <param name="acquireLicenseFunc">The function to acquire the license.</param>
+        /// <param name="logger">The logger instance for logging information.</param>
+        /// <param name="cancellationToken">The cancellation token to observe while waiting for the task to complete. Optional.</param>
+        /// <exception cref="ArgumentNullException">Thrown when <paramref name="releaseLicenseFunc"/> is null.</exception>
+        public LicenseController(
+            TimeSpan licenseLockTime,
+            Func<object> releaseLicenseFunc,
+            Func<object> acquireLicenseFunc,
+            ILogger logger,
+            CancellationToken cancellationToken = new CancellationToken()
+        )
+        {
+            _licenseLockTime = licenseLockTime;
+            _licenseHeld = false;
+            _releaseLicenseFunc = releaseLicenseFunc ?? throw new ArgumentNullException(nameof(releaseLicenseFunc));
+            _acquireLicenseFunc = acquireLicenseFunc ?? throw new ArgumentNullException(nameof(acquireLicenseFunc));
+            _releaseTimer = new Timer(ReleaseLicenseAfterTimeout, null, Timeout.Infinite, Timeout.Infinite);
+            _logger = logger;
+            _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+            _ = StartLoggingTask(_cts.Token);
+        }
+
+        /// <summary>
+        /// Gets a boolean indicating whether the license is currently held or not.
+        /// </summary>
+        public bool LicenseHeld => _licenseHeld;
+
+        private async Task StartLoggingTask(CancellationToken cancellationToken)
+        {
+            try
+            {
+                while (!cancellationToken.IsCancellationRequested)
+                {
+                    LogLicenseStatus();
+                    await Task.Delay(LoggingIntervalMs, cancellationToken).ConfigureAwait(false);
+                }
+            }
+            catch (OperationCanceledException)
+            {
+                // Task was cancelled, log final status
+                LogLicenseStatus();
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error in license status logging task");
+            }
+        }
+
+        private void LogLicenseStatus()
+        {
+            _logger.LogInformation("License is currently {Status}", _licenseHeld ? "held" : "not held");
+            if (_licenseHeld)
+            {
+                var duration = DateTime.Now - _lastUsageTime;
+                _logger.LogInformation("License has been held for {Duration}", CommonUtils.FormatDuration(duration));
+                var timeUntilRelease = _scheduledReleaseTime - DateTime.Now;
+                _logger.LogInformation("License will be released in {TimeUntilRelease} (at {ReleaseTime})", 
+                    CommonUtils.FormatDuration(timeUntilRelease),
+                    _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+            }
+        }
+        
+        /// <summary>
+        /// Acquires a license by invoking the license acquisition function and sets the license status to held.
+        /// Also updates the release timer to ensure the license is released after a certain period.
+        /// </summary>
+        /// <exception cref="Exception">Thrown when an error occurs while acquiring the license.</exception>
+        public void AcquireLicense() {
+            lock (_lock)
+            {
+                try
+                {
+                    if (_licenseHeld)
+                    {
+                        _logger.LogInformation("License already held, skipping acquisition");
+                        return;
+                    }
+                    _logger.LogInformation("Attempting to acquire license");
+                    _acquireLicenseFunc();
+                    _licenseHeld = true;
+                    UpdateReleaseTimer();
+                    _logger.LogInformation("License acquired successfully");
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, "Error acquiring license");
+                    throw;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license after the timeout has elapsed. This is called by the timer.
+        /// </summary>
+        private void ReleaseLicenseAfterTimeout(object state)
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Attempting to release license at scheduled time {ScheduledTime}", _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+                if (DateTime.Now >= _scheduledReleaseTime)
+                {
+                    _releaseLicenseFunc();
+                    _licenseHeld = false;  
+                    _logger.LogInformation("License released successfully");
+                }
+                else
+                {
+                    _logger.LogInformation("License not released - current time {CurrentTime} is before scheduled release time", 
+                        DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license immediately. This is used externally to reset the internal state when an external process finds out
+        /// that the license is no longer held.
+        /// </summary>
+        public void ReleaseLicenseWithoutCallback() {
+            lock (_lock)
+            {
+                _licenseHeld = false;
+                _lastUsageTime = DateTime.Now;
+                _scheduledReleaseTime = DateTime.Now;
+                _releaseTimer.Change(Timeout.Infinite, Timeout.Infinite);
+                _logger.LogInformation("License released without callback");
+            }
+        }
+
+        private void UpdateReleaseTimer() {
+            _lastUsageTime = DateTime.Now;
path: Cognite.Simulator.Utils/LicenseController.cs
user: login: polomani
body: I believe we should update _lastUsageTime only on `EndUsage`. otherwise the logline `License has been held for {Duration}` would be lying
created_at: 2025-02-27T14:15:21Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,176 @@
+using Xunit;
+using System;
+using System.Threading;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Cognite.Simulator.Utils;
+
+namespace Cognite.Simulator.Tests.UtilsTests{
+
+    public enum TestLicenseState
+    {
+        Released,
+        Held
+    }
+
+    public class LicenseTrackerTests
+    {
+        private readonly Mock<ILogger> _loggerMock = new Mock<ILogger>();
+
+        private (LicenseController controller, StateHolder<TestLicenseState> stateHolder) CreateTracker(
+            Mock<Func<object>>? releaseMock = null,
+            Mock<Func<object>>? acquireMock = null,
+            TimeSpan? licenseLockTime = null)
+        {
+            var stateHolder = new StateHolder<TestLicenseState> { State = TestLicenseState.Released };
+            
+            releaseMock ??= new Mock<Func<object>>();
+            acquireMock ??= new Mock<Func<object>>();
+            
+            // Set up the mocks to track the state changes
+            releaseMock.Setup(m => m()).Callback(() => stateHolder.State = TestLicenseState.Released);
+            acquireMock.Setup(m => m()).Callback(() => stateHolder.State = TestLicenseState.Held);
+            
+            var tracker = new LicenseController(
+                licenseLockTime ?? TimeSpan.FromMilliseconds(100),
+                () => { releaseMock.Object(); return null; },
+                () => { acquireMock.Object(); return null; },
+                _loggerMock.Object
+            );
+            
+            return (tracker, stateHolder);
+        }
+
+        // Simple class to hold state that can be captured in lambdas
+        private class StateHolder<T>
+        {
+            public T State { get; set; }
+        }
+
+
+        [Fact]
+        public void LicenseTracker_ShouldReleaseLicense_WhenNotInUse()
+        {
+
+            // Arrange
+            Mock<Func<object>> _releaseLicenseFuncMock = new Mock<Func<object>>();
+            Mock<Func<object>> _acquireLicenseFuncMock = new Mock<Func<object>>();
+            var licenseLockTime = TimeSpan.FromMilliseconds(100);
+            var (tracker, license) = CreateTracker( _releaseLicenseFuncMock, _acquireLicenseFuncMock, licenseLockTime);
+
+            // Check if the default state of the license is released
+            Assert.Equal(TestLicenseState.Released, license.State);
+
+            // Check if acquiring the license calls the acquire license function and changes the state
+            tracker.AcquireLicense();
+            _acquireLicenseFuncMock.Verify(f => f(), Times.Once);
+            Assert.Equal(TestLicenseState.Held, license.State);
+            Assert.True(tracker.LicenseHeld);
+
+            using (tracker.BeginUsage()) { /* Use and immediately release */ } 
+            
+            // Add buffer time to account for timer inconsistencies
+            Thread.Sleep(250);
+            
+            // Check if the release license callback was called
+            _releaseLicenseFuncMock.Verify(f => f(), Times.Once);
+
+            // Check if the license is released
+            Assert.Equal(TestLicenseState.Released, license.State);
+            Assert.False(tracker.LicenseHeld);
+            tracker.Dispose();
+        }
+
+        [Fact]
+        public void LicenseTracker_ShouldNotRelease_WhileInUse()
+        {
+            // Arrange
+            var licenseLockTime = TimeSpan.FromMilliseconds(100);
+            var (tracker, license) = CreateTracker();
+            
+            // Act
+            tracker.AcquireLicense();
+            Assert.Equal(TestLicenseState.Held, license.State);
+            Assert.True(tracker.LicenseHeld);
+            using (var usage = tracker.BeginUsage())
+            {
+                Thread.Sleep(200); // Wait longer than lock time
+                Assert.True(tracker.LicenseHeld); // Should not release while in use
+            }
+
+            tracker.Dispose();
+        }
+
+        [Fact]
+        public void LicenseTracker_ShouldResetTimer_OnNewUsage()
+        {
+            // Arrange
+            var licenseLockTime = TimeSpan.FromMilliseconds(100);
+            var (tracker, license) = CreateTracker(licenseLockTime: licenseLockTime);
+
+            // Act  
+            tracker.AcquireLicense();
+            Assert.Equal(TestLicenseState.Held, license.State);
+            using (tracker.BeginUsage()) { } // First usage
+            Thread.Sleep(50);
+            
+            using (tracker.BeginUsage()) { } // Second usage should reset timer
+            Thread.Sleep(50);
+            
+            // Assert
+            Assert.True(tracker.LicenseHeld); // Should not be released yet due to timer reset
+
+            Thread.Sleep(150); // Wait for full lock time + buffer
+            Assert.True(license.State == TestLicenseState.Released);
+            Assert.False(tracker.LicenseHeld);
+
+            tracker.Dispose();
+        }
+
+        [Fact]
+        public void LicenseTracker_ShouldPreventRelease_DuringContinuousUsage()
+        {
+            // Arrange
+            var (tracker, license) = CreateTracker();
+
+            // Act
+            tracker.AcquireLicense();
+            using (var usage1 = tracker.BeginUsage())
+            {
+                Thread.Sleep(50);
+                using (var usage2 = tracker.BeginUsage())
+                {
+                    Thread.Sleep(100);
+                    // Should not release during overlapping usage
+                    Assert.True(tracker.LicenseHeld);
+                }
+            }
+
+            Thread.Sleep(200); // Wait longer than lock time after all usage ends
+            
+            // Assert
+            Assert.True(license.State == TestLicenseState.Released);
+            tracker.Dispose();
+        }
+
+        [Fact]
+        public void LicenseTracker_ShouldUpdateState_WhenLicenseReleasedManually()
+        {
+            // Arrange
+            var (tracker, license) = CreateTracker();
+            tracker.AcquireLicense();
+            using (tracker.BeginUsage()) { }
+            Assert.True(tracker.LicenseHeld);
+
+            // Act
+            tracker.ReleaseLicenseWithoutCallback();
+            
+            // Assert
+            Assert.False(tracker.LicenseHeld);
+            // Should not change external license state (since this is a manual release)
+            Assert.True(license.State == TestLicenseState.Held);
path: Cognite.Simulator.Tests/UtilsTests/LicenseControllerTest.cs
user: login: polomani
body: looks like not, unless different kind of timers are used
created_at: 2025-02-27T14:18:29Z
author_association: COLLABORATOR
diff_hunk: @@ -60,6 +60,11 @@ public Task TestConnection(CancellationToken token)
     {
         return Task.CompletedTask;
     }
+
+    public void SetExternalCancellationToken(CancellationToken token)
+    {
+        throw new NotImplementedException();
+    }
path: Sample.Calculator.Simulator/Simulator.cs
user: login: polomani
body: I would rather suggest passing token into methods directly.
1. tokens can have different lifetimes, but this forces only one to be used at a time
2. optional cancellation token on each async method is a common pattern (both internet and chatgpt agree)
created_at: 2025-02-27T14:30:49Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,226 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Cognite.Simulator.Utils
+{
+    /// <summary>
+    /// Class to handle license acquisition and release.
+    /// With this a license can be acquired and released after a certain period of time.
+    /// </summary>
+   public class LicenseController : IDisposable
+    {
+        private readonly TimeSpan _licenseLockTime;
+        private readonly object _lock = new object();
+        private Timer _releaseTimer;
+        private readonly Func<object> _releaseLicenseFunc;
+        private readonly Func<object> _acquireLicenseFunc;
+        private bool _licenseHeld;
+        private DateTime _lastUsageTime;
+        private DateTime _scheduledReleaseTime;
+        private readonly ILogger _logger;
+        private const int LoggingIntervalMs = 60000; // 1 minute
+        private readonly CancellationTokenSource _cts = new CancellationTokenSource();
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="LicenseController"/> class.
+        /// </summary>
+        /// <param name="licenseLockTime">The time span for which the license is locked.</param>
+        /// <param name="releaseLicenseFunc">The function to release the license.</param>
+        /// <param name="acquireLicenseFunc">The function to acquire the license.</param>
+        /// <param name="logger">The logger instance for logging information.</param>
+        /// <param name="cancellationToken">The cancellation token to observe while waiting for the task to complete. Optional.</param>
+        /// <exception cref="ArgumentNullException">Thrown when <paramref name="releaseLicenseFunc"/> is null.</exception>
+        public LicenseController(
+            TimeSpan licenseLockTime,
+            Func<object> releaseLicenseFunc,
+            Func<object> acquireLicenseFunc,
+            ILogger logger,
+            CancellationToken cancellationToken = new CancellationToken()
+        )
+        {
+            _licenseLockTime = licenseLockTime;
+            _licenseHeld = false;
+            _releaseLicenseFunc = releaseLicenseFunc ?? throw new ArgumentNullException(nameof(releaseLicenseFunc));
+            _acquireLicenseFunc = acquireLicenseFunc ?? throw new ArgumentNullException(nameof(acquireLicenseFunc));
+            _releaseTimer = new Timer(ReleaseLicenseAfterTimeout, null, Timeout.Infinite, Timeout.Infinite);
+            _logger = logger;
+            _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+            _ = StartLoggingTask(_cts.Token);
+        }
+
+        /// <summary>
+        /// Gets a boolean indicating whether the license is currently held or not.
+        /// </summary>
+        public bool LicenseHeld => _licenseHeld;
+
+        private async Task StartLoggingTask(CancellationToken cancellationToken)
+        {
+            try
+            {
+                while (!cancellationToken.IsCancellationRequested)
+                {
+                    LogLicenseStatus();
+                    await Task.Delay(LoggingIntervalMs, cancellationToken).ConfigureAwait(false);
+                }
+            }
+            catch (OperationCanceledException)
+            {
+                // Task was cancelled, log final status
+                LogLicenseStatus();
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error in license status logging task");
+            }
+        }
+
+        private void LogLicenseStatus()
+        {
+            _logger.LogInformation("License is currently {Status}", _licenseHeld ? "held" : "not held");
+            if (_licenseHeld)
+            {
+                var duration = DateTime.Now - _lastUsageTime;
+                _logger.LogInformation("License has been held for {Duration}", CommonUtils.FormatDuration(duration));
+                var timeUntilRelease = _scheduledReleaseTime - DateTime.Now;
+                _logger.LogInformation("License will be released in {TimeUntilRelease} (at {ReleaseTime})", 
+                    CommonUtils.FormatDuration(timeUntilRelease),
+                    _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+            }
+        }
+        
+        /// <summary>
+        /// Acquires a license by invoking the license acquisition function and sets the license status to held.
+        /// Also updates the release timer to ensure the license is released after a certain period.
+        /// </summary>
+        /// <exception cref="Exception">Thrown when an error occurs while acquiring the license.</exception>
+        public void AcquireLicense() {
+            lock (_lock)
+            {
+                try
+                {
+                    if (_licenseHeld)
+                    {
+                        _logger.LogInformation("License already held, skipping acquisition");
+                        return;
+                    }
+                    _logger.LogInformation("Attempting to acquire license");
+                    _acquireLicenseFunc();
+                    _licenseHeld = true;
+                    UpdateReleaseTimer();
+                    _logger.LogInformation("License acquired successfully");
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, "Error acquiring license");
+                    throw;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license after the timeout has elapsed. This is called by the timer.
+        /// </summary>
+        private void ReleaseLicenseAfterTimeout(object state)
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Attempting to release license at scheduled time {ScheduledTime}", _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+                if (DateTime.Now >= _scheduledReleaseTime)
+                {
+                    _releaseLicenseFunc();
+                    _licenseHeld = false;  
+                    _logger.LogInformation("License released successfully");
+                }
+                else
+                {
+                    _logger.LogInformation("License not released - current time {CurrentTime} is before scheduled release time", 
+                        DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license immediately. This is used externally to reset the internal state when an external process finds out
+        /// that the license is no longer held.
+        /// </summary>
+        public void ReleaseLicenseWithoutCallback() {
+            lock (_lock)
+            {
+                _licenseHeld = false;
+                _lastUsageTime = DateTime.Now;
+                _scheduledReleaseTime = DateTime.Now;
+                _releaseTimer.Change(Timeout.Infinite, Timeout.Infinite);
+                _logger.LogInformation("License released without callback");
+            }
+        }
+
+        private void UpdateReleaseTimer() {
+            _lastUsageTime = DateTime.Now;
+            // Extend release time
+            _scheduledReleaseTime = DateTime.Now.Add(_licenseLockTime);
path: Cognite.Simulator.Utils/LicenseController.cs
user: login: abdullah-cognite
body: agreed, removed this from here
created_at: 2025-02-28T08:44:14Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,226 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Cognite.Simulator.Utils
+{
+    /// <summary>
+    /// Class to handle license acquisition and release.
+    /// With this a license can be acquired and released after a certain period of time.
+    /// </summary>
+   public class LicenseController : IDisposable
+    {
+        private readonly TimeSpan _licenseLockTime;
+        private readonly object _lock = new object();
+        private Timer _releaseTimer;
+        private readonly Func<object> _releaseLicenseFunc;
+        private readonly Func<object> _acquireLicenseFunc;
+        private bool _licenseHeld;
+        private DateTime _lastUsageTime;
+        private DateTime _scheduledReleaseTime;
+        private readonly ILogger _logger;
+        private const int LoggingIntervalMs = 60000; // 1 minute
+        private readonly CancellationTokenSource _cts = new CancellationTokenSource();
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="LicenseController"/> class.
+        /// </summary>
+        /// <param name="licenseLockTime">The time span for which the license is locked.</param>
+        /// <param name="releaseLicenseFunc">The function to release the license.</param>
+        /// <param name="acquireLicenseFunc">The function to acquire the license.</param>
+        /// <param name="logger">The logger instance for logging information.</param>
+        /// <param name="cancellationToken">The cancellation token to observe while waiting for the task to complete. Optional.</param>
+        /// <exception cref="ArgumentNullException">Thrown when <paramref name="releaseLicenseFunc"/> is null.</exception>
+        public LicenseController(
+            TimeSpan licenseLockTime,
+            Func<object> releaseLicenseFunc,
+            Func<object> acquireLicenseFunc,
+            ILogger logger,
+            CancellationToken cancellationToken = new CancellationToken()
+        )
+        {
+            _licenseLockTime = licenseLockTime;
+            _licenseHeld = false;
+            _releaseLicenseFunc = releaseLicenseFunc ?? throw new ArgumentNullException(nameof(releaseLicenseFunc));
+            _acquireLicenseFunc = acquireLicenseFunc ?? throw new ArgumentNullException(nameof(acquireLicenseFunc));
+            _releaseTimer = new Timer(ReleaseLicenseAfterTimeout, null, Timeout.Infinite, Timeout.Infinite);
+            _logger = logger;
+            _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+            _ = StartLoggingTask(_cts.Token);
+        }
+
+        /// <summary>
+        /// Gets a boolean indicating whether the license is currently held or not.
+        /// </summary>
+        public bool LicenseHeld => _licenseHeld;
+
+        private async Task StartLoggingTask(CancellationToken cancellationToken)
+        {
+            try
+            {
+                while (!cancellationToken.IsCancellationRequested)
+                {
+                    LogLicenseStatus();
+                    await Task.Delay(LoggingIntervalMs, cancellationToken).ConfigureAwait(false);
+                }
+            }
+            catch (OperationCanceledException)
+            {
+                // Task was cancelled, log final status
+                LogLicenseStatus();
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error in license status logging task");
+            }
+        }
+
+        private void LogLicenseStatus()
+        {
+            _logger.LogInformation("License is currently {Status}", _licenseHeld ? "held" : "not held");
+            if (_licenseHeld)
+            {
+                var duration = DateTime.Now - _lastUsageTime;
+                _logger.LogInformation("License has been held for {Duration}", CommonUtils.FormatDuration(duration));
+                var timeUntilRelease = _scheduledReleaseTime - DateTime.Now;
+                _logger.LogInformation("License will be released in {TimeUntilRelease} (at {ReleaseTime})", 
+                    CommonUtils.FormatDuration(timeUntilRelease),
+                    _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+            }
+        }
+        
+        /// <summary>
+        /// Acquires a license by invoking the license acquisition function and sets the license status to held.
+        /// Also updates the release timer to ensure the license is released after a certain period.
+        /// </summary>
+        /// <exception cref="Exception">Thrown when an error occurs while acquiring the license.</exception>
+        public void AcquireLicense() {
+            lock (_lock)
+            {
+                try
+                {
+                    if (_licenseHeld)
+                    {
+                        _logger.LogInformation("License already held, skipping acquisition");
+                        return;
+                    }
+                    _logger.LogInformation("Attempting to acquire license");
+                    _acquireLicenseFunc();
+                    _licenseHeld = true;
+                    UpdateReleaseTimer();
+                    _logger.LogInformation("License acquired successfully");
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, "Error acquiring license");
+                    throw;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license after the timeout has elapsed. This is called by the timer.
+        /// </summary>
+        private void ReleaseLicenseAfterTimeout(object state)
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Attempting to release license at scheduled time {ScheduledTime}", _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+                if (DateTime.Now >= _scheduledReleaseTime)
+                {
+                    _releaseLicenseFunc();
+                    _licenseHeld = false;  
+                    _logger.LogInformation("License released successfully");
+                }
+                else
+                {
+                    _logger.LogInformation("License not released - current time {CurrentTime} is before scheduled release time", 
+                        DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license immediately. This is used externally to reset the internal state when an external process finds out
+        /// that the license is no longer held.
+        /// </summary>
+        public void ReleaseLicenseWithoutCallback() {
+            lock (_lock)
+            {
+                _licenseHeld = false;
+                _lastUsageTime = DateTime.Now;
+                _scheduledReleaseTime = DateTime.Now;
+                _releaseTimer.Change(Timeout.Infinite, Timeout.Infinite);
+                _logger.LogInformation("License released without callback");
+            }
+        }
+
+        private void UpdateReleaseTimer() {
+            _lastUsageTime = DateTime.Now;
+            // Extend release time
+            _scheduledReleaseTime = DateTime.Now.Add(_licenseLockTime);
+            _logger.LogInformation("License release scheduled for {ReleaseTime}", 
+            _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+            _releaseTimer.Change(Timeout.Infinite, Timeout.Infinite);
+        }
+
+        /// <summary>
+        /// Begins usage of the license.
+        /// </summary>
+        /// <returns>An <see cref="IDisposable"/> object that can be used to end the license usage.</returns>
+        public IDisposable BeginUsage()
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Starting license usage");
+                UpdateReleaseTimer();
+                return new LicenseUsageScope(this);
+            }
+        }
+
+        private void EndUsage()
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Ending license usage, scheduling release");
+                _lastUsageTime = DateTime.Now;
+                _scheduledReleaseTime = _lastUsageTime.Add(_licenseLockTime);
+                _logger.LogInformation("License release scheduled for {ReleaseTime}", 
+                    _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+                _releaseTimer.Change(_licenseLockTime, Timeout.InfiniteTimeSpan);
+            }
+        }
+
+        /// <summary>
+        /// Releases the license immediately.
path: Cognite.Simulator.Utils/LicenseController.cs
user: login: abdullah-cognite
body: updated
created_at: 2025-02-28T11:03:33Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,226 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Cognite.Simulator.Utils
+{
+    /// <summary>
+    /// Class to handle license acquisition and release.
+    /// With this a license can be acquired and released after a certain period of time.
+    /// </summary>
+   public class LicenseController : IDisposable
+    {
+        private readonly TimeSpan _licenseLockTime;
+        private readonly object _lock = new object();
+        private Timer _releaseTimer;
+        private readonly Func<object> _releaseLicenseFunc;
+        private readonly Func<object> _acquireLicenseFunc;
+        private bool _licenseHeld;
+        private DateTime _lastUsageTime;
+        private DateTime _scheduledReleaseTime;
+        private readonly ILogger _logger;
+        private const int LoggingIntervalMs = 60000; // 1 minute
+        private readonly CancellationTokenSource _cts = new CancellationTokenSource();
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="LicenseController"/> class.
+        /// </summary>
+        /// <param name="licenseLockTime">The time span for which the license is locked.</param>
+        /// <param name="releaseLicenseFunc">The function to release the license.</param>
+        /// <param name="acquireLicenseFunc">The function to acquire the license.</param>
+        /// <param name="logger">The logger instance for logging information.</param>
+        /// <param name="cancellationToken">The cancellation token to observe while waiting for the task to complete. Optional.</param>
+        /// <exception cref="ArgumentNullException">Thrown when <paramref name="releaseLicenseFunc"/> is null.</exception>
+        public LicenseController(
+            TimeSpan licenseLockTime,
+            Func<object> releaseLicenseFunc,
+            Func<object> acquireLicenseFunc,
path: Cognite.Simulator.Utils/LicenseController.cs
user: login: abdullah-cognite
body: much better
created_at: 2025-02-28T11:03:48Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,75 @@
+using System.Linq;
+using System;
+using Microsoft.Extensions.Logging;
+using System.Diagnostics;
+using System.Collections.Generic;
+using System.Runtime.Versioning;
+using Cognite.Simulator.Utils.Automation;
+using Cognite.Simulator.Utils;
+using System.Threading;
+using System.Globalization;
+using System.Management;
+using System.Security.Principal;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Cognite.Simulator.Utils
+{
+     public static class ProcessUtils {
+        public static string GetProcessOwnerWmi(int processId)
+        {
+            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
+                throw new PlatformNotSupportedException("This method is only supported on Windows");
path: Cognite.Simulator.Utils/ProcessUtils.cs
user: login: abdullah-cognite
body: added an action, check it out and let me know if this is okay
created_at: 2025-02-28T11:04:07Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,75 @@
+using System.Linq;
+using System;
+using Microsoft.Extensions.Logging;
+using System.Diagnostics;
+using System.Collections.Generic;
+using System.Runtime.Versioning;
+using Cognite.Simulator.Utils.Automation;
+using Cognite.Simulator.Utils;
+using System.Threading;
+using System.Globalization;
+using System.Management;
+using System.Security.Principal;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Cognite.Simulator.Utils
+{
+     public static class ProcessUtils {
+        public static string GetProcessOwnerWmi(int processId)
+        {
+            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
+                throw new PlatformNotSupportedException("This method is only supported on Windows");
+            }
+            string query = $"SELECT * FROM Win32_Process WHERE ProcessId = {processId}";
+            using (var searcher = new ManagementObjectSearcher(query))
+            using (var results = searcher.Get())
+            {
+                foreach (ManagementObject process in results)
+                {
+                    try
+                    {
+                        object[] args = new object[] { "", "" };
+                        if (Convert.ToInt32(process.InvokeMethod("GetOwner", args)) == 0)
+                        {
+                            string user = (string)args[0];
+                            string domain = (string)args[1];
+                            return $"{domain}\\{user}";
+                        }
+                    }
+                    catch
+                    {
+                        return "Access Denied or Process Exited";
+                    }
+                }
+            }
+            return "No Owner Found";
path: Cognite.Simulator.Utils/ProcessUtils.cs
user: login: abdullah-cognite
body: we decided to throw an error here instead
created_at: 2025-02-28T11:06:34Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,226 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Cognite.Simulator.Utils
+{
+    /// <summary>
+    /// Class to handle license acquisition and release.
+    /// With this a license can be acquired and released after a certain period of time.
+    /// </summary>
+   public class LicenseController : IDisposable
+    {
+        private readonly TimeSpan _licenseLockTime;
+        private readonly object _lock = new object();
+        private Timer _releaseTimer;
+        private readonly Func<object> _releaseLicenseFunc;
+        private readonly Func<object> _acquireLicenseFunc;
+        private bool _licenseHeld;
+        private DateTime _lastUsageTime;
+        private DateTime _scheduledReleaseTime;
+        private readonly ILogger _logger;
+        private const int LoggingIntervalMs = 60000; // 1 minute
+        private readonly CancellationTokenSource _cts = new CancellationTokenSource();
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="LicenseController"/> class.
+        /// </summary>
+        /// <param name="licenseLockTime">The time span for which the license is locked.</param>
+        /// <param name="releaseLicenseFunc">The function to release the license.</param>
+        /// <param name="acquireLicenseFunc">The function to acquire the license.</param>
+        /// <param name="logger">The logger instance for logging information.</param>
+        /// <param name="cancellationToken">The cancellation token to observe while waiting for the task to complete. Optional.</param>
+        /// <exception cref="ArgumentNullException">Thrown when <paramref name="releaseLicenseFunc"/> is null.</exception>
+        public LicenseController(
+            TimeSpan licenseLockTime,
+            Func<object> releaseLicenseFunc,
+            Func<object> acquireLicenseFunc,
+            ILogger logger,
+            CancellationToken cancellationToken = new CancellationToken()
+        )
+        {
+            _licenseLockTime = licenseLockTime;
+            _licenseHeld = false;
+            _releaseLicenseFunc = releaseLicenseFunc ?? throw new ArgumentNullException(nameof(releaseLicenseFunc));
+            _acquireLicenseFunc = acquireLicenseFunc ?? throw new ArgumentNullException(nameof(acquireLicenseFunc));
+            _releaseTimer = new Timer(ReleaseLicenseAfterTimeout, null, Timeout.Infinite, Timeout.Infinite);
+            _logger = logger;
+            _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+            _ = StartLoggingTask(_cts.Token);
+        }
+
+        /// <summary>
+        /// Gets a boolean indicating whether the license is currently held or not.
+        /// </summary>
+        public bool LicenseHeld => _licenseHeld;
+
+        private async Task StartLoggingTask(CancellationToken cancellationToken)
+        {
+            try
+            {
+                while (!cancellationToken.IsCancellationRequested)
+                {
+                    LogLicenseStatus();
+                    await Task.Delay(LoggingIntervalMs, cancellationToken).ConfigureAwait(false);
+                }
+            }
+            catch (OperationCanceledException)
+            {
+                // Task was cancelled, log final status
+                LogLicenseStatus();
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error in license status logging task");
+            }
+        }
+
+        private void LogLicenseStatus()
+        {
+            _logger.LogInformation("License is currently {Status}", _licenseHeld ? "held" : "not held");
+            if (_licenseHeld)
+            {
+                var duration = DateTime.Now - _lastUsageTime;
+                _logger.LogInformation("License has been held for {Duration}", CommonUtils.FormatDuration(duration));
+                var timeUntilRelease = _scheduledReleaseTime - DateTime.Now;
+                _logger.LogInformation("License will be released in {TimeUntilRelease} (at {ReleaseTime})", 
+                    CommonUtils.FormatDuration(timeUntilRelease),
+                    _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+            }
+        }
+        
+        /// <summary>
+        /// Acquires a license by invoking the license acquisition function and sets the license status to held.
+        /// Also updates the release timer to ensure the license is released after a certain period.
+        /// </summary>
+        /// <exception cref="Exception">Thrown when an error occurs while acquiring the license.</exception>
+        public void AcquireLicense() {
+            lock (_lock)
+            {
+                try
+                {
+                    if (_licenseHeld)
+                    {
+                        _logger.LogInformation("License already held, skipping acquisition");
+                        return;
+                    }
+                    _logger.LogInformation("Attempting to acquire license");
+                    _acquireLicenseFunc();
+                    _licenseHeld = true;
+                    UpdateReleaseTimer();
+                    _logger.LogInformation("License acquired successfully");
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, "Error acquiring license");
+                    throw;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license after the timeout has elapsed. This is called by the timer.
+        /// </summary>
+        private void ReleaseLicenseAfterTimeout(object state)
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Attempting to release license at scheduled time {ScheduledTime}", _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+                if (DateTime.Now >= _scheduledReleaseTime)
+                {
+                    _releaseLicenseFunc();
+                    _licenseHeld = false;  
+                    _logger.LogInformation("License released successfully");
+                }
+                else
+                {
+                    _logger.LogInformation("License not released - current time {CurrentTime} is before scheduled release time", 
+                        DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license immediately. This is used externally to reset the internal state when an external process finds out
+        /// that the license is no longer held.
+        /// </summary>
+        public void ReleaseLicenseWithoutCallback() {
+            lock (_lock)
+            {
+                _licenseHeld = false;
+                _lastUsageTime = DateTime.Now;
+                _scheduledReleaseTime = DateTime.Now;
+                _releaseTimer.Change(Timeout.Infinite, Timeout.Infinite);
+                _logger.LogInformation("License released without callback");
+            }
+        }
+
+        private void UpdateReleaseTimer() {
+            _lastUsageTime = DateTime.Now;
+            // Extend release time
+            _scheduledReleaseTime = DateTime.Now.Add(_licenseLockTime);
+            _logger.LogInformation("License release scheduled for {ReleaseTime}", 
+            _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+            _releaseTimer.Change(Timeout.Infinite, Timeout.Infinite);
+        }
+
+        /// <summary>
+        /// Begins usage of the license.
+        /// </summary>
+        /// <returns>An <see cref="IDisposable"/> object that can be used to end the license usage.</returns>
+        public IDisposable BeginUsage()
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Starting license usage");
+                UpdateReleaseTimer();
+                return new LicenseUsageScope(this);
+            }
+        }
+
+        private void EndUsage()
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Ending license usage, scheduling release");
+                _lastUsageTime = DateTime.Now;
+                _scheduledReleaseTime = _lastUsageTime.Add(_licenseLockTime);
+                _logger.LogInformation("License release scheduled for {ReleaseTime}", 
+                    _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+                _releaseTimer.Change(_licenseLockTime, Timeout.InfiniteTimeSpan);
+            }
+        }
+
+        /// <summary>
+        /// Releases the license immediately.
+        /// </summary>
+        public void Dispose()
+        {
+            _releaseTimer?.Dispose();
+            _releaseTimer = null;
+            GC.SuppressFinalize(this);
path: Cognite.Simulator.Utils/LicenseController.cs
user: login: abdullah-cognite
body: not needed
created_at: 2025-02-28T11:06:40Z
author_association: COLLABORATOR
diff_hunk: @@ -29,7 +27,28 @@ jobs:
         AZURE_TENANT: ${{ secrets.AZURE_TENANT }}
         AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
         AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
-    - name: Upload coverage to Codecov
-      uses: codecov/codecov-action@v5
+    # - name: Upload coverage to Codecov
+    #   uses: codecov/codecov-action@v5
+    #   with:
+    #     token: ${{ secrets.CODECOV_TOKEN }}
+
path: .github/workflows/buildandtest.yml
user: login: sagar-thalwar
body: Question: Why are we commenting this? 
created_at: 2025-02-28T11:39:40Z
author_association: COLLABORATOR
diff_hunk: @@ -29,7 +27,28 @@ jobs:
         AZURE_TENANT: ${{ secrets.AZURE_TENANT }}
         AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
         AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
-    - name: Upload coverage to Codecov
-      uses: codecov/codecov-action@v5
+    # - name: Upload coverage to Codecov
+    #   uses: codecov/codecov-action@v5
+    #   with:
+    #     token: ${{ secrets.CODECOV_TOKEN }}
+
path: .github/workflows/buildandtest.yml
user: login: abdullah-cognite
body: The intention was that now because the windows GH action is running 100% of the tests and it is reporting coverage to codecov. The linux one was unable to run ProcessUtilities tests.
created_at: 2025-02-28T11:46:09Z
author_association: COLLABORATOR
diff_hunk: @@ -29,7 +27,24 @@ jobs:
         AZURE_TENANT: ${{ secrets.AZURE_TENANT }}
         AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
         AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
-    - name: Upload coverage to Codecov
-      uses: codecov/codecov-action@v5
+
+  build-windows:
+    runs-on: windows-latest
path: .github/workflows/buildandtest.yml
user: login: polomani
body: if implementation is the same, you can use a matrix, instead of copy-pasting the job

```
runs-on: ${{ matrix.os }}
        strategy:
            matrix:
                os: [ubuntu-latest, windows-latest]
```
created_at: 2025-03-03T09:31:06Z
author_association: COLLABORATOR
diff_hunk: @@ -29,7 +27,28 @@ jobs:
         AZURE_TENANT: ${{ secrets.AZURE_TENANT }}
         AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
         AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
-    - name: Upload coverage to Codecov
-      uses: codecov/codecov-action@v5
+    # - name: Upload coverage to Codecov
+    #   uses: codecov/codecov-action@v5
+    #   with:
+    #     token: ${{ secrets.CODECOV_TOKEN }}
+
path: .github/workflows/buildandtest.yml
user: login: polomani
body: > it is reporting coverage to codecov

it is not, it only gathers the coverage. upload is a separate step

to merge this PR we need to fix the codecov.

- gather coverage from linux job
- gather coverage from windows job
- move cov files to one folder
- upload to codecov
created_at: 2025-03-03T09:33:36Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,176 @@
+using Xunit;
+using System;
+using System.Threading;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Cognite.Simulator.Utils;
+
+namespace Cognite.Simulator.Tests.UtilsTests{
+
+    public enum TestLicenseState
+    {
+        Released,
+        Held
+    }
+
+    public class LicenseControllerTests
+    {
+        private readonly Mock<ILogger> _loggerMock = new Mock<ILogger>();
+
+        private (LicenseController controller, StateHolder<TestLicenseState> stateHolder) CreateTracker(
+            Mock<Func<object>>? releaseMock = null,
+            Mock<Func<object>>? acquireMock = null,
+            TimeSpan? licenseLockTime = null)
+        {
+            var stateHolder = new StateHolder<TestLicenseState> { State = TestLicenseState.Released };
+            
+            releaseMock ??= new Mock<Func<object>>();
+            acquireMock ??= new Mock<Func<object>>();
+            
+            // Set up the mocks to track the state changes
+            releaseMock.Setup(m => m()).Callback(() => stateHolder.State = TestLicenseState.Released);
+            acquireMock.Setup(m => m()).Callback(() => stateHolder.State = TestLicenseState.Held);
+            
+            var tracker = new LicenseController(
+                licenseLockTime ?? TimeSpan.FromMilliseconds(100),
+                () => { releaseMock.Object(); },
+                (CancellationToken _token) => { acquireMock.Object(); },
+                _loggerMock.Object
+            );
+            
+            return (tracker, stateHolder);
+        }
+
+        // Simple class to hold state that can be captured in lambdas
+        private class StateHolder<T>
+        {
+            public T State { get; set; }
+        }
+
+
+        [Fact]
+        public void LicenseTracker_ShouldReleaseLicense_WhenNotInUse()
+        {
+
+            // Arrange
+            Mock<Func<object>> _releaseLicenseFuncMock = new Mock<Func<object>>();
+            Mock<Func<object>> _acquireLicenseFuncMock = new Mock<Func<object>>();
+            var licenseLockTime = TimeSpan.FromMilliseconds(100);
+            var (tracker, license) = CreateTracker( _releaseLicenseFuncMock, _acquireLicenseFuncMock, licenseLockTime);
+
+            // Check if the default state of the license is released
+            Assert.Equal(TestLicenseState.Released, license.State);
+
+            // Check if acquiring the license calls the acquire license function and changes the state
+            tracker.AcquireLicense(CancellationToken.None);
+            _acquireLicenseFuncMock.Verify(f => f(), Times.Once);
+            Assert.Equal(TestLicenseState.Held, license.State);
+            Assert.True(tracker.LicenseHeld);
+
+            using (tracker.BeginUsage()) { /* Use and immediately release */ } 
+            
+            // Add buffer time to account for timer inconsistencies
+            Thread.Sleep(400);
+            
+            // Check if the release license callback was called
+            _releaseLicenseFuncMock.Verify(f => f(), Times.Once);
+
+            // Check if the license is released
path: Cognite.Simulator.Tests/UtilsTests/LicenseControllerTest.cs
user: login: polomani
body: Please leave comments that only provide any additional information.
A lot of these saying exactly the same as the code line
created_at: 2025-03-03T09:45:13Z
author_association: COLLABORATOR
diff_hunk: @@ -1,55 +1,90 @@
 name: Build and Test
 
-on:
-  push:
-    branches: [ "main" ]
-  pull_request:
-    branches: [ "main" ]
+on: [push, pull_request]
 
 jobs:
   build-ubuntu:
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v4
-    - name: Setup .NET
-      uses: actions/setup-dotnet@v4
-      with:
-        dotnet-version: 8
-    - name: Restore dependencies
-      run: dotnet restore
-    - name: Build
-      run: dotnet build --no-restore
-    - name: Test
-      run: dotnet test --no-restore --verbosity normal /p:Exclude="[*.Test]*" /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput=TestResults/ ./Cognite.Simulator.Tests/  --logger:"console"
-      env:
-        COGNITE_PROJECT: ${{ secrets.COGNITE_PROJECT }}
-        COGNITE_HOST: ${{ secrets.COGNITE_HOST }}
-        AZURE_TENANT: ${{ secrets.AZURE_TENANT }}
-        AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
-        AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
-
+      - uses: actions/checkout@v4
+      - name: Setup .NET
+        uses: actions/setup-dotnet@v4
+        with:
+          dotnet-version: 8
+      - name: Restore dependencies
+        run: dotnet restore
+      - name: Build
+        run: dotnet build --no-restore
+      - name: Test
+        run: dotnet test --no-restore --verbosity normal /p:Exclude="[*.Test]*" /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput=TestResults/ ./Cognite.Simulator.Tests/ --logger:"console"
+        env:
+          COGNITE_PROJECT: ${{ secrets.COGNITE_PROJECT }}
+          COGNITE_HOST: ${{ secrets.COGNITE_HOST }}
+          AZURE_TENANT: ${{ secrets.AZURE_TENANT }}
+          AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
+          AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
+      - name: Upload coverage to artifacts
+        uses: actions/upload-artifact@v4
+        with:
+          name: coverage-ubuntu
+          path: ./Cognite.Simulator.Tests/TestResults/coverage.opencover.xml
+          retention-days: 1
 
   build-windows:
     runs-on: windows-latest
     steps:
-    - uses: actions/checkout@v4
-    - name: Setup .NET
-      uses: actions/setup-dotnet@v4
-      with:
-        dotnet-version: 8
-    - name: Restore dependencies
-      run: dotnet restore
-    - name: Build
-      run: dotnet build --no-restore
-    - name: Test
-      run: dotnet test --no-restore --verbosity normal /p:Exclude="[*.Test]*" /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput=TestResults/ ./Cognite.Simulator.Tests/  --logger:"console"
-      env:
-        COGNITE_PROJECT: ${{ secrets.COGNITE_PROJECT }}
-        COGNITE_HOST: ${{ secrets.COGNITE_HOST }}
-        AZURE_TENANT: ${{ secrets.AZURE_TENANT }}
-        AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
-        AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
-    - name: Upload coverage to Codecov
-      uses: codecov/codecov-action@v5
-      with:
-        token: ${{ secrets.CODECOV_TOKEN }}
+      - uses: actions/checkout@v4
+      - name: Setup .NET
+        uses: actions/setup-dotnet@v4
+        with:
+          dotnet-version: 8
+      - name: Restore dependencies
+        run: dotnet restore
+      - name: Build
+        run: dotnet build --no-restore
+      - name: Test
+        run: dotnet test --no-restore --verbosity normal /p:Exclude="[*.Test]*" /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput=TestResults/ ./Cognite.Simulator.Tests/ --logger:"console"
+        env:
+          COGNITE_PROJECT: ${{ secrets.COGNITE_PROJECT }}
+          COGNITE_HOST: ${{ secrets.COGNITE_HOST }}
+          AZURE_TENANT: ${{ secrets.AZURE_TENANT }}
+          AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
+          AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
+      - name: Upload coverage to artifacts
+        uses: actions/upload-artifact@v4
+        with:
+          name: coverage-windows
+          path: ./Cognite.Simulator.Tests/TestResults/coverage.opencover.xml
+          retention-days: 1
+
+  upload-coverage:
+    needs: [build-ubuntu, build-windows]
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      
+      - name: Download Ubuntu coverage
+        uses: actions/download-artifact@v4
+        with:
+          name: coverage-ubuntu
+          path: ./coverage-ubuntu
+      
+      - name: Download Windows coverage
+        uses: actions/download-artifact@v4
+        with:
+          name: coverage-windows
+          path: ./coverage-windows
+      
+      - name: Install ReportGenerator
+        run: dotnet tool install -g dotnet-reportgenerator-globaltool
+      
+      - name: Merge coverage reports
+        run: |
+          reportgenerator "-reports:./coverage-ubuntu/coverage.opencover.xml;./coverage-windows/coverage.opencover.xml" "-targetdir:./merged-coverage" "-reporttypes:Cobertura"
path: .github/workflows/buildandtest.yml
user: login: polomani
body: codecov supports multifile uploads
created_at: 2025-03-03T09:50:01Z
author_association: COLLABORATOR
diff_hunk: @@ -29,7 +27,28 @@ jobs:
         AZURE_TENANT: ${{ secrets.AZURE_TENANT }}
         AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
         AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
-    - name: Upload coverage to Codecov
-      uses: codecov/codecov-action@v5
+    # - name: Upload coverage to Codecov
+    #   uses: codecov/codecov-action@v5
+    #   with:
+    #     token: ${{ secrets.CODECOV_TOKEN }}
+
path: .github/workflows/buildandtest.yml
user: login: abdullah-cognite
body: ^ updated the gh action to do above
created_at: 2025-03-03T10:14:31Z
author_association: COLLABORATOR
diff_hunk: @@ -8,28 +8,59 @@ on:
 
 jobs:
   build:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        os: [ubuntu-latest, windows-latest]
+    steps:
+      - uses: actions/checkout@v4
+      - name: Setup .NET
+        uses: actions/setup-dotnet@v4
+        with:
+          dotnet-version: 8
+      - name: Restore dependencies
+        run: dotnet restore
+      - name: Build
+        run: dotnet build --no-restore
+      - name: Test
+        run: dotnet test --no-restore --verbosity normal /p:Exclude="[*.Test]*" /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput=TestResults/ ./Cognite.Simulator.Tests/ --logger:"console"
+        env:
+          COGNITE_PROJECT: ${{ secrets.COGNITE_PROJECT }}
+          COGNITE_HOST: ${{ secrets.COGNITE_HOST }}
+          AZURE_TENANT: ${{ secrets.AZURE_TENANT }}
+          AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
+          AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
+      - name: Upload coverage to artifacts
+        uses: actions/upload-artifact@v4
+        with:
+          name: coverage-${{ matrix.os }}
+          path: ./Cognite.Simulator.Tests/TestResults/coverage.opencover.xml
+          retention-days: 1
 
+  upload-coverage:
+    needs: build
     runs-on: ubuntu-latest
-
     steps:
-    - uses: actions/checkout@v4
-    - name: Setup .NET
-      uses: actions/setup-dotnet@v4
-      with:
-        dotnet-version: 8
-    - name: Restore dependencies
-      run: dotnet restore
-    - name: Build
-      run: dotnet build --no-restore
-    - name: Test
-      run: dotnet test --no-restore --verbosity normal /p:Exclude="[*.Test]*" /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:CoverletOutput=TestResults/ ./Cognite.Simulator.Tests/  --logger:"console"
-      env:
-        COGNITE_PROJECT: ${{ secrets.COGNITE_PROJECT }}
-        COGNITE_HOST: ${{ secrets.COGNITE_HOST }}
-        AZURE_TENANT: ${{ secrets.AZURE_TENANT }}
-        AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
-        AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
-    - name: Upload coverage to Codecov
-      uses: codecov/codecov-action@v5
-      with:
-        token: ${{ secrets.CODECOV_TOKEN }}
+      - uses: actions/checkout@v4
+      
+      - name: Download Ubuntu coverage
+        uses: actions/download-artifact@v4
+        with:
+          name: coverage-ubuntu-latest
+          path: ./coverage-ubuntu
+      
+      - name: Download Windows coverage
+        uses: actions/download-artifact@v4
+        with:
+          name: coverage-windows-latest
+          path: ./coverage-windows
+      
+      - name: Install ReportGenerator
+        run: dotnet tool install -g dotnet-reportgenerator-globaltool
path: .github/workflows/buildandtest.yml
user: login: polomani
body: ```suggestion
```
created_at: 2025-03-03T13:35:20Z
author_association: COLLABORATOR
diff_hunk: @@ -2,6 +2,14 @@
 
 All notable changes to this project will be documented in this file.
 
+## Release v1.0.0-beta-014 (2025-02-20)
+
+### Features
+
+* Fix bug where invalid config file lead to connector running in a loop displaying connection error
+* Introduce support for license tracking and holding.
+* Introduce support for Process termination.
path: CHANGELOG.md
user: login: polomani
body: ```suggestion
* Fixed bug where invalid config file led to connector running in a loop displaying connection error.
* Introduced support for license tracking and holding.
* Introduced support for Process termination.
```
minor
created_at: 2025-03-03T13:38:18Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,233 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Cognite.Simulator.Utils
+{
+
+    /// <summary>
+    /// Class to handle license acquisition and release.
+    /// With this a license can be acquired and released after a certain period of time.
+    /// </summary>
+    public class LicenseController : IDisposable
+    {
+        private readonly TimeSpan _licenseLockTime;
+        private readonly object _lock = new object();
+        private ITimer _releaseTimer;
+        private readonly Action _releaseLicenseFunc;
+        private readonly Action<CancellationToken> _acquireLicenseFunc;
+        private bool _licenseHeld;
+        private DateTimeOffset _lastUsageTime;
+        private DateTimeOffset _scheduledReleaseTime;
+        private readonly ILogger _logger;
+        private readonly TimeProvider _timeProvider;
+        private const int LoggingIntervalMs = 60000; // 1 minute
+        private readonly CancellationTokenSource _cts = new CancellationTokenSource();
+
+        /// <summary>
+        /// Class to handle license acquisition and release.
+        /// When this is used in a using block, the license is released based on the time span provided after the block ends.
+        /// When the block ends a timer is started to release the license after the time span has elapsed.
+        /// </summary>
+        /// <param name="licenseLockTime">The time span for which the license is locked.</param>
+        /// <param name="releaseLicenseFunc">The function to release the license.</param>
+        /// <param name="acquireLicenseFunc">The function to acquire the license.</param>
+        /// <param name="logger">The logger instance for logging information.</param>
+        /// <param name="timeProvider">The time provider to use for all time-related operations.</param>
+        /// <param name="cancellationToken">The cancellation token to observe while waiting for the task to complete. Optional.</param>
+        /// <exception cref="ArgumentNullException">Thrown when <paramref name="releaseLicenseFunc"/> is null.</exception>
+        public LicenseController(
+            TimeSpan licenseLockTime,
+            Action releaseLicenseFunc,
+            Action<CancellationToken> acquireLicenseFunc,
+            ILogger logger,
+            TimeProvider timeProvider = null,
+            CancellationToken cancellationToken = new CancellationToken()
+        )
+        {
+            _licenseLockTime = licenseLockTime;
+            _licenseHeld = false;
+            _releaseLicenseFunc = releaseLicenseFunc ?? throw new ArgumentNullException(nameof(releaseLicenseFunc));
+            _acquireLicenseFunc = acquireLicenseFunc ?? throw new ArgumentNullException(nameof(acquireLicenseFunc));
+            _logger = logger;
+            _timeProvider = timeProvider == null? TimeProvider.System : timeProvider;
+            _releaseTimer = _timeProvider.CreateTimer(ReleaseLicenseAfterTimeout, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
+            _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+            
+            // Dispose the timer on cancellation
+            _cts.Token.Register(() => {
+                _releaseTimer?.Dispose();
+                _releaseTimer = null;
+            }); 
+            _ = StartLoggingTask(_cts.Token);
+        }
+
+        /// <summary>
+        /// Gets a boolean indicating whether the license is currently held or not.
+        /// </summary>
+        public bool LicenseHeld => _licenseHeld;
+
+        private async Task StartLoggingTask(CancellationToken cancellationToken)
+        {
+            try
+            {
+                while (!cancellationToken.IsCancellationRequested)
+                {
+                    LogLicenseStatus();
+                    await Task.Delay(LoggingIntervalMs, cancellationToken).ConfigureAwait(false);
+                }
+            }
+            catch (OperationCanceledException)
+            {
+                // Task was cancelled, log final status
+                LogLicenseStatus();
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error in license status logging task");
+            }
+        }
+
+        private void LogLicenseStatus()
+        {
+            _logger.LogInformation("License is currently {Status}", _licenseHeld ? "held" : "not held");
+            if (_licenseHeld)
+            {
+                var duration = _timeProvider.GetUtcNow() - _lastUsageTime;
+                _logger.LogInformation("License has been held for {Duration}", CommonUtils.FormatDuration(duration));
+                var timeUntilRelease = _scheduledReleaseTime - _timeProvider.GetUtcNow();
+                _logger.LogInformation("License will be released in {TimeUntilRelease} (at {ReleaseTime})", 
+                    CommonUtils.FormatDuration(timeUntilRelease),
+                    _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+            }
+        }
+        
+        /// <summary>
+        /// Acquires a license by invoking the license acquisition function and sets the license status to held.
+        /// Also updates the release timer to ensure the license is released after a certain period.
+        /// </summary>
+        /// <exception cref="Exception">Thrown when an error occurs while acquiring the license.</exception>
+        public void AcquireLicense(CancellationToken cancellationToken) {
+            lock (_lock)
+            {
+                try
+                {
+                    if (_licenseHeld)
+                    {
+                        _logger.LogInformation("License already held, skipping acquisition");
+                        return;
+                    }
+                    _logger.LogInformation("Attempting to acquire license");
+                    _acquireLicenseFunc(cancellationToken);
+                    _licenseHeld = true;
+                    PauseTimer();
+                    _logger.LogInformation("License acquired successfully");
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, "Error acquiring license");
+                    throw;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license after the timeout has elapsed. This is called by the timer.
+        /// </summary>
+        private void ReleaseLicenseAfterTimeout(object state)
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Attempting to release license at scheduled time {ScheduledTime}", _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+                if (_timeProvider.GetUtcNow() >= _scheduledReleaseTime)
+                {
+                    _releaseLicenseFunc();
+                    _licenseHeld = false;  
+                    _logger.LogInformation("License released successfully");
+                }
+                else
+                {
+                    _logger.LogInformation("License not released - current time {CurrentTime} is before scheduled release time", 
+                        _timeProvider.GetUtcNow().ToString("yyyy-MM-dd HH:mm:ss"));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license immediately. This is used externally to reset the internal state when an external process finds out
+        /// that the license is no longer held.
+        /// </summary>
+        public void ClearLicenseState() {
+            lock (_lock)
+            {
+                _licenseHeld = false;
+                _lastUsageTime = _timeProvider.GetUtcNow();
+                _scheduledReleaseTime = _timeProvider.GetUtcNow();
+                _releaseTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
+                _logger.LogInformation("License released without callback");
+            }
+        }
+
+        private void PauseTimer() {
+            _logger.LogInformation("License usage started pausing release timer");
+            _releaseTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
+        }
+
+        /// <summary>
+        /// Begins usage of the license.
+        /// </summary>
+        /// <returns>An <see cref="IDisposable"/> object that can be used to end the license usage.</returns>
+        public IDisposable BeginUsage()
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Starting license usage");
+                PauseTimer();
+                return new LicenseUsageScope(this);
+            }
+        }
+
+        private void EndUsage()
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Ending license usage, scheduling release");
+                _lastUsageTime = _timeProvider.GetUtcNow();
+                _scheduledReleaseTime = _lastUsageTime.Add(_licenseLockTime);
+                _logger.LogInformation("License release scheduled for {ReleaseTime}", 
+                _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
path: Cognite.Simulator.Utils/LicenseController.cs
user: login: polomani
body: ```suggestion
```
this is noop?
created_at: 2025-03-03T13:46:14Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,233 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Cognite.Simulator.Utils
+{
+
+    /// <summary>
+    /// Class to handle license acquisition and release.
+    /// With this a license can be acquired and released after a certain period of time.
+    /// </summary>
+    public class LicenseController : IDisposable
+    {
+        private readonly TimeSpan _licenseLockTime;
+        private readonly object _lock = new object();
+        private ITimer _releaseTimer;
+        private readonly Action _releaseLicenseFunc;
+        private readonly Action<CancellationToken> _acquireLicenseFunc;
+        private bool _licenseHeld;
+        private DateTimeOffset _lastUsageTime;
+        private DateTimeOffset _scheduledReleaseTime;
+        private readonly ILogger _logger;
+        private readonly TimeProvider _timeProvider;
+        private const int LoggingIntervalMs = 60000; // 1 minute
+        private readonly CancellationTokenSource _cts = new CancellationTokenSource();
+
+        /// <summary>
+        /// Class to handle license acquisition and release.
+        /// When this is used in a using block, the license is released based on the time span provided after the block ends.
+        /// When the block ends a timer is started to release the license after the time span has elapsed.
+        /// </summary>
+        /// <param name="licenseLockTime">The time span for which the license is locked.</param>
+        /// <param name="releaseLicenseFunc">The function to release the license.</param>
+        /// <param name="acquireLicenseFunc">The function to acquire the license.</param>
+        /// <param name="logger">The logger instance for logging information.</param>
+        /// <param name="timeProvider">The time provider to use for all time-related operations.</param>
+        /// <param name="cancellationToken">The cancellation token to observe while waiting for the task to complete. Optional.</param>
+        /// <exception cref="ArgumentNullException">Thrown when <paramref name="releaseLicenseFunc"/> is null.</exception>
+        public LicenseController(
+            TimeSpan licenseLockTime,
+            Action releaseLicenseFunc,
+            Action<CancellationToken> acquireLicenseFunc,
+            ILogger logger,
+            TimeProvider timeProvider = null,
+            CancellationToken cancellationToken = new CancellationToken()
+        )
+        {
+            _licenseLockTime = licenseLockTime;
+            _licenseHeld = false;
+            _releaseLicenseFunc = releaseLicenseFunc ?? throw new ArgumentNullException(nameof(releaseLicenseFunc));
+            _acquireLicenseFunc = acquireLicenseFunc ?? throw new ArgumentNullException(nameof(acquireLicenseFunc));
+            _logger = logger;
+            _timeProvider = timeProvider == null? TimeProvider.System : timeProvider;
+            _releaseTimer = _timeProvider.CreateTimer(ReleaseLicenseAfterTimeout, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
+            _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+            
+            // Dispose the timer on cancellation
+            _cts.Token.Register(() => {
+                _releaseTimer?.Dispose();
+                _releaseTimer = null;
+            }); 
+            _ = StartLoggingTask(_cts.Token);
+        }
+
+        /// <summary>
+        /// Gets a boolean indicating whether the license is currently held or not.
+        /// </summary>
+        public bool LicenseHeld => _licenseHeld;
+
+        private async Task StartLoggingTask(CancellationToken cancellationToken)
+        {
+            try
+            {
+                while (!cancellationToken.IsCancellationRequested)
+                {
+                    LogLicenseStatus();
+                    await Task.Delay(LoggingIntervalMs, cancellationToken).ConfigureAwait(false);
+                }
+            }
+            catch (OperationCanceledException)
+            {
+                // Task was cancelled, log final status
+                LogLicenseStatus();
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error in license status logging task");
+            }
+        }
+
+        private void LogLicenseStatus()
+        {
+            _logger.LogInformation("License is currently {Status}", _licenseHeld ? "held" : "not held");
+            if (_licenseHeld)
+            {
+                var duration = _timeProvider.GetUtcNow() - _lastUsageTime;
+                _logger.LogInformation("License has been held for {Duration}", CommonUtils.FormatDuration(duration));
+                var timeUntilRelease = _scheduledReleaseTime - _timeProvider.GetUtcNow();
+                _logger.LogInformation("License will be released in {TimeUntilRelease} (at {ReleaseTime})", 
+                    CommonUtils.FormatDuration(timeUntilRelease),
+                    _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+            }
+        }
+        
+        /// <summary>
+        /// Acquires a license by invoking the license acquisition function and sets the license status to held.
+        /// Also updates the release timer to ensure the license is released after a certain period.
+        /// </summary>
+        /// <exception cref="Exception">Thrown when an error occurs while acquiring the license.</exception>
+        public void AcquireLicense(CancellationToken cancellationToken) {
+            lock (_lock)
+            {
+                try
+                {
+                    if (_licenseHeld)
+                    {
+                        _logger.LogInformation("License already held, skipping acquisition");
+                        return;
+                    }
+                    _logger.LogInformation("Attempting to acquire license");
+                    _acquireLicenseFunc(cancellationToken);
+                    _licenseHeld = true;
+                    PauseTimer();
+                    _logger.LogInformation("License acquired successfully");
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, "Error acquiring license");
+                    throw;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license after the timeout has elapsed. This is called by the timer.
+        /// </summary>
+        private void ReleaseLicenseAfterTimeout(object state)
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Attempting to release license at scheduled time {ScheduledTime}", _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+                if (_timeProvider.GetUtcNow() >= _scheduledReleaseTime)
+                {
+                    _releaseLicenseFunc();
+                    _licenseHeld = false;  
+                    _logger.LogInformation("License released successfully");
+                }
+                else
+                {
+                    _logger.LogInformation("License not released - current time {CurrentTime} is before scheduled release time", 
+                        _timeProvider.GetUtcNow().ToString("yyyy-MM-dd HH:mm:ss"));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license immediately. This is used externally to reset the internal state when an external process finds out
+        /// that the license is no longer held.
+        /// </summary>
+        public void ClearLicenseState() {
+            lock (_lock)
+            {
+                _licenseHeld = false;
+                _lastUsageTime = _timeProvider.GetUtcNow();
+                _scheduledReleaseTime = _timeProvider.GetUtcNow();
+                _releaseTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
+                _logger.LogInformation("License released without callback");
+            }
+        }
+
+        private void PauseTimer() {
+            _logger.LogInformation("License usage started pausing release timer");
path: Cognite.Simulator.Utils/LicenseController.cs
user: login: polomani
body: ```suggestion
            _logger.LogInformation("License usage started: pausing release timer");
```
created_at: 2025-03-03T13:48:04Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,233 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Cognite.Simulator.Utils
+{
+
+    /// <summary>
+    /// Class to handle license acquisition and release.
+    /// With this a license can be acquired and released after a certain period of time.
+    /// </summary>
+    public class LicenseController : IDisposable
+    {
+        private readonly TimeSpan _licenseLockTime;
+        private readonly object _lock = new object();
+        private ITimer _releaseTimer;
+        private readonly Action _releaseLicenseFunc;
+        private readonly Action<CancellationToken> _acquireLicenseFunc;
+        private bool _licenseHeld;
+        private DateTimeOffset _lastUsageTime;
+        private DateTimeOffset _scheduledReleaseTime;
+        private readonly ILogger _logger;
+        private readonly TimeProvider _timeProvider;
+        private const int LoggingIntervalMs = 60000; // 1 minute
+        private readonly CancellationTokenSource _cts = new CancellationTokenSource();
+
+        /// <summary>
+        /// Class to handle license acquisition and release.
+        /// When this is used in a using block, the license is released based on the time span provided after the block ends.
+        /// When the block ends a timer is started to release the license after the time span has elapsed.
+        /// </summary>
+        /// <param name="licenseLockTime">The time span for which the license is locked.</param>
+        /// <param name="releaseLicenseFunc">The function to release the license.</param>
+        /// <param name="acquireLicenseFunc">The function to acquire the license.</param>
+        /// <param name="logger">The logger instance for logging information.</param>
+        /// <param name="timeProvider">The time provider to use for all time-related operations.</param>
+        /// <param name="cancellationToken">The cancellation token to observe while waiting for the task to complete. Optional.</param>
+        /// <exception cref="ArgumentNullException">Thrown when <paramref name="releaseLicenseFunc"/> is null.</exception>
+        public LicenseController(
+            TimeSpan licenseLockTime,
+            Action releaseLicenseFunc,
+            Action<CancellationToken> acquireLicenseFunc,
+            ILogger logger,
+            TimeProvider timeProvider = null,
+            CancellationToken cancellationToken = new CancellationToken()
+        )
+        {
+            _licenseLockTime = licenseLockTime;
+            _licenseHeld = false;
+            _releaseLicenseFunc = releaseLicenseFunc ?? throw new ArgumentNullException(nameof(releaseLicenseFunc));
+            _acquireLicenseFunc = acquireLicenseFunc ?? throw new ArgumentNullException(nameof(acquireLicenseFunc));
+            _logger = logger;
+            _timeProvider = timeProvider == null? TimeProvider.System : timeProvider;
+            _releaseTimer = _timeProvider.CreateTimer(ReleaseLicenseAfterTimeout, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
+            _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+            
+            // Dispose the timer on cancellation
+            _cts.Token.Register(() => {
+                _releaseTimer?.Dispose();
+                _releaseTimer = null;
+            }); 
+            _ = StartLoggingTask(_cts.Token);
+        }
+
+        /// <summary>
+        /// Gets a boolean indicating whether the license is currently held or not.
+        /// </summary>
+        public bool LicenseHeld => _licenseHeld;
+
+        private async Task StartLoggingTask(CancellationToken cancellationToken)
+        {
+            try
+            {
+                while (!cancellationToken.IsCancellationRequested)
+                {
+                    LogLicenseStatus();
+                    await Task.Delay(LoggingIntervalMs, cancellationToken).ConfigureAwait(false);
+                }
+            }
+            catch (OperationCanceledException)
+            {
+                // Task was cancelled, log final status
+                LogLicenseStatus();
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error in license status logging task");
+            }
+        }
+
+        private void LogLicenseStatus()
+        {
+            _logger.LogInformation("License is currently {Status}", _licenseHeld ? "held" : "not held");
+            if (_licenseHeld)
+            {
+                var duration = _timeProvider.GetUtcNow() - _lastUsageTime;
+                _logger.LogInformation("License has been held for {Duration}", CommonUtils.FormatDuration(duration));
+                var timeUntilRelease = _scheduledReleaseTime - _timeProvider.GetUtcNow();
+                _logger.LogInformation("License will be released in {TimeUntilRelease} (at {ReleaseTime})", 
+                    CommonUtils.FormatDuration(timeUntilRelease),
+                    _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+            }
+        }
+        
+        /// <summary>
+        /// Acquires a license by invoking the license acquisition function and sets the license status to held.
+        /// Also updates the release timer to ensure the license is released after a certain period.
+        /// </summary>
+        /// <exception cref="Exception">Thrown when an error occurs while acquiring the license.</exception>
+        public void AcquireLicense(CancellationToken cancellationToken) {
+            lock (_lock)
+            {
+                try
+                {
+                    if (_licenseHeld)
+                    {
+                        _logger.LogInformation("License already held, skipping acquisition");
+                        return;
+                    }
+                    _logger.LogInformation("Attempting to acquire license");
+                    _acquireLicenseFunc(cancellationToken);
+                    _licenseHeld = true;
+                    PauseTimer();
+                    _logger.LogInformation("License acquired successfully");
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, "Error acquiring license");
+                    throw;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license after the timeout has elapsed. This is called by the timer.
+        /// </summary>
+        private void ReleaseLicenseAfterTimeout(object state)
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Attempting to release license at scheduled time {ScheduledTime}", _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+                if (_timeProvider.GetUtcNow() >= _scheduledReleaseTime)
+                {
+                    _releaseLicenseFunc();
+                    _licenseHeld = false;  
+                    _logger.LogInformation("License released successfully");
+                }
+                else
+                {
+                    _logger.LogInformation("License not released - current time {CurrentTime} is before scheduled release time", 
+                        _timeProvider.GetUtcNow().ToString("yyyy-MM-dd HH:mm:ss"));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license immediately. This is used externally to reset the internal state when an external process finds out
+        /// that the license is no longer held.
+        /// </summary>
+        public void ClearLicenseState() {
+            lock (_lock)
+            {
+                _licenseHeld = false;
+                _lastUsageTime = _timeProvider.GetUtcNow();
+                _scheduledReleaseTime = _timeProvider.GetUtcNow();
+                _releaseTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
+                _logger.LogInformation("License released without callback");
path: Cognite.Simulator.Utils/LicenseController.cs
user: login: polomani
body: ```suggestion
                _logger.LogInformation("License released forcefully");
```

created_at: 2025-03-03T13:49:39Z
author_association: COLLABORATOR
diff_hunk: @@ -2,6 +2,14 @@
 
 All notable changes to this project will be documented in this file.
 
+## Release v1.0.0-beta-014 (2025-02-20)
+
+### Features
+
+* Fix bug where invalid config file lead to connector running in a loop displaying connection error
+* Introduce support for license tracking and holding.
+* Introduce support for Process termination.
path: CHANGELOG.md
user: login: polomani
body: mention that `TerminateProcess` config was removed
created_at: 2025-03-03T13:53:49Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,178 @@
+using Xunit;
+using System;
+using System.Threading;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Cognite.Simulator.Utils;
+using Cognite.Extractor.Common;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Time.Testing;
+
+namespace Cognite.Simulator.Tests.UtilsTests{
+
+    public enum TestLicenseState
+    {
+        Released,
+        Held
+    }
+
+    public class LicenseControllerTests
+    {
+        private readonly Mock<ILogger> _loggerMock = new Mock<ILogger>();
+
+        private FakeTimeProvider fakeTimeProvider = new FakeTimeProvider();
+
+        private (LicenseController controller, StateHolder<TestLicenseState> stateHolder) CreateTracker(
+            Mock<Func<object>>? releaseMock = null,
+            Mock<Func<object>>? acquireMock = null,
+            TimeSpan? licenseLockTime = null,
+            TimeProvider timeProvider = null)
+        {
+            var stateHolder = new StateHolder<TestLicenseState> { State = TestLicenseState.Released };
+            
+            releaseMock ??= new Mock<Func<object>>();
+            acquireMock ??= new Mock<Func<object>>();
+            
+            // Set up the mocks to track the state changes
+            releaseMock.Setup(m => m()).Callback(() => stateHolder.State = TestLicenseState.Released);
+            acquireMock.Setup(m => m()).Callback(() => stateHolder.State = TestLicenseState.Held);
+            
+            var tracker = new LicenseController(
+                licenseLockTime ?? TimeSpan.FromMilliseconds(100),
+                () => { releaseMock.Object(); },
+                (CancellationToken _token) => { acquireMock.Object(); },
+                _loggerMock.Object,
+                fakeTimeProvider
+            );
+            
+            return (tracker, stateHolder);
+        }
+
+        // Simple class to hold state that can be captured in lambdas
+        private class StateHolder<T>
+        {
+            public T State { get; set; }
+        }
+
+
+        [Fact]
+        public void LicenseTracker_ShouldReleaseLicense_WhenNotInUse()
+        {
+            // Arrange
+            Mock<Func<object>> _releaseLicenseFuncMock = new Mock<Func<object>>();
+            Mock<Func<object>> _acquireLicenseFuncMock = new Mock<Func<object>>();
+            var licenseLockTime = TimeSpan.FromMilliseconds(100);
+            var (tracker, license) = CreateTracker( _releaseLicenseFuncMock, _acquireLicenseFuncMock, licenseLockTime);
+
+            Assert.Equal(TestLicenseState.Released, license.State);
+
+            // Check if acquiring the license calls the acquire license function and changes the state
+            tracker.AcquireLicense(CancellationToken.None);
+            _acquireLicenseFuncMock.Verify(f => f(), Times.Once);
+            Assert.Equal(TestLicenseState.Held, license.State);
+            Assert.True(tracker.LicenseHeld);
+
+            using (tracker.BeginUsage()) { /* Use and immediately release */ } 
+            
+            fakeTimeProvider.Advance(TimeSpan.FromMilliseconds(100));
+            
+            _releaseLicenseFuncMock.Verify(f => f(), Times.Once);
+
+            Assert.Equal(TestLicenseState.Released, license.State);
+            Assert.False(tracker.LicenseHeld);
+            tracker.Dispose();
path: Cognite.Simulator.Tests/UtilsTests/LicenseControllerTest.cs
user: login: polomani
body: dispose is normally called automatically 
created_at: 2025-03-03T13:54:58Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,233 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Cognite.Simulator.Utils
+{
+
+    /// <summary>
+    /// Class to handle license acquisition and release.
+    /// With this a license can be acquired and released after a certain period of time.
+    /// </summary>
+    public class LicenseController : IDisposable
+    {
+        private readonly TimeSpan _licenseLockTime;
+        private readonly object _lock = new object();
+        private ITimer _releaseTimer;
+        private readonly Action _releaseLicenseFunc;
+        private readonly Action<CancellationToken> _acquireLicenseFunc;
+        private bool _licenseHeld;
+        private DateTimeOffset _lastUsageTime;
+        private DateTimeOffset _scheduledReleaseTime;
+        private readonly ILogger _logger;
+        private readonly TimeProvider _timeProvider;
+        private const int LoggingIntervalMs = 60000; // 1 minute
+        private readonly CancellationTokenSource _cts = new CancellationTokenSource();
+
+        /// <summary>
+        /// Class to handle license acquisition and release.
+        /// When this is used in a using block, the license is released based on the time span provided after the block ends.
+        /// When the block ends a timer is started to release the license after the time span has elapsed.
+        /// </summary>
+        /// <param name="licenseLockTime">The time span for which the license is locked.</param>
+        /// <param name="releaseLicenseFunc">The function to release the license.</param>
+        /// <param name="acquireLicenseFunc">The function to acquire the license.</param>
+        /// <param name="logger">The logger instance for logging information.</param>
+        /// <param name="timeProvider">The time provider to use for all time-related operations.</param>
+        /// <param name="cancellationToken">The cancellation token to observe while waiting for the task to complete. Optional.</param>
+        /// <exception cref="ArgumentNullException">Thrown when <paramref name="releaseLicenseFunc"/> is null.</exception>
+        public LicenseController(
+            TimeSpan licenseLockTime,
+            Action releaseLicenseFunc,
+            Action<CancellationToken> acquireLicenseFunc,
+            ILogger logger,
+            TimeProvider timeProvider = null,
+            CancellationToken cancellationToken = new CancellationToken()
+        )
+        {
+            _licenseLockTime = licenseLockTime;
+            _licenseHeld = false;
+            _releaseLicenseFunc = releaseLicenseFunc ?? throw new ArgumentNullException(nameof(releaseLicenseFunc));
+            _acquireLicenseFunc = acquireLicenseFunc ?? throw new ArgumentNullException(nameof(acquireLicenseFunc));
+            _logger = logger;
+            _timeProvider = timeProvider == null? TimeProvider.System : timeProvider;
+            _releaseTimer = _timeProvider.CreateTimer(ReleaseLicenseAfterTimeout, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
+            _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+            
+            // Dispose the timer on cancellation
+            _cts.Token.Register(() => {
+                _releaseTimer?.Dispose();
+                _releaseTimer = null;
+            }); 
+            _ = StartLoggingTask(_cts.Token);
+        }
+
+        /// <summary>
+        /// Gets a boolean indicating whether the license is currently held or not.
+        /// </summary>
+        public bool LicenseHeld => _licenseHeld;
+
+        private async Task StartLoggingTask(CancellationToken cancellationToken)
+        {
+            try
+            {
+                while (!cancellationToken.IsCancellationRequested)
+                {
+                    LogLicenseStatus();
+                    await Task.Delay(LoggingIntervalMs, cancellationToken).ConfigureAwait(false);
+                }
+            }
+            catch (OperationCanceledException)
+            {
+                // Task was cancelled, log final status
+                LogLicenseStatus();
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error in license status logging task");
+            }
+        }
+
+        private void LogLicenseStatus()
+        {
+            _logger.LogInformation("License is currently {Status}", _licenseHeld ? "held" : "not held");
+            if (_licenseHeld)
+            {
+                var duration = _timeProvider.GetUtcNow() - _lastUsageTime;
+                _logger.LogInformation("License has been held for {Duration}", CommonUtils.FormatDuration(duration));
+                var timeUntilRelease = _scheduledReleaseTime - _timeProvider.GetUtcNow();
+                _logger.LogInformation("License will be released in {TimeUntilRelease} (at {ReleaseTime})", 
+                    CommonUtils.FormatDuration(timeUntilRelease),
+                    _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+            }
+        }
+        
+        /// <summary>
+        /// Acquires a license by invoking the license acquisition function and sets the license status to held.
+        /// Also updates the release timer to ensure the license is released after a certain period.
+        /// </summary>
+        /// <exception cref="Exception">Thrown when an error occurs while acquiring the license.</exception>
+        public void AcquireLicense(CancellationToken cancellationToken) {
+            lock (_lock)
+            {
+                try
+                {
+                    if (_licenseHeld)
+                    {
+                        _logger.LogInformation("License already held, skipping acquisition");
+                        return;
+                    }
+                    _logger.LogInformation("Attempting to acquire license");
+                    _acquireLicenseFunc(cancellationToken);
+                    _licenseHeld = true;
+                    PauseTimer();
+                    _logger.LogInformation("License acquired successfully");
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, "Error acquiring license");
+                    throw;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license after the timeout has elapsed. This is called by the timer.
+        /// </summary>
+        private void ReleaseLicenseAfterTimeout(object state)
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Attempting to release license at scheduled time {ScheduledTime}", _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+                if (_timeProvider.GetUtcNow() >= _scheduledReleaseTime)
+                {
+                    _releaseLicenseFunc();
+                    _licenseHeld = false;  
+                    _logger.LogInformation("License released successfully");
+                }
+                else
+                {
+                    _logger.LogInformation("License not released - current time {CurrentTime} is before scheduled release time", 
+                        _timeProvider.GetUtcNow().ToString("yyyy-MM-dd HH:mm:ss"));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license immediately. This is used externally to reset the internal state when an external process finds out
+        /// that the license is no longer held.
+        /// </summary>
+        public void ClearLicenseState() {
+            lock (_lock)
+            {
+                _licenseHeld = false;
+                _lastUsageTime = _timeProvider.GetUtcNow();
+                _scheduledReleaseTime = _timeProvider.GetUtcNow();
+                _releaseTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
+                _logger.LogInformation("License released without callback");
+            }
+        }
+
+        private void PauseTimer() {
+            _logger.LogInformation("License usage started pausing release timer");
+            _releaseTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
+        }
+
+        /// <summary>
+        /// Begins usage of the license.
+        /// </summary>
+        /// <returns>An <see cref="IDisposable"/> object that can be used to end the license usage.</returns>
+        public IDisposable BeginUsage()
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Starting license usage");
+                PauseTimer();
+                return new LicenseUsageScope(this);
+            }
+        }
+
+        private void EndUsage()
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Ending license usage, scheduling release");
+                _lastUsageTime = _timeProvider.GetUtcNow();
+                _scheduledReleaseTime = _lastUsageTime.Add(_licenseLockTime);
+                _logger.LogInformation("License release scheduled for {ReleaseTime}", 
+                _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
path: Cognite.Simulator.Utils/LicenseController.cs
user: login: abdullah-cognite
body: it should work: https://learn.microsoft.com/en-us/dotnet/api/system.datetimeoffset.tostring?view=net-8.0
created_at: 2025-03-03T14:10:00Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,233 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Cognite.Simulator.Utils
+{
+
+    /// <summary>
+    /// Class to handle license acquisition and release.
+    /// With this a license can be acquired and released after a certain period of time.
+    /// </summary>
+    public class LicenseController : IDisposable
+    {
+        private readonly TimeSpan _licenseLockTime;
+        private readonly object _lock = new object();
+        private ITimer _releaseTimer;
+        private readonly Action _releaseLicenseFunc;
+        private readonly Action<CancellationToken> _acquireLicenseFunc;
+        private bool _licenseHeld;
+        private DateTimeOffset _lastUsageTime;
+        private DateTimeOffset _scheduledReleaseTime;
+        private readonly ILogger _logger;
+        private readonly TimeProvider _timeProvider;
+        private const int LoggingIntervalMs = 60000; // 1 minute
+        private readonly CancellationTokenSource _cts = new CancellationTokenSource();
+
+        /// <summary>
+        /// Class to handle license acquisition and release.
+        /// When this is used in a using block, the license is released based on the time span provided after the block ends.
+        /// When the block ends a timer is started to release the license after the time span has elapsed.
+        /// </summary>
+        /// <param name="licenseLockTime">The time span for which the license is locked.</param>
+        /// <param name="releaseLicenseFunc">The function to release the license.</param>
+        /// <param name="acquireLicenseFunc">The function to acquire the license.</param>
+        /// <param name="logger">The logger instance for logging information.</param>
+        /// <param name="timeProvider">The time provider to use for all time-related operations.</param>
+        /// <param name="cancellationToken">The cancellation token to observe while waiting for the task to complete. Optional.</param>
+        /// <exception cref="ArgumentNullException">Thrown when <paramref name="releaseLicenseFunc"/> is null.</exception>
+        public LicenseController(
+            TimeSpan licenseLockTime,
+            Action releaseLicenseFunc,
+            Action<CancellationToken> acquireLicenseFunc,
+            ILogger logger,
+            TimeProvider timeProvider = null,
+            CancellationToken cancellationToken = new CancellationToken()
+        )
+        {
+            _licenseLockTime = licenseLockTime;
+            _licenseHeld = false;
+            _releaseLicenseFunc = releaseLicenseFunc ?? throw new ArgumentNullException(nameof(releaseLicenseFunc));
+            _acquireLicenseFunc = acquireLicenseFunc ?? throw new ArgumentNullException(nameof(acquireLicenseFunc));
+            _logger = logger;
+            _timeProvider = timeProvider == null? TimeProvider.System : timeProvider;
+            _releaseTimer = _timeProvider.CreateTimer(ReleaseLicenseAfterTimeout, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
+            _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+            
+            // Dispose the timer on cancellation
+            _cts.Token.Register(() => {
+                _releaseTimer?.Dispose();
+                _releaseTimer = null;
+            }); 
+            _ = StartLoggingTask(_cts.Token);
+        }
+
+        /// <summary>
+        /// Gets a boolean indicating whether the license is currently held or not.
+        /// </summary>
+        public bool LicenseHeld => _licenseHeld;
+
+        private async Task StartLoggingTask(CancellationToken cancellationToken)
+        {
+            try
+            {
+                while (!cancellationToken.IsCancellationRequested)
+                {
+                    LogLicenseStatus();
+                    await Task.Delay(LoggingIntervalMs, cancellationToken).ConfigureAwait(false);
+                }
+            }
+            catch (OperationCanceledException)
+            {
+                // Task was cancelled, log final status
+                LogLicenseStatus();
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error in license status logging task");
+            }
+        }
+
+        private void LogLicenseStatus()
+        {
+            _logger.LogInformation("License is currently {Status}", _licenseHeld ? "held" : "not held");
+            if (_licenseHeld)
+            {
+                var duration = _timeProvider.GetUtcNow() - _lastUsageTime;
+                _logger.LogInformation("License has been held for {Duration}", CommonUtils.FormatDuration(duration));
+                var timeUntilRelease = _scheduledReleaseTime - _timeProvider.GetUtcNow();
+                _logger.LogInformation("License will be released in {TimeUntilRelease} (at {ReleaseTime})", 
+                    CommonUtils.FormatDuration(timeUntilRelease),
+                    _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+            }
+        }
+        
+        /// <summary>
+        /// Acquires a license by invoking the license acquisition function and sets the license status to held.
+        /// Also updates the release timer to ensure the license is released after a certain period.
+        /// </summary>
+        /// <exception cref="Exception">Thrown when an error occurs while acquiring the license.</exception>
+        public void AcquireLicense(CancellationToken cancellationToken) {
+            lock (_lock)
+            {
+                try
+                {
+                    if (_licenseHeld)
+                    {
+                        _logger.LogInformation("License already held, skipping acquisition");
+                        return;
+                    }
+                    _logger.LogInformation("Attempting to acquire license");
+                    _acquireLicenseFunc(cancellationToken);
+                    _licenseHeld = true;
+                    PauseTimer();
+                    _logger.LogInformation("License acquired successfully");
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, "Error acquiring license");
+                    throw;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license after the timeout has elapsed. This is called by the timer.
+        /// </summary>
+        private void ReleaseLicenseAfterTimeout(object state)
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Attempting to release license at scheduled time {ScheduledTime}", _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+                if (_timeProvider.GetUtcNow() >= _scheduledReleaseTime)
+                {
+                    _releaseLicenseFunc();
+                    _licenseHeld = false;  
+                    _logger.LogInformation("License released successfully");
+                }
+                else
+                {
+                    _logger.LogInformation("License not released - current time {CurrentTime} is before scheduled release time", 
+                        _timeProvider.GetUtcNow().ToString("yyyy-MM-dd HH:mm:ss"));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license immediately. This is used externally to reset the internal state when an external process finds out
+        /// that the license is no longer held.
+        /// </summary>
+        public void ClearLicenseState() {
+            lock (_lock)
+            {
+                _licenseHeld = false;
+                _lastUsageTime = _timeProvider.GetUtcNow();
+                _scheduledReleaseTime = _timeProvider.GetUtcNow();
+                _releaseTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
+                _logger.LogInformation("License released without callback");
+            }
+        }
+
+        private void PauseTimer() {
+            _logger.LogInformation("License usage started pausing release timer");
+            _releaseTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
+        }
+
+        /// <summary>
+        /// Begins usage of the license.
+        /// </summary>
+        /// <returns>An <see cref="IDisposable"/> object that can be used to end the license usage.</returns>
+        public IDisposable BeginUsage()
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Starting license usage");
+                PauseTimer();
+                return new LicenseUsageScope(this);
+            }
+        }
+
+        private void EndUsage()
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Ending license usage, scheduling release");
+                _lastUsageTime = _timeProvider.GetUtcNow();
+                _scheduledReleaseTime = _lastUsageTime.Add(_licenseLockTime);
+                _logger.LogInformation("License release scheduled for {ReleaseTime}", 
+                _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
path: Cognite.Simulator.Utils/LicenseController.cs
user: login: polomani
body: ah, I misread it, sorry
created_at: 2025-03-03T14:13:04Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,233 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Cognite.Simulator.Utils
+{
+
+    /// <summary>
+    /// Class to handle license acquisition and release.
+    /// With this a license can be acquired and released after a certain period of time.
+    /// </summary>
+    public class LicenseController : IDisposable
+    {
+        private readonly TimeSpan _licenseLockTime;
+        private readonly object _lock = new object();
+        private ITimer _releaseTimer;
+        private readonly Action _releaseLicenseFunc;
+        private readonly Action<CancellationToken> _acquireLicenseFunc;
+        private bool _licenseHeld;
+        private DateTimeOffset _lastUsageTime;
+        private DateTimeOffset _scheduledReleaseTime;
+        private readonly ILogger _logger;
+        private readonly TimeProvider _timeProvider;
+        private const int LoggingIntervalMs = 60000; // 1 minute
+        private readonly CancellationTokenSource _cts = new CancellationTokenSource();
+
+        /// <summary>
+        /// Class to handle license acquisition and release.
+        /// When this is used in a using block, the license is released based on the time span provided after the block ends.
+        /// When the block ends a timer is started to release the license after the time span has elapsed.
+        /// </summary>
+        /// <param name="licenseLockTime">The time span for which the license is locked.</param>
+        /// <param name="releaseLicenseFunc">The function to release the license.</param>
+        /// <param name="acquireLicenseFunc">The function to acquire the license.</param>
+        /// <param name="logger">The logger instance for logging information.</param>
+        /// <param name="timeProvider">The time provider to use for all time-related operations.</param>
+        /// <param name="cancellationToken">The cancellation token to observe while waiting for the task to complete. Optional.</param>
+        /// <exception cref="ArgumentNullException">Thrown when <paramref name="releaseLicenseFunc"/> is null.</exception>
+        public LicenseController(
+            TimeSpan licenseLockTime,
+            Action releaseLicenseFunc,
+            Action<CancellationToken> acquireLicenseFunc,
+            ILogger logger,
+            TimeProvider timeProvider = null,
+            CancellationToken cancellationToken = new CancellationToken()
+        )
+        {
+            _licenseLockTime = licenseLockTime;
+            _licenseHeld = false;
+            _releaseLicenseFunc = releaseLicenseFunc ?? throw new ArgumentNullException(nameof(releaseLicenseFunc));
+            _acquireLicenseFunc = acquireLicenseFunc ?? throw new ArgumentNullException(nameof(acquireLicenseFunc));
+            _logger = logger;
+            _timeProvider = timeProvider == null? TimeProvider.System : timeProvider;
+            _releaseTimer = _timeProvider.CreateTimer(ReleaseLicenseAfterTimeout, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
+            _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+            
+            // Dispose the timer on cancellation
+            _cts.Token.Register(() => {
+                _releaseTimer?.Dispose();
+                _releaseTimer = null;
+            }); 
+            _ = StartLoggingTask(_cts.Token);
+        }
+
+        /// <summary>
+        /// Gets a boolean indicating whether the license is currently held or not.
+        /// </summary>
+        public bool LicenseHeld => _licenseHeld;
+
+        private async Task StartLoggingTask(CancellationToken cancellationToken)
+        {
+            try
+            {
+                while (!cancellationToken.IsCancellationRequested)
+                {
+                    LogLicenseStatus();
+                    await Task.Delay(LoggingIntervalMs, cancellationToken).ConfigureAwait(false);
+                }
+            }
+            catch (OperationCanceledException)
+            {
+                // Task was cancelled, log final status
+                LogLicenseStatus();
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error in license status logging task");
+            }
+        }
+
+        private void LogLicenseStatus()
+        {
+            _logger.LogInformation("License is currently {Status}", _licenseHeld ? "held" : "not held");
+            if (_licenseHeld)
+            {
+                var duration = _timeProvider.GetUtcNow() - _lastUsageTime;
+                _logger.LogInformation("License has been held for {Duration}", CommonUtils.FormatDuration(duration));
+                var timeUntilRelease = _scheduledReleaseTime - _timeProvider.GetUtcNow();
+                _logger.LogInformation("License will be released in {TimeUntilRelease} (at {ReleaseTime})", 
+                    CommonUtils.FormatDuration(timeUntilRelease),
+                    _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+            }
+        }
+        
+        /// <summary>
+        /// Acquires a license by invoking the license acquisition function and sets the license status to held.
+        /// Also updates the release timer to ensure the license is released after a certain period.
+        /// </summary>
+        /// <exception cref="Exception">Thrown when an error occurs while acquiring the license.</exception>
+        public void AcquireLicense(CancellationToken cancellationToken) {
+            lock (_lock)
+            {
+                try
+                {
+                    if (_licenseHeld)
+                    {
+                        _logger.LogInformation("License already held, skipping acquisition");
+                        return;
+                    }
+                    _logger.LogInformation("Attempting to acquire license");
+                    _acquireLicenseFunc(cancellationToken);
+                    _licenseHeld = true;
+                    PauseTimer();
+                    _logger.LogInformation("License acquired successfully");
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, "Error acquiring license");
+                    throw;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license after the timeout has elapsed. This is called by the timer.
+        /// </summary>
+        private void ReleaseLicenseAfterTimeout(object state)
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Attempting to release license at scheduled time {ScheduledTime}", _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+                if (_timeProvider.GetUtcNow() >= _scheduledReleaseTime)
+                {
+                    _releaseLicenseFunc();
+                    _licenseHeld = false;  
+                    _logger.LogInformation("License released successfully");
+                }
+                else
+                {
+                    _logger.LogInformation("License not released - current time {CurrentTime} is before scheduled release time", 
+                        _timeProvider.GetUtcNow().ToString("yyyy-MM-dd HH:mm:ss"));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license immediately. This is used externally to reset the internal state when an external process finds out
+        /// that the license is no longer held.
+        /// </summary>
+        public void ClearLicenseState() {
+            lock (_lock)
+            {
+                _licenseHeld = false;
+                _lastUsageTime = _timeProvider.GetUtcNow();
+                _scheduledReleaseTime = _timeProvider.GetUtcNow();
+                _releaseTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
+                _logger.LogInformation("License released without callback");
+            }
+        }
+
+        private void PauseTimer() {
+            _logger.LogInformation("License usage started pausing release timer");
+            _releaseTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
+        }
+
+        /// <summary>
+        /// Begins usage of the license.
+        /// </summary>
+        /// <returns>An <see cref="IDisposable"/> object that can be used to end the license usage.</returns>
+        public IDisposable BeginUsage()
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Starting license usage");
+                PauseTimer();
+                return new LicenseUsageScope(this);
+            }
+        }
+
+        private void EndUsage()
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Ending license usage, scheduling release");
+                _lastUsageTime = _timeProvider.GetUtcNow();
+                _scheduledReleaseTime = _lastUsageTime.Add(_licenseLockTime);
+                _logger.LogInformation("License release scheduled for {ReleaseTime}", 
+                _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
path: Cognite.Simulator.Utils/LicenseController.cs
user: login: polomani
body: due to the formatting it looks like a separate code line! can you even read it?
created_at: 2025-03-03T14:13:43Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,233 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Cognite.Simulator.Utils
+{
+
+    /// <summary>
+    /// Class to handle license acquisition and release.
+    /// With this a license can be acquired and released after a certain period of time.
+    /// </summary>
+    public class LicenseController : IDisposable
+    {
+        private readonly TimeSpan _licenseLockTime;
+        private readonly object _lock = new object();
+        private ITimer _releaseTimer;
+        private readonly Action _releaseLicenseFunc;
+        private readonly Action<CancellationToken> _acquireLicenseFunc;
+        private bool _licenseHeld;
+        private DateTimeOffset _lastUsageTime;
+        private DateTimeOffset _scheduledReleaseTime;
+        private readonly ILogger _logger;
+        private readonly TimeProvider _timeProvider;
+        private const int LoggingIntervalMs = 60000; // 1 minute
+        private readonly CancellationTokenSource _cts = new CancellationTokenSource();
+
+        /// <summary>
+        /// Class to handle license acquisition and release.
+        /// When this is used in a using block, the license is released based on the time span provided after the block ends.
+        /// When the block ends a timer is started to release the license after the time span has elapsed.
+        /// </summary>
+        /// <param name="licenseLockTime">The time span for which the license is locked.</param>
+        /// <param name="releaseLicenseFunc">The function to release the license.</param>
+        /// <param name="acquireLicenseFunc">The function to acquire the license.</param>
+        /// <param name="logger">The logger instance for logging information.</param>
+        /// <param name="timeProvider">The time provider to use for all time-related operations.</param>
+        /// <param name="cancellationToken">The cancellation token to observe while waiting for the task to complete. Optional.</param>
+        /// <exception cref="ArgumentNullException">Thrown when <paramref name="releaseLicenseFunc"/> is null.</exception>
+        public LicenseController(
+            TimeSpan licenseLockTime,
+            Action releaseLicenseFunc,
+            Action<CancellationToken> acquireLicenseFunc,
+            ILogger logger,
+            TimeProvider timeProvider = null,
+            CancellationToken cancellationToken = new CancellationToken()
+        )
+        {
+            _licenseLockTime = licenseLockTime;
+            _licenseHeld = false;
+            _releaseLicenseFunc = releaseLicenseFunc ?? throw new ArgumentNullException(nameof(releaseLicenseFunc));
+            _acquireLicenseFunc = acquireLicenseFunc ?? throw new ArgumentNullException(nameof(acquireLicenseFunc));
+            _logger = logger;
+            _timeProvider = timeProvider == null? TimeProvider.System : timeProvider;
+            _releaseTimer = _timeProvider.CreateTimer(ReleaseLicenseAfterTimeout, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
+            _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+            
+            // Dispose the timer on cancellation
+            _cts.Token.Register(() => {
+                _releaseTimer?.Dispose();
+                _releaseTimer = null;
+            }); 
+            _ = StartLoggingTask(_cts.Token);
+        }
+
+        /// <summary>
+        /// Gets a boolean indicating whether the license is currently held or not.
+        /// </summary>
+        public bool LicenseHeld => _licenseHeld;
+
+        private async Task StartLoggingTask(CancellationToken cancellationToken)
+        {
+            try
+            {
+                while (!cancellationToken.IsCancellationRequested)
+                {
+                    LogLicenseStatus();
+                    await Task.Delay(LoggingIntervalMs, cancellationToken).ConfigureAwait(false);
+                }
+            }
+            catch (OperationCanceledException)
+            {
+                // Task was cancelled, log final status
+                LogLicenseStatus();
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error in license status logging task");
+            }
+        }
+
+        private void LogLicenseStatus()
+        {
+            _logger.LogInformation("License is currently {Status}", _licenseHeld ? "held" : "not held");
+            if (_licenseHeld)
+            {
+                var duration = _timeProvider.GetUtcNow() - _lastUsageTime;
+                _logger.LogInformation("License has been held for {Duration}", CommonUtils.FormatDuration(duration));
+                var timeUntilRelease = _scheduledReleaseTime - _timeProvider.GetUtcNow();
+                _logger.LogInformation("License will be released in {TimeUntilRelease} (at {ReleaseTime})", 
+                    CommonUtils.FormatDuration(timeUntilRelease),
+                    _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+            }
+        }
+        
+        /// <summary>
+        /// Acquires a license by invoking the license acquisition function and sets the license status to held.
+        /// Also updates the release timer to ensure the license is released after a certain period.
+        /// </summary>
+        /// <exception cref="Exception">Thrown when an error occurs while acquiring the license.</exception>
+        public void AcquireLicense(CancellationToken cancellationToken) {
+            lock (_lock)
+            {
+                try
+                {
+                    if (_licenseHeld)
+                    {
+                        _logger.LogInformation("License already held, skipping acquisition");
+                        return;
+                    }
+                    _logger.LogInformation("Attempting to acquire license");
+                    _acquireLicenseFunc(cancellationToken);
+                    _licenseHeld = true;
+                    PauseTimer();
+                    _logger.LogInformation("License acquired successfully");
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, "Error acquiring license");
+                    throw;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license after the timeout has elapsed. This is called by the timer.
+        /// </summary>
+        private void ReleaseLicenseAfterTimeout(object state)
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Attempting to release license at scheduled time {ScheduledTime}", _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
+                if (_timeProvider.GetUtcNow() >= _scheduledReleaseTime)
+                {
+                    _releaseLicenseFunc();
+                    _licenseHeld = false;  
+                    _logger.LogInformation("License released successfully");
+                }
+                else
+                {
+                    _logger.LogInformation("License not released - current time {CurrentTime} is before scheduled release time", 
+                        _timeProvider.GetUtcNow().ToString("yyyy-MM-dd HH:mm:ss"));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Releases the license immediately. This is used externally to reset the internal state when an external process finds out
+        /// that the license is no longer held.
+        /// </summary>
+        public void ClearLicenseState() {
+            lock (_lock)
+            {
+                _licenseHeld = false;
+                _lastUsageTime = _timeProvider.GetUtcNow();
+                _scheduledReleaseTime = _timeProvider.GetUtcNow();
+                _releaseTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
+                _logger.LogInformation("License released without callback");
+            }
+        }
+
+        private void PauseTimer() {
+            _logger.LogInformation("License usage started pausing release timer");
+            _releaseTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
+        }
+
+        /// <summary>
+        /// Begins usage of the license.
+        /// </summary>
+        /// <returns>An <see cref="IDisposable"/> object that can be used to end the license usage.</returns>
+        public IDisposable BeginUsage()
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Starting license usage");
+                PauseTimer();
+                return new LicenseUsageScope(this);
+            }
+        }
+
+        private void EndUsage()
+        {
+            lock (_lock)
+            {
+                _logger.LogInformation("Ending license usage, scheduling release");
+                _lastUsageTime = _timeProvider.GetUtcNow();
+                _scheduledReleaseTime = _lastUsageTime.Add(_licenseLockTime);
+                _logger.LogInformation("License release scheduled for {ReleaseTime}", 
+                _scheduledReleaseTime.ToString("yyyy-MM-dd HH:mm:ss"));
path: Cognite.Simulator.Utils/LicenseController.cs
user: login: abdullah-cognite
body: Is this formatting better? https://github.com/cognitedata/dotnet-simulator-utils/pull/239/commits/cba6a490d42c9183cf5bf86184ddfc1c3a6bc9d0
created_at: 2025-03-03T14:46:55Z
author_association: COLLABORATOR
diff_hunk: @@ -130,6 +130,7 @@ public void AcquireLicense(CancellationToken cancellationToken) {
                     _logger.LogInformation("Attempting to acquire license");
                     _acquireLicenseFunc(cancellationToken);
                     _licenseHeld = true;
+                    _scheduledReleaseTime = _timeProvider.GetUtcNow().Add(_licenseLockTime);
path: Cognite.Simulator.Utils/LicenseController.cs
user: login: polomani
body: I think I was saying this before, but the correct place for this should be in `EndUsage` and ot here, what's the purpose of it here?
created_at: 2025-03-04T12:32:53Z
author_association: COLLABORATOR
diff_hunk: @@ -130,6 +130,7 @@ public void AcquireLicense(CancellationToken cancellationToken) {
                     _logger.LogInformation("Attempting to acquire license");
                     _acquireLicenseFunc(cancellationToken);
                     _licenseHeld = true;
+                    _scheduledReleaseTime = _timeProvider.GetUtcNow().Add(_licenseLockTime);
path: Cognite.Simulator.Utils/LicenseController.cs
user: login: abdullah-cognite
body: EndUsage already has it and ignore this line, Ive removed it. It was just a local experiment accidentally committed. 
created_at: 2025-03-05T09:38:21Z
author_association: COLLABORATOR
diff_hunk: @@ -75,8 +75,8 @@ public override void GenerateDefaults()
                 Cognite = new CogniteConfig();
             }
 
-            if (Cognite.CdfRetries == null) {
-                Cognite.CdfRetries = new RetryConfig();
+            // the default max retries of 5 does not work well with simint
+            if (Cognite.CdfRetries.MaxRetries == 5) {
path: Cognite.Simulator.Utils/Configuration.cs
user: login: polomani
body: any better way of doing this? looks very sketchy
we could have our own version of BaseConfig to override this on a lower level

created_at: 2025-03-06T10:17:42Z
author_association: COLLABORATOR
diff_hunk: @@ -75,8 +75,8 @@ public override void GenerateDefaults()
                 Cognite = new CogniteConfig();
             }
 
-            if (Cognite.CdfRetries == null) {
-                Cognite.CdfRetries = new RetryConfig();
+            // the default max retries of 5 does not work well with simint
+            if (Cognite.CdfRetries.MaxRetries == 5) {
path: Cognite.Simulator.Utils/Configuration.cs
user: login: abdullah-cognite
body: yeah, i dont like this approach either, let me remove this whole part and do it in a separate pr
created_at: 2025-03-06T10:20:05Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,97 @@
+using System.Linq;
+using System;
+using Microsoft.Extensions.Logging;
+using System.Diagnostics;
+using System.Collections.Generic;
+using System.Runtime.Versioning;
+using Cognite.Simulator.Utils.Automation;
+using Cognite.Simulator.Utils;
+using System.Threading;
+using System.Globalization;
+using System.Management;
+using System.Security.Principal;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+
+namespace Cognite.Simulator.Utils
+{
+     public static class ProcessUtils {
+
+        /// <summary>
+        /// Get the owner of a process using WMI
+        /// </summary>
+        /// <param name="processId">The process ID</param>
+        /// <returns>The owner of the process</returns>
+        /// <exception cref="PlatformNotSupportedException">Thrown when the method is called on a non-Windows platform</exception>
+        /// <exception cref="Exception">Thrown when the process is not found or access is denied</exception>
+        public static string GetProcessOwnerWmi(int processId)
+        {
+            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
+                throw new PlatformNotSupportedException("This method is only supported on Windows");
+            }
+            string query = $"SELECT * FROM Win32_Process WHERE ProcessId = {processId}";
+            using (var searcher = new ManagementObjectSearcher(query))
+            using (var results = searcher.Get())
+            {
+                foreach (ManagementObject process in results)
+                {
+                    try
+                    {
+                        object[] args = new object[] { "", "" };
+                        if (Convert.ToInt32(process.InvokeMethod("GetOwner", args)) == 0)
+                        {
+                            string user = (string)args[0];
+                            string domain = (string)args[1];
+                            return $"{domain}\\{user}";
+                        }
+                    }
+                    catch
+                    {
+                        throw new Exception("Access Denied or Process Exited");
+                    }
+                }
+            }
+            throw new Exception("Process not found");
+        }
+
+        /// <summary>
+        /// Kill a process by ID. Can throw exceptions if it is unable to kill the process or if it cannot find the process owner.
+        /// </summary>
+        /// <param name="processId"></param>
+        /// <param name="_logger"></param>
+        public static void KillProcess(string processId, ILogger _logger) {
+            try {
+                Process[] processes = Process.GetProcessesByName(processId);
+                _logger.LogDebug("Searching for process : " + processId);
+                _logger.LogDebug("Found {Count} matching processes", processes.Length);
+                bool found = false;
+                foreach (Process process in processes) {
+                    string owner = GetProcessOwnerWmi(process.Id);
+                    _logger.LogDebug($"Found process . Process owner is : {owner.ToLower()} . Current user is : {GetCurrentUsername().ToLower()}");
+                    if (owner.ToLower() == GetCurrentUsername().ToLower()) {
+                        _logger.LogInformation("Killing process with PID {PID}", process.Id);
+                        process.Kill();
+                        process.WaitForExit();
+                        found = true;
+                    } else {
+                        _logger.LogWarning("Process with PID {PID} is owned by a different user ({Owner}). Skipping.", process.Id, owner);
+                    }
+                }
+                if (!found) {
+                    throw new Exception("No processes found to kill for the current user");
path: Cognite.Simulator.Utils/ProcessUtils.cs
user: login: abdullah-cognite
body: Refactor this into a separate unsafe method
created_at: 2025-03-06T12:32:08Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,97 @@
+using System.Linq;
+using System;
+using Microsoft.Extensions.Logging;
+using System.Diagnostics;
+using System.Collections.Generic;
+using System.Runtime.Versioning;
+using Cognite.Simulator.Utils.Automation;
+using Cognite.Simulator.Utils;
+using System.Threading;
+using System.Globalization;
+using System.Management;
+using System.Security.Principal;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+
+namespace Cognite.Simulator.Utils
+{
+     public static class ProcessUtils {
+
+        /// <summary>
+        /// Get the owner of a process using WMI
+        /// </summary>
+        /// <param name="processId">The process ID</param>
+        /// <returns>The owner of the process</returns>
+        /// <exception cref="PlatformNotSupportedException">Thrown when the method is called on a non-Windows platform</exception>
+        /// <exception cref="Exception">Thrown when the process is not found or access is denied</exception>
+        public static string GetProcessOwnerWmi(int processId)
+        {
+            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
+                throw new PlatformNotSupportedException("This method is only supported on Windows");
+            }
+            string query = $"SELECT * FROM Win32_Process WHERE ProcessId = {processId}";
+            using (var searcher = new ManagementObjectSearcher(query))
+            using (var results = searcher.Get())
+            {
+                foreach (ManagementObject process in results)
+                {
+                    try
path: Cognite.Simulator.Utils/ProcessUtils.cs
user: login: abdullah-cognite
body: can be outside the for loop as well
created_at: 2025-03-06T12:34:05Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,97 @@
+using System.Linq;
+using System;
+using Microsoft.Extensions.Logging;
+using System.Diagnostics;
+using System.Collections.Generic;
+using System.Runtime.Versioning;
+using Cognite.Simulator.Utils.Automation;
+using Cognite.Simulator.Utils;
+using System.Threading;
+using System.Globalization;
+using System.Management;
+using System.Security.Principal;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+
+namespace Cognite.Simulator.Utils
+{
+     public static class ProcessUtils {
+
+        /// <summary>
+        /// Get the owner of a process using WMI
+        /// </summary>
+        /// <param name="processId">The process ID</param>
+        /// <returns>The owner of the process</returns>
+        /// <exception cref="PlatformNotSupportedException">Thrown when the method is called on a non-Windows platform</exception>
+        /// <exception cref="Exception">Thrown when the process is not found or access is denied</exception>
+        public static string GetProcessOwnerWmi(int processId)
+        {
+            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
+                throw new PlatformNotSupportedException("This method is only supported on Windows");
+            }
+            string query = $"SELECT * FROM Win32_Process WHERE ProcessId = {processId}";
+            using (var searcher = new ManagementObjectSearcher(query))
+            using (var results = searcher.Get())
+            {
+                foreach (ManagementObject process in results)
+                {
+                    try
+                    {
+                        object[] args = new object[] { "", "" };
+                        if (Convert.ToInt32(process.InvokeMethod("GetOwner", args)) == 0)
+                        {
+                            string user = (string)args[0];
+                            string domain = (string)args[1];
+                            return $"{domain}\\{user}";
+                        }
+                    }
+                    catch
+                    {
+                        throw new Exception("Access Denied or Process Exited");
+                    }
+                }
+            }
+            throw new Exception("Process not found");
+        }
+
+        /// <summary>
+        /// Kill a process by ID. Can throw exceptions if it is unable to kill the process or if it cannot find the process owner.
+        /// </summary>
+        /// <param name="processId"></param>
+        /// <param name="_logger"></param>
+        public static void KillProcess(string processId, ILogger _logger) {
+            try {
+                Process[] processes = Process.GetProcessesByName(processId);
+                _logger.LogDebug("Searching for process : " + processId);
+                _logger.LogDebug("Found {Count} matching processes", processes.Length);
+                bool found = false;
+                foreach (Process process in processes) {
+                    string owner = GetProcessOwnerWmi(process.Id);
+                    _logger.LogDebug($"Found process . Process owner is : {owner.ToLower()} . Current user is : {GetCurrentUsername().ToLower()}");
+                    if (owner.ToLower() == GetCurrentUsername().ToLower()) {
+                        _logger.LogInformation("Killing process with PID {PID}", process.Id);
+                        process.Kill();
+                        process.WaitForExit();
+                        found = true;
+                    } else {
+                        _logger.LogWarning("Process with PID {PID} is owned by a different user ({Owner}). Skipping.", process.Id, owner);
+                    }
+                }
+                if (!found) {
+                    throw new Exception("No processes found to kill for the current user");
path: Cognite.Simulator.Utils/ProcessUtils.cs
user: login: abdullah-cognite
body: can be done in a separate pr as well
created_at: 2025-03-07T09:43:11Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,97 @@
+using System.Linq;
+using System;
+using Microsoft.Extensions.Logging;
+using System.Diagnostics;
+using System.Collections.Generic;
+using System.Runtime.Versioning;
+using Cognite.Simulator.Utils.Automation;
+using Cognite.Simulator.Utils;
+using System.Threading;
+using System.Globalization;
+using System.Management;
+using System.Security.Principal;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+
+namespace Cognite.Simulator.Utils
+{
+     public static class ProcessUtils {
+
+        /// <summary>
+        /// Get the owner of a process using WMI
+        /// </summary>
+        /// <param name="processId">The process ID</param>
+        /// <returns>The owner of the process</returns>
+        /// <exception cref="PlatformNotSupportedException">Thrown when the method is called on a non-Windows platform</exception>
+        /// <exception cref="Exception">Thrown when the process is not found or access is denied</exception>
+        public static string GetProcessOwnerWmi(int processId)
+        {
+            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
+                throw new PlatformNotSupportedException("This method is only supported on Windows");
+            }
+            string query = $"SELECT * FROM Win32_Process WHERE ProcessId = {processId}";
+            using (var searcher = new ManagementObjectSearcher(query))
+            using (var results = searcher.Get())
+            {
+                foreach (ManagementObject process in results)
+                {
+                    try
path: Cognite.Simulator.Utils/ProcessUtils.cs
user: login: abdullah-cognite
body: can be done in a separate pr as well
created_at: 2025-03-07T09:43:14Z
author_association: COLLABORATOR
diff_hunk: @@ -74,11 +75,6 @@ public override void GenerateDefaults()
                 Cognite = new CogniteConfig();
             }
 
-            if (Cognite.CdfRetries == null) {
-                Cognite.CdfRetries.MaxRetries = 11;
-                Cognite.CdfRetries.MaxDelay = 60 * 1000; //ms
-            }
-
path: Cognite.Simulator.Utils/Configuration.cs
user: login: dbrattli
body: I'm unsure why these lines are removed?
created_at: 2025-03-07T14:04:13Z
author_association: NONE
diff_hunk: @@ -74,11 +75,6 @@ public override void GenerateDefaults()
                 Cognite = new CogniteConfig();
             }
 
-            if (Cognite.CdfRetries == null) {
-                Cognite.CdfRetries.MaxRetries = 11;
-                Cognite.CdfRetries.MaxDelay = 60 * 1000; //ms
-            }
-
path: Cognite.Simulator.Utils/Configuration.cs
user: login: abdullah-cognite
body: We found out these lines were never hit, the Cdfretries values would always default to the ones set here: https://github.com/cognitedata/dotnet-extractor-utils/blob/8b44bc4291d49a28ec6dd3a86f540a0650a6a420/ExtractorUtils/Configuration/BaseConfig.cs#L297

So, we have removed these lines for now.
created_at: 2025-03-07T14:08:08Z
author_association: COLLABORATOR
diff_hunk: @@ -23,8 +23,10 @@
 
   <ItemGroup>
     <PackageReference Include="Cognite.ExtractorUtils" Version="1.29.0" />
+    <PackageReference Include="Microsoft.Bcl.TimeProvider" Version="10.0.0-preview.1.25080.5" />
path: Cognite.Simulator.Utils/Cognite.Simulator.Utils.csproj
user: login: dbrattli
body: Why do we use the preview version?
created_at: 2025-03-07T15:48:37Z
author_association: NONE
diff_hunk: @@ -175,7 +175,8 @@ public string GetSimulatorVersion()
             public Task<Dictionary<string, SimulatorValueItem>> RunSimulation(
                 SampleModelFilestate modelState,
                 SimulatorRoutineRevision routineRevision,
-                Dictionary<string, SimulatorValueItem> inputData
+                Dictionary<string, SimulatorValueItem> inputData,
+                CancellationToken _token
path: Cognite.Simulator.Tests/IntegrationTests/ConnectorRuntimeTest.cs
user: login: dbrattli
body: Why do we underscore the parameter name?
created_at: 2025-03-07T15:51:49Z
author_association: NONE
diff_hunk: @@ -175,7 +175,8 @@ public string GetSimulatorVersion()
             public Task<Dictionary<string, SimulatorValueItem>> RunSimulation(
                 SampleModelFilestate modelState,
                 SimulatorRoutineRevision routineRevision,
-                Dictionary<string, SimulatorValueItem> inputData
+                Dictionary<string, SimulatorValueItem> inputData,
+                CancellationToken _token
path: Cognite.Simulator.Tests/IntegrationTests/ConnectorRuntimeTest.cs
user: login: abdullah-cognite
body: We usually do it for unused variables, but since our latest changes from master this variable is now in use, so Ive updated it. 
created_at: 2025-03-10T10:05:46Z
author_association: COLLABORATOR
diff_hunk: @@ -23,8 +23,10 @@
 
   <ItemGroup>
     <PackageReference Include="Cognite.ExtractorUtils" Version="1.29.0" />
+    <PackageReference Include="Microsoft.Bcl.TimeProvider" Version="10.0.0-preview.1.25080.5" />
path: Cognite.Simulator.Utils/Cognite.Simulator.Utils.csproj
user: login: abdullah-cognite
body: Good catch, modified this to the last non-preview version.
created_at: 2025-03-10T10:06:16Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,97 @@
+using System.Linq;
+using System;
+using Microsoft.Extensions.Logging;
+using System.Diagnostics;
+using System.Collections.Generic;
+using System.Runtime.Versioning;
+using Cognite.Simulator.Utils.Automation;
+using Cognite.Simulator.Utils;
+using System.Threading;
+using System.Globalization;
+using System.Management;
+using System.Security.Principal;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+
+namespace Cognite.Simulator.Utils
+{
+     public static class ProcessUtils {
+
+        /// <summary>
+        /// Get the owner of a process using WMI
+        /// </summary>
+        /// <param name="processId">The process ID</param>
+        /// <returns>The owner of the process</returns>
+        /// <exception cref="PlatformNotSupportedException">Thrown when the method is called on a non-Windows platform</exception>
+        /// <exception cref="Exception">Thrown when the process is not found or access is denied</exception>
+        public static string GetProcessOwnerWmi(int processId)
+        {
+            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
+                throw new PlatformNotSupportedException("This method is only supported on Windows");
+            }
+            string query = $"SELECT * FROM Win32_Process WHERE ProcessId = {processId}";
+            using (var searcher = new ManagementObjectSearcher(query))
+            using (var results = searcher.Get())
+            {
+                foreach (ManagementObject process in results)
+                {
+                    try
path: Cognite.Simulator.Utils/ProcessUtils.cs
user: login: abdullah-cognite
body: addressed
created_at: 2025-03-10T10:08:26Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,97 @@
+using System.Linq;
+using System;
+using Microsoft.Extensions.Logging;
+using System.Diagnostics;
+using System.Collections.Generic;
+using System.Runtime.Versioning;
+using Cognite.Simulator.Utils.Automation;
+using Cognite.Simulator.Utils;
+using System.Threading;
+using System.Globalization;
+using System.Management;
+using System.Security.Principal;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+
+namespace Cognite.Simulator.Utils
+{
+     public static class ProcessUtils {
+
+        /// <summary>
+        /// Get the owner of a process using WMI
+        /// </summary>
+        /// <param name="processId">The process ID</param>
+        /// <returns>The owner of the process</returns>
+        /// <exception cref="PlatformNotSupportedException">Thrown when the method is called on a non-Windows platform</exception>
+        /// <exception cref="Exception">Thrown when the process is not found or access is denied</exception>
+        public static string GetProcessOwnerWmi(int processId)
+        {
+            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
+                throw new PlatformNotSupportedException("This method is only supported on Windows");
+            }
+            string query = $"SELECT * FROM Win32_Process WHERE ProcessId = {processId}";
+            using (var searcher = new ManagementObjectSearcher(query))
+            using (var results = searcher.Get())
+            {
+                foreach (ManagementObject process in results)
+                {
+                    try
+                    {
+                        object[] args = new object[] { "", "" };
+                        if (Convert.ToInt32(process.InvokeMethod("GetOwner", args)) == 0)
+                        {
+                            string user = (string)args[0];
+                            string domain = (string)args[1];
+                            return $"{domain}\\{user}";
+                        }
+                    }
+                    catch
+                    {
+                        throw new Exception("Access Denied or Process Exited");
+                    }
+                }
+            }
+            throw new Exception("Process not found");
+        }
+
+        /// <summary>
+        /// Kill a process by ID. Can throw exceptions if it is unable to kill the process or if it cannot find the process owner.
+        /// </summary>
+        /// <param name="processId"></param>
+        /// <param name="_logger"></param>
+        public static void KillProcess(string processId, ILogger _logger) {
+            try {
+                Process[] processes = Process.GetProcessesByName(processId);
+                _logger.LogDebug("Searching for process : " + processId);
+                _logger.LogDebug("Found {Count} matching processes", processes.Length);
+                bool found = false;
+                foreach (Process process in processes) {
+                    string owner = GetProcessOwnerWmi(process.Id);
+                    _logger.LogDebug($"Found process . Process owner is : {owner.ToLower()} . Current user is : {GetCurrentUsername().ToLower()}");
+                    if (owner.ToLower() == GetCurrentUsername().ToLower()) {
+                        _logger.LogInformation("Killing process with PID {PID}", process.Id);
+                        process.Kill();
+                        process.WaitForExit();
+                        found = true;
+                    } else {
+                        _logger.LogWarning("Process with PID {PID} is owned by a different user ({Owner}). Skipping.", process.Id, owner);
+                    }
+                }
+                if (!found) {
+                    throw new Exception("No processes found to kill for the current user");
path: Cognite.Simulator.Utils/ProcessUtils.cs
user: login: abdullah-cognite
body: addressed
created_at: 2025-03-10T10:08:32Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,100 @@
+using System.Linq;
+using System;
+using Microsoft.Extensions.Logging;
+using System.Diagnostics;
path: Cognite.Simulator.Utils/ProcessUtils.cs
user: login: dbrattli
body: Sort using statements system, third-party, local. Check all files
created_at: 2025-03-10T13:41:18Z
author_association: NONE
diff_hunk: @@ -0,0 +1,100 @@
+using System.Linq;
+using System;
+using Microsoft.Extensions.Logging;
+using System.Diagnostics;
path: Cognite.Simulator.Utils/ProcessUtils.cs
user: login: abdullah-cognite
body: Since this comment applies to all files in this repo, we can take this up in a separate PR for the whole repository. 
created_at: 2025-03-10T15:08:24Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,100 @@
+using System.Linq;
+using System;
+using Microsoft.Extensions.Logging;
+using System.Diagnostics;
path: Cognite.Simulator.Utils/ProcessUtils.cs
user: login: dbrattli
body: No, this comment is for this file. Check all files in PR is this PR. Check rest of the files in a separate PR.
created_at: 2025-03-10T17:37:49Z
author_association: NONE
diff_hunk: @@ -0,0 +1,100 @@
+using System.Linq;
+using System;
+using Microsoft.Extensions.Logging;
+using System.Diagnostics;
path: Cognite.Simulator.Utils/ProcessUtils.cs
user: login: abdullah-cognite
body: Noted
created_at: 2025-03-11T09:07:07Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,100 @@
+using System.Linq;
+using System;
+using Microsoft.Extensions.Logging;
+using System.Diagnostics;
path: Cognite.Simulator.Utils/ProcessUtils.cs
user: login: abdullah-cognite
body: We are already working on fixing our linter [here](https://github.com/cognitedata/dotnet-simulator-utils/pull/242) , so to avoid dismissing this review, we are taking this up as part of the above PR.
created_at: 2025-03-11T09:26:24Z
author_association: COLLABORATOR

PR comments:
user: login: codecov[bot]
created_at: 2025-02-20T08:28:39Z
body: ## [Codecov](https://app.codecov.io/gh/cognitedata/dotnet-simulator-utils/pull/239?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata) Report
Attention: Patch coverage is `75.10917%` with `57 lines` in your changes missing coverage. Please review.
> Project coverage is 73.26%. Comparing base [(`6e3fb7e`)](https://app.codecov.io/gh/cognitedata/dotnet-simulator-utils/commit/6e3fb7e65c3e4c1337f2bc3026f3d9779862f883?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata) to head [(`d3b0d6a`)](https://app.codecov.io/gh/cognitedata/dotnet-simulator-utils/commit/d3b0d6abce9e5dcf8974ea1d2e634333df382273?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata).
> Report is 1 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/cognitedata/dotnet-simulator-utils/pull/239?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata) | Patch % | Lines |
|---|---|---|
| [Cognite.Simulator.Utils/LicenseController.cs](https://app.codecov.io/gh/cognitedata/dotnet-simulator-utils/pull/239?src=pr&el=tree&filepath=Cognite.Simulator.Utils%2FLicenseController.cs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata#diff-Q29nbml0ZS5TaW11bGF0b3IuVXRpbHMvTGljZW5zZUNvbnRyb2xsZXIuY3M=) | 80.26% | [25 Missing and 5 partials :warning: ](https://app.codecov.io/gh/cognitedata/dotnet-simulator-utils/pull/239?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata) |
| [...nite.Simulator.Utils/Automation/AutomationUtils.cs](https://app.codecov.io/gh/cognitedata/dotnet-simulator-utils/pull/239?src=pr&el=tree&filepath=Cognite.Simulator.Utils%2FAutomation%2FAutomationUtils.cs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata#diff-Q29nbml0ZS5TaW11bGF0b3IuVXRpbHMvQXV0b21hdGlvbi9BdXRvbWF0aW9uVXRpbHMuY3M=) | 0.00% | [14 Missing :warning: ](https://app.codecov.io/gh/cognitedata/dotnet-simulator-utils/pull/239?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata) |
| [Cognite.Simulator.Utils/CommonUtils.cs](https://app.codecov.io/gh/cognitedata/dotnet-simulator-utils/pull/239?src=pr&el=tree&filepath=Cognite.Simulator.Utils%2FCommonUtils.cs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata#diff-Q29nbml0ZS5TaW11bGF0b3IuVXRpbHMvQ29tbW9uVXRpbHMuY3M=) | 33.33% | [3 Missing and 3 partials :warning: ](https://app.codecov.io/gh/cognitedata/dotnet-simulator-utils/pull/239?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata) |
| [Cognite.Simulator.Utils/ProcessUtils.cs](https://app.codecov.io/gh/cognitedata/dotnet-simulator-utils/pull/239?src=pr&el=tree&filepath=Cognite.Simulator.Utils%2FProcessUtils.cs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata#diff-Q29nbml0ZS5TaW11bGF0b3IuVXRpbHMvUHJvY2Vzc1V0aWxzLmNz) | 90.19% | [3 Missing and 2 partials :warning: ](https://app.codecov.io/gh/cognitedata/dotnet-simulator-utils/pull/239?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata) |
| [...Simulator.Utils/DefaultClasses/DefaultConnector.cs](https://app.codecov.io/gh/cognitedata/dotnet-simulator-utils/pull/239?src=pr&el=tree&filepath=Cognite.Simulator.Utils%2FDefaultClasses%2FDefaultConnector.cs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata#diff-Q29nbml0ZS5TaW11bGF0b3IuVXRpbHMvRGVmYXVsdENsYXNzZXMvRGVmYXVsdENvbm5lY3Rvci5jcw==) | 50.00% | [1 Missing :warning: ](https://app.codecov.io/gh/cognitedata/dotnet-simulator-utils/pull/239?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata) |
| [...or.Utils/DefaultClasses/DefaultConnectorRuntime.cs](https://app.codecov.io/gh/cognitedata/dotnet-simulator-utils/pull/239?src=pr&el=tree&filepath=Cognite.Simulator.Utils%2FDefaultClasses%2FDefaultConnectorRuntime.cs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata#diff-Q29nbml0ZS5TaW11bGF0b3IuVXRpbHMvRGVmYXVsdENsYXNzZXMvRGVmYXVsdENvbm5lY3RvclJ1bnRpbWUuY3M=) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/cognitedata/dotnet-simulator-utils/pull/239?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #239      +/-   ##
==========================================
+ Coverage   71.83%   73.26%   +1.42%     
==========================================
  Files          33       35       +2     
  Lines        3554     3740     +186     
  Branches      422      437      +15     
==========================================
+ Hits         2553     2740     +187     
+ Misses        843      840       -3     
- Partials      158      160       +2     
```

| [Files with missing lines](https://app.codecov.io/gh/cognitedata/dotnet-simulator-utils/pull/239?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata) | Coverage Δ | |
|---|---|---|
| [Cognite.Simulator.Utils/Configuration.cs](https://app.codecov.io/gh/cognitedata/dotnet-simulator-utils/pull/239?src=pr&el=tree&filepath=Cognite.Simulator.Utils%2FConfiguration.cs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata#diff-Q29nbml0ZS5TaW11bGF0b3IuVXRpbHMvQ29uZmlndXJhdGlvbi5jcw==) | `76.00% <ø> (+3.84%)` | :arrow_up: |
| [...Simulator.Utils/DefaultClasses/DefaultConnector.cs](https://app.codecov.io/gh/cognitedata/dotnet-simulator-utils/pull/239?src=pr&el=tree&filepath=Cognite.Simulator.Utils%2FDefaultClasses%2FDefaultConnector.cs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata#diff-Q29nbml0ZS5TaW11bGF0b3IuVXRpbHMvRGVmYXVsdENsYXNzZXMvRGVmYXVsdENvbm5lY3Rvci5jcw==) | `84.93% <50.00%> (+2.73%)` | :arrow_up: |
| [...or.Utils/DefaultClasses/DefaultConnectorRuntime.cs](https://app.codecov.io/gh/cognitedata/dotnet-simulator-utils/pull/239?src=pr&el=tree&filepath=Cognite.Simulator.Utils%2FDefaultClasses%2FDefaultConnectorRuntime.cs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata#diff-Q29nbml0ZS5TaW11bGF0b3IuVXRpbHMvRGVmYXVsdENsYXNzZXMvRGVmYXVsdENvbm5lY3RvclJ1bnRpbWUuY3M=) | `55.48% <0.00%> (+0.60%)` | :arrow_up: |
| [Cognite.Simulator.Utils/ProcessUtils.cs](https://app.codecov.io/gh/cognitedata/dotnet-simulator-utils/pull/239?src=pr&el=tree&filepath=Cognite.Simulator.Utils%2FProcessUtils.cs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata#diff-Q29nbml0ZS5TaW11bGF0b3IuVXRpbHMvUHJvY2Vzc1V0aWxzLmNz) | `90.19% <90.19%> (ø)` | |
| [Cognite.Simulator.Utils/CommonUtils.cs](https://app.codecov.io/gh/cognitedata/dotnet-simulator-utils/pull/239?src=pr&el=tree&filepath=Cognite.Simulator.Utils%2FCommonUtils.cs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata#diff-Q29nbml0ZS5TaW11bGF0b3IuVXRpbHMvQ29tbW9uVXRpbHMuY3M=) | `72.67% <33.33%> (-2.04%)` | :arrow_down: |
| [...nite.Simulator.Utils/Automation/AutomationUtils.cs](https://app.codecov.io/gh/cognitedata/dotnet-simulator-utils/pull/239?src=pr&el=tree&filepath=Cognite.Simulator.Utils%2FAutomation%2FAutomationUtils.cs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata#diff-Q29nbml0ZS5TaW11bGF0b3IuVXRpbHMvQXV0b21hdGlvbi9BdXRvbWF0aW9uVXRpbHMuY3M=) | `8.06% <0.00%> (+2.11%)` | :arrow_up: |
| [Cognite.Simulator.Utils/LicenseController.cs](https://app.codecov.io/gh/cognitedata/dotnet-simulator-utils/pull/239?src=pr&el=tree&filepath=Cognite.Simulator.Utils%2FLicenseController.cs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata#diff-Q29nbml0ZS5TaW11bGF0b3IuVXRpbHMvTGljZW5zZUNvbnRyb2xsZXIuY3M=) | `80.26% <80.26%> (ø)` | |

... and [5 files with indirect coverage changes](https://app.codecov.io/gh/cognitedata/dotnet-simulator-utils/pull/239/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cognitedata)

</details>
<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: abdullah-cognite
created_at: 2025-02-28T11:49:07Z
body: This timer drift is really annoying in the license tracker tests, makes them flaky, currently Ive just increased the sleep time by a lot. But there has to be a better way to do this.
author_association: COLLABORATOR
user: login: abdullah-cognite
created_at: 2025-03-03T12:14:12Z
body: > This timer drift is really annoying in the license tracker tests, makes them flaky, currently Ive just increased the sleep time by a lot. But there has to be a better way to do this.

Resolved with using a FakeTimeProvider

Also codecov report from both windows and linux is now being uploaded: https://app.codecov.io/gh/cognitedata/dotnet-simulator-utils/blob/fix-cdf-retries/Cognite.Simulator.Utils%2FProcessUtils.cs
author_association: COLLABORATOR
user: login: dbrattli
created_at: 2025-03-10T14:00:54Z
body: Is this connector still using alpha resources in the SDK ref: `CogniteSdk.Alpha;`?
author_association: NONE
user: login: abdullah-cognite
created_at: 2025-03-10T15:09:04Z
body: > Is this connector still using alpha resources in the SDK ref: `CogniteSdk.Alpha;`?

@dbrattli Yes, because currently thats the maturity level of our [dotnet SDK](https://github.com/cognitedata/cognite-sdk-dotnet/tree/master/CogniteSdk/src/Resources/Alpha) . We need to consider moving out of alpha soon, but that will require a bigger separate PR. 
author_association: COLLABORATOR
user: login: dbrattli
created_at: 2025-03-10T17:42:13Z
body: As a general rule you cannot have a dependency on something less mature than what you are building. This needs to be addressed asap by the team @abdullah-cognite 
author_association: NONE