From 2a2a92a0a4144f5dd04a7f1f0e6ef947a1a2064d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 14 Sep 2026 19:25:38 +0000 Subject: [PATCH 1/3] fix: confirm process identity for legacy PID files [patch] The legacy plain-integer PID path treated any process holding the stored PID as a live instance, so a stale legacy PID file left behind by an abnormal exit made the application refuse to start once the operating system recycled that PID onto an unrelated process. This is the same PID-recycling failure the JSON format already guards against. The legacy path now resolves the PID and compares the running process's name against the current process's name before reporting another instance, and treats the PID as stale when identity cannot be confirmed. The existing legacy test pinned the weaker behaviour, so it now asserts that an unrelated running process is not mistaken for an instance, with a new test covering a running process that does carry this application's name. Fixes #167 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012EVRMVaSnFGJQBKLpYbMZ9 --- README.md | 2 +- .../SingleAppInstanceTests.cs | 227 ++++++++++++++---- SingleAppInstance/SingleAppInstance.cs | 49 +++- 3 files changed, 214 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index bb93b07..7423678 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ - **Enhanced Process Identification**: Verifies running instances using multiple attributes (PID, process name, start time, executable path) for accurate detection - **Race Condition Handling**: Includes a built-in 1-second delay to safely detect simultaneous startup attempts - **PID File Management**: Stores process information as JSON in the application data directory -- **Backward Compatibility**: Gracefully handles legacy PID files that stored only a plain integer PID +- **Backward Compatibility**: Gracefully handles legacy PID files that stored only a plain integer PID, confirming the process name before treating a recycled PID as a running instance - **Simple API**: Two methods — `ExitIfAlreadyRunning()` for automatic exit and `ShouldLaunch()` for custom logic - **Multi-Target Support**: Works across .NET 10.0 through .NET 5.0, .NET Standard 2.0/2.1 diff --git a/SingleAppInstance.Test/SingleAppInstanceTests.cs b/SingleAppInstance.Test/SingleAppInstanceTests.cs index fd14664..3a7acb4 100644 --- a/SingleAppInstance.Test/SingleAppInstanceTests.cs +++ b/SingleAppInstance.Test/SingleAppInstanceTests.cs @@ -2,8 +2,10 @@ namespace ktsu.SingleAppInstance.Test; +using System.ComponentModel; using System.Diagnostics; using System.Globalization; +using System.Runtime.InteropServices; using System.Text.Json; [TestClass] @@ -140,38 +142,24 @@ public void IsAlreadyRunning_WithLegacyPidFile_NonExistentProcess_ShouldReturnFa } [TestMethod] - public void IsAlreadyRunning_WithLegacyPidFile_RunningProcess_ShouldReturnTrue() + public void IsAlreadyRunning_WithLegacyPidFile_RunningProcessWithDifferentName_ShouldReturnFalse() { - // Arrange - legacy PID file with a PID of a process that IS running - // Use a well-known process that should always be running + // Arrange - legacy PID file holding the PID of an unrelated running process, which is what a + // stale legacy PID file looks like once the operating system has recycled the PID string pidFilePath = SingleAppInstance.PidFilePath; using Process currentProcess = Process.GetCurrentProcess(); - Process? targetProcess = null; + Process? targetProcess = FindRunningProcessWithDifferentName(currentProcess); try { - // Find a different running process to use - foreach (Process p in Process.GetProcesses()) - { - if (p.Id != currentProcess.Id) - { - targetProcess = p; - break; - } - else - { - p.Dispose(); - } - } - - Assert.IsNotNull(targetProcess, "Should find at least one other running process"); + Assert.IsNotNull(targetProcess, "Should find at least one other running process with a different name"); File.WriteAllText(pidFilePath, targetProcess.Id.ToString(CultureInfo.InvariantCulture)); // Act bool result = SingleAppInstance.IsAlreadyRunning(); // Assert - Assert.IsTrue(result, "Should return true for a running process in legacy format"); + Assert.IsFalse(result, "Should return false when the recycled PID belongs to a process that is not this application"); } finally { @@ -179,6 +167,31 @@ public void IsAlreadyRunning_WithLegacyPidFile_RunningProcess_ShouldReturnTrue() } } + [TestMethod] + public void IsAlreadyRunning_WithLegacyPidFile_RunningProcessWithSameName_ShouldReturnTrue() + { + // Arrange - legacy PID file holding the PID of a running process that carries this + // application's own process name, which is as close to another instance as the legacy + // format can describe + string pidFilePath = SingleAppInstance.PidFilePath; + using Process currentProcess = Process.GetCurrentProcess(); + string currentProcessName = currentProcess.ProcessName; + + using HelperProcess helper = HelperProcess.Start(currentProcessName); + if (!string.Equals(helper.Process.ProcessName, currentProcessName, StringComparison.Ordinal)) + { + Assert.Inconclusive($"Could not launch a helper process named '{currentProcessName}' on this platform; it reported '{helper.Process.ProcessName}'"); + } + + File.WriteAllText(pidFilePath, helper.Process.Id.ToString(CultureInfo.InvariantCulture)); + + // Act + bool result = SingleAppInstance.IsAlreadyRunning(); + + // Assert + Assert.IsTrue(result, "Should return true for a running process that matches this application's name in legacy format"); + } + [TestMethod] public void IsAlreadyRunning_WithNonExistentPid_ShouldReturnFalse() { @@ -376,42 +389,27 @@ public void ShouldLaunch_WithNoExistingInstance_ShouldReturnTrue() [TestMethod] public void ShouldLaunch_WhenAlreadyRunning_ShouldReturnFalse() { - // Arrange - Write a PID file for a different running process using legacy format - // This ensures IsAlreadyRunning() returns true on the first call + // Arrange - describe a genuinely running helper process in the PID file so that the full + // identity verification in IsAlreadyRunning() succeeds on the first call string pidFilePath = SingleAppInstance.PidFilePath; - Process? targetProcess = null; - try - { - // Find a different running process - foreach (Process p in Process.GetProcesses()) - { - if (p.Id != Environment.ProcessId) - { - targetProcess = p; - break; - } - else - { - p.Dispose(); - } - } + using HelperProcess helper = HelperProcess.Start(processName: null); - Assert.IsNotNull(targetProcess, "Should find at least one other running process"); + ProcessInfo processInfo = new() + { + ProcessId = helper.Process.Id, + ProcessName = helper.Process.ProcessName, + StartTime = helper.Process.StartTime, + MainModuleFileName = helper.Process.MainModule?.FileName, + }; - // Write legacy format PID file so IsAlreadyRunning returns true - File.WriteAllText(pidFilePath, targetProcess.Id.ToString(CultureInfo.InvariantCulture)); + File.WriteAllText(pidFilePath, JsonSerializer.Serialize(processInfo)); - // Act - bool result = SingleAppInstance.ShouldLaunch(); + // Act + bool result = SingleAppInstance.ShouldLaunch(); - // Assert - Assert.IsFalse(result, "ShouldLaunch should return false when another instance is detected"); - } - finally - { - targetProcess?.Dispose(); - } + // Assert + Assert.IsFalse(result, "ShouldLaunch should return false when another instance is detected"); } [TestMethod] @@ -516,6 +514,135 @@ public void IsAlreadyRunning_WithJsonArrayInPidFile_ShouldReturnFalse() Assert.IsFalse(result); } + /// + /// Finds a running process that is neither the current process nor shares its process name. + /// + private static Process? FindRunningProcessWithDifferentName(Process currentProcess) + { + foreach (Process candidate in Process.GetProcesses()) + { + bool isUsable = false; + + try + { + isUsable = candidate.Id != currentProcess.Id && + !string.Equals(candidate.ProcessName, currentProcess.ProcessName, StringComparison.Ordinal); + } + catch (InvalidOperationException) + { + // Process exited between enumeration and inspection + } + catch (Win32Exception) + { + // Process details are not accessible + } + + if (isUsable) + { + return candidate; + } + + candidate.Dispose(); + } + + return null; + } + + /// + /// A long-lived child process used to stand in for another instance of the application. + /// + /// + /// When a process name is requested, a long-running system executable is copied to a temporary + /// file with that name so the child reports the requested process name to the operating system. + /// + private sealed class HelperProcess : IDisposable + { + private readonly string? temporaryDirectory; + + public Process Process { get; } + + private HelperProcess(Process process, string? temporaryDirectory) + { + Process = process; + this.temporaryDirectory = temporaryDirectory; + } + + public static HelperProcess Start(string? processName) + { + bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + string executable = isWindows + ? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "ping.exe") + : "/bin/sleep"; + string arguments = isWindows ? "-n 120 127.0.0.1" : "120"; + string? temporaryDirectory = null; + + if (processName is not null) + { + temporaryDirectory = Path.Combine(Path.GetTempPath(), $"{nameof(SingleAppInstanceTests)}_{Guid.NewGuid():N}"); + Directory.CreateDirectory(temporaryDirectory); + + string copiedExecutable = Path.Combine(temporaryDirectory, isWindows ? $"{processName}.exe" : processName); + File.Copy(executable, copiedExecutable); + + if (!isWindows) + { + File.SetUnixFileMode(copiedExecutable, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute); + } + + executable = copiedExecutable; + } + + ProcessStartInfo startInfo = new(executable, arguments) + { + UseShellExecute = false, + CreateNoWindow = true, + }; + + Process? process = Process.Start(startInfo); + Assert.IsNotNull(process, "Should be able to start a helper process"); + + return new HelperProcess(process, temporaryDirectory); + } + + public void Dispose() + { + try + { + if (!Process.HasExited) + { + Process.Kill(); + Process.WaitForExit(5000); + } + } + catch (InvalidOperationException) + { + // Process already exited + } + catch (Win32Exception) + { + // Process could not be signalled + } + + Process.Dispose(); + + if (temporaryDirectory is not null) + { + try + { + Directory.Delete(temporaryDirectory, recursive: true); + } + catch (IOException) + { + // Temporary files are left for the operating system to reclaim + } + catch (UnauthorizedAccessException) + { + // Temporary files are left for the operating system to reclaim + } + } + } + } + // This class needs to mirror the internal ProcessInfo class for testing private sealed class ProcessInfo { diff --git a/SingleAppInstance/SingleAppInstance.cs b/SingleAppInstance/SingleAppInstance.cs index b036d2e..37faf89 100644 --- a/SingleAppInstance/SingleAppInstance.cs +++ b/SingleAppInstance/SingleAppInstance.cs @@ -175,7 +175,7 @@ private static bool CheckPidFileContents(string pidFileContents, int currentPid) /// /// The raw contents of the PID file. /// The current process ID. - /// true if the legacy PID corresponds to a running process; otherwise, false. + /// true if the legacy PID corresponds to a running instance of this application; otherwise, false. private static bool HandleLegacyPidFile(string pidFileContents, int currentPid) { if (!int.TryParse(pidFileContents, NumberStyles.Integer, CultureInfo.InvariantCulture, out int filePid)) @@ -188,7 +188,7 @@ private static bool HandleLegacyPidFile(string pidFileContents, int currentPid) return false; } - return IsProcessRunning(filePid); + return IsLegacyProcessRunning(filePid); } /// @@ -257,23 +257,46 @@ private static bool IsStoredProcessRunningByName(ProcessInfo storedProcess) } /// - /// Checks if a process with the given PID is currently running. + /// Checks if the PID read from a legacy PID file belongs to another running instance of this application. /// - /// The process ID to check. - /// true if a process with the given PID is running; otherwise, false. - private static bool IsProcessRunning(int pid) + /// The process ID read from the legacy PID file. + /// true if the PID belongs to a running process with the same name as the current process; otherwise, false. + /// + /// The legacy format stores nothing but the PID, so a stale file left behind by an abnormal exit + /// will match whatever unrelated process the operating system later recycles that PID onto. The + /// running process's name is therefore compared against the current process's name before it is + /// treated as another instance, mirroring the identity confirmation the JSON format performs. + /// When identity cannot be confirmed the PID is treated as stale so the application still launches. + /// + private static bool IsLegacyProcessRunning(int pid) { - Process[] processes = Process.GetProcesses(); + string currentProcessName; + using (Process currentProcess = Process.GetCurrentProcess()) + { + currentProcessName = currentProcess.ProcessName; + } + try { - return Array.Exists(processes, p => p.Id == pid); + using Process runningProcess = Process.GetProcessById(pid); + + return !runningProcess.HasExited && + string.Equals(runningProcess.ProcessName, currentProcessName, StringComparison.Ordinal); } - finally + catch (ArgumentException) { - foreach (Process p in processes) - { - p.Dispose(); - } + // Process not found - no longer running + return false; + } + catch (InvalidOperationException) + { + // Process has exited + return false; + } + catch (System.ComponentModel.Win32Exception) + { + // Access denied to process details - identity cannot be confirmed + return false; } } From 094a6ea84990db2dc9e84bfeb9806abb87d2e159 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 14 Sep 2026 19:28:41 +0000 Subject: [PATCH 2/3] style: use a using declaration for the legacy PID test process [patch] Addresses the CodeQL "missed using opportunity" review comment on the manual try/finally disposal. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012EVRMVaSnFGJQBKLpYbMZ9 --- .../SingleAppInstanceTests.cs | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/SingleAppInstance.Test/SingleAppInstanceTests.cs b/SingleAppInstance.Test/SingleAppInstanceTests.cs index 3a7acb4..91ebcf6 100644 --- a/SingleAppInstance.Test/SingleAppInstanceTests.cs +++ b/SingleAppInstance.Test/SingleAppInstanceTests.cs @@ -148,23 +148,16 @@ public void IsAlreadyRunning_WithLegacyPidFile_RunningProcessWithDifferentName_S // stale legacy PID file looks like once the operating system has recycled the PID string pidFilePath = SingleAppInstance.PidFilePath; using Process currentProcess = Process.GetCurrentProcess(); - Process? targetProcess = FindRunningProcessWithDifferentName(currentProcess); + using Process? targetProcess = FindRunningProcessWithDifferentName(currentProcess); - try - { - Assert.IsNotNull(targetProcess, "Should find at least one other running process with a different name"); - File.WriteAllText(pidFilePath, targetProcess.Id.ToString(CultureInfo.InvariantCulture)); + Assert.IsNotNull(targetProcess, "Should find at least one other running process with a different name"); + File.WriteAllText(pidFilePath, targetProcess.Id.ToString(CultureInfo.InvariantCulture)); - // Act - bool result = SingleAppInstance.IsAlreadyRunning(); + // Act + bool result = SingleAppInstance.IsAlreadyRunning(); - // Assert - Assert.IsFalse(result, "Should return false when the recycled PID belongs to a process that is not this application"); - } - finally - { - targetProcess?.Dispose(); - } + // Assert + Assert.IsFalse(result, "Should return false when the recycled PID belongs to a process that is not this application"); } [TestMethod] From f6231c1bdb931e77a519b118fac38c7efec0f66c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 14 Sep 2026 19:36:54 +0000 Subject: [PATCH 3/3] fix: tolerate truncated process names in the legacy PID check [patch] Windows CI showed ShouldLaunch_WhenAlreadyRunning_ShouldReturnFalse failing: it described a helper process in the JSON format, which depends on MainModule resolving identically for a freshly started child. That arrangement now uses the same legacy lookalike process the other legacy test uses, which passes on all three platforms. Running the tests in Release then exposed a real gap in the fix: Linux reports another process's name truncated to 15 characters while the current process reports its full name, so a genuine second instance of an application with a longer name would no longer be detected. The legacy check now accepts a name truncated at exactly that limit when it prefixes the current process's name, and the test that looks for an unrelated process skips any candidate whose name prefixes it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012EVRMVaSnFGJQBKLpYbMZ9 --- .../SingleAppInstanceTests.cs | 112 +++++++++--------- SingleAppInstance/SingleAppInstance.cs | 30 ++++- 2 files changed, 87 insertions(+), 55 deletions(-) diff --git a/SingleAppInstance.Test/SingleAppInstanceTests.cs b/SingleAppInstance.Test/SingleAppInstanceTests.cs index 91ebcf6..871d8b9 100644 --- a/SingleAppInstance.Test/SingleAppInstanceTests.cs +++ b/SingleAppInstance.Test/SingleAppInstanceTests.cs @@ -167,14 +167,7 @@ public void IsAlreadyRunning_WithLegacyPidFile_RunningProcessWithSameName_Should // application's own process name, which is as close to another instance as the legacy // format can describe string pidFilePath = SingleAppInstance.PidFilePath; - using Process currentProcess = Process.GetCurrentProcess(); - string currentProcessName = currentProcess.ProcessName; - - using HelperProcess helper = HelperProcess.Start(currentProcessName); - if (!string.Equals(helper.Process.ProcessName, currentProcessName, StringComparison.Ordinal)) - { - Assert.Inconclusive($"Could not launch a helper process named '{currentProcessName}' on this platform; it reported '{helper.Process.ProcessName}'"); - } + using HelperProcess helper = StartInstanceLookalikeProcess(); File.WriteAllText(pidFilePath, helper.Process.Id.ToString(CultureInfo.InvariantCulture)); @@ -382,21 +375,12 @@ public void ShouldLaunch_WithNoExistingInstance_ShouldReturnTrue() [TestMethod] public void ShouldLaunch_WhenAlreadyRunning_ShouldReturnFalse() { - // Arrange - describe a genuinely running helper process in the PID file so that the full - // identity verification in IsAlreadyRunning() succeeds on the first call + // Arrange - a running process carrying this application's name, recorded in the legacy + // format, is what another instance looks like to IsAlreadyRunning() string pidFilePath = SingleAppInstance.PidFilePath; + using HelperProcess helper = StartInstanceLookalikeProcess(); - using HelperProcess helper = HelperProcess.Start(processName: null); - - ProcessInfo processInfo = new() - { - ProcessId = helper.Process.Id, - ProcessName = helper.Process.ProcessName, - StartTime = helper.Process.StartTime, - MainModuleFileName = helper.Process.MainModule?.FileName, - }; - - File.WriteAllText(pidFilePath, JsonSerializer.Serialize(processInfo)); + File.WriteAllText(pidFilePath, helper.Process.Id.ToString(CultureInfo.InvariantCulture)); // Act bool result = SingleAppInstance.ShouldLaunch(); @@ -518,8 +502,10 @@ public void IsAlreadyRunning_WithJsonArrayInPidFile_ShouldReturnFalse() try { + // A name that prefixes the current process's name is excluded too, because a + // platform that truncates process names reports this application that way isUsable = candidate.Id != currentProcess.Id && - !string.Equals(candidate.ProcessName, currentProcess.ProcessName, StringComparison.Ordinal); + !currentProcess.ProcessName.StartsWith(candidate.ProcessName, StringComparison.Ordinal); } catch (InvalidOperationException) { @@ -541,48 +527,69 @@ public void IsAlreadyRunning_WithJsonArrayInPidFile_ShouldReturnFalse() return null; } + /// + /// Launches a long-lived process that reports this application's own process name, so it is + /// indistinguishable from another instance to a PID file that records nothing but a PID. + /// + private static HelperProcess StartInstanceLookalikeProcess() + { + using Process currentProcess = Process.GetCurrentProcess(); + string currentProcessName = currentProcess.ProcessName; + + HelperProcess helper = HelperProcess.Start(currentProcessName); + string helperProcessName = helper.Process.ProcessName; + + // Some platforms report another process's name truncated, so a prefix of the current + // process's name is as close a lookalike as the platform allows + bool isLookalike = string.Equals(helperProcessName, currentProcessName, StringComparison.Ordinal) || + (helperProcessName.Length < currentProcessName.Length && + currentProcessName.StartsWith(helperProcessName, StringComparison.Ordinal)); + + if (!isLookalike) + { + helper.Dispose(); + Assert.Inconclusive($"Could not launch a helper process named '{currentProcessName}' on this platform; it reported '{helperProcessName}'"); + } + + return helper; + } + /// /// A long-lived child process used to stand in for another instance of the application. /// /// - /// When a process name is requested, a long-running system executable is copied to a temporary - /// file with that name so the child reports the requested process name to the operating system. + /// A long-running system executable is copied to a temporary file named after the requested + /// process name, so the child reports that process name to the operating system. /// private sealed class HelperProcess : IDisposable { - private readonly string? temporaryDirectory; + private readonly string temporaryDirectory; public Process Process { get; } - private HelperProcess(Process process, string? temporaryDirectory) + private HelperProcess(Process process, string temporaryDirectory) { Process = process; this.temporaryDirectory = temporaryDirectory; } - public static HelperProcess Start(string? processName) + public static HelperProcess Start(string processName) { bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - string executable = isWindows + string systemExecutable = isWindows ? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "ping.exe") : "/bin/sleep"; string arguments = isWindows ? "-n 120 127.0.0.1" : "120"; - string? temporaryDirectory = null; - - if (processName is not null) - { - temporaryDirectory = Path.Combine(Path.GetTempPath(), $"{nameof(SingleAppInstanceTests)}_{Guid.NewGuid():N}"); - Directory.CreateDirectory(temporaryDirectory); - string copiedExecutable = Path.Combine(temporaryDirectory, isWindows ? $"{processName}.exe" : processName); - File.Copy(executable, copiedExecutable); + string temporaryDirectory = Path.Combine(Path.GetTempPath(), $"{nameof(SingleAppInstanceTests)}_{Guid.NewGuid():N}"); + Directory.CreateDirectory(temporaryDirectory); - if (!isWindows) - { - File.SetUnixFileMode(copiedExecutable, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute); - } + string executable = Path.Combine(temporaryDirectory, isWindows ? $"{processName}.exe" : processName); + File.Copy(systemExecutable, executable); - executable = copiedExecutable; + if (!isWindows) + { + File.SetUnixFileMode(executable, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute); } ProcessStartInfo startInfo = new(executable, arguments) @@ -618,20 +625,17 @@ public void Dispose() Process.Dispose(); - if (temporaryDirectory is not null) + try { - try - { - Directory.Delete(temporaryDirectory, recursive: true); - } - catch (IOException) - { - // Temporary files are left for the operating system to reclaim - } - catch (UnauthorizedAccessException) - { - // Temporary files are left for the operating system to reclaim - } + Directory.Delete(temporaryDirectory, recursive: true); + } + catch (IOException) + { + // Temporary files are left for the operating system to reclaim + } + catch (UnauthorizedAccessException) + { + // Temporary files are left for the operating system to reclaim } } } diff --git a/SingleAppInstance/SingleAppInstance.cs b/SingleAppInstance/SingleAppInstance.cs index 37faf89..f7803f2 100644 --- a/SingleAppInstance/SingleAppInstance.cs +++ b/SingleAppInstance/SingleAppInstance.cs @@ -281,7 +281,7 @@ private static bool IsLegacyProcessRunning(int pid) using Process runningProcess = Process.GetProcessById(pid); return !runningProcess.HasExited && - string.Equals(runningProcess.ProcessName, currentProcessName, StringComparison.Ordinal); + IsSameApplicationName(runningProcess.ProcessName, currentProcessName); } catch (ArgumentException) { @@ -300,6 +300,34 @@ private static bool IsLegacyProcessRunning(int pid) } } + /// + /// The longest process name some platforms report when asked about a process other than the current one. + /// + /// Linux stores at most 15 characters of a process name, so longer names are read back truncated. + private const int TruncatedProcessNameLength = 15; + + /// + /// Compares the name of another running process against the current process's own name. + /// + /// The name reported for the other running process. + /// The name reported for the current process. + /// true if both names describe the same application; otherwise, false. + /// + /// The current process reports its full name while another process's name can come back truncated, + /// so a name truncated at exactly the platform limit is accepted when it prefixes the current name. + /// + private static bool IsSameApplicationName(string runningProcessName, string currentProcessName) + { + if (string.Equals(runningProcessName, currentProcessName, StringComparison.Ordinal)) + { + return true; + } + + return runningProcessName.Length == TruncatedProcessNameLength && + currentProcessName.Length > TruncatedProcessNameLength && + currentProcessName.StartsWith(runningProcessName, StringComparison.Ordinal); + } + /// /// Writes the current process information to the PID file. ///