Skip to content

Commit febc6f6

Browse files
authored
make launching async (#9)
1 parent 4ce3476 commit febc6f6

9 files changed

Lines changed: 63 additions & 42 deletions

File tree

src/MsExcelDiff.Tests/ConcurrentLaunchTests.cs

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public async Task WaitForProcess_FindsNewProcess()
6868
{
6969
var process = StartTestProcess();
7070

71-
using var found = SpreadsheetCompare.WaitForProcess(processName, []);
71+
using var found = await SpreadsheetCompare.WaitForProcess(processName, []);
7272

7373
await Assert.That(found).IsNotNull();
7474
await Assert.That(found!.Id).IsEqualTo(process.Id);
@@ -84,7 +84,7 @@ public async Task WaitForProcess_SkipsExistingPids()
8484
existing.Id
8585
};
8686

87-
using var found = SpreadsheetCompare.WaitForProcess(processName, existingPids);
87+
using var found = await SpreadsheetCompare.WaitForProcess(processName, existingPids);
8888

8989
await Assert.That(found).IsNotNull();
9090
await Assert.That(found!.Id).IsNotEqualTo(existing.Id);
@@ -100,7 +100,7 @@ public async Task WaitForProcess_ReturnsNull_WhenAllPidsExcluded()
100100
};
101101

102102
// Use maxAttempts=1 to avoid 10s timeout
103-
using var found = SpreadsheetCompare.WaitForProcess(processName, existingPids, maxAttempts: 1);
103+
using var found = await SpreadsheetCompare.WaitForProcess(processName, existingPids, maxAttempts: 1);
104104

105105
await Assert.That(found).IsNull();
106106
}
@@ -110,30 +110,29 @@ public async Task SerializedIdentification_YieldsUniqueProcesses()
110110
{
111111
const int count = 5;
112112
var identifiedPids = new ConcurrentBag<int>();
113-
var mutexName = $@"Global\Test_{Guid.NewGuid():N}";
113+
using var semaphore = new SemaphoreSlim(1, 1);
114114

115115
// Simulate N concurrent diffexcel instances, each doing the
116-
// mutex-protected snapshot-launch-identify sequence.
117-
// The mutex ensures each snapshot sees previously identified processes.
116+
// serialized snapshot-launch-identify sequence.
117+
// The semaphore ensures each snapshot sees previously identified processes.
118118
var tasks = Enumerable
119119
.Range(0, count)
120-
.Select(_ => Task.Run(() =>
120+
.Select(_ => Task.Run(async () =>
121121
{
122-
using var mutex = new Mutex(false, mutexName);
123-
mutex.WaitOne();
122+
await semaphore.WaitAsync();
124123
try
125124
{
126125
var existing = SpreadsheetCompare.GetProcessPids(processName);
127126
StartTestProcess();
128-
using var found = SpreadsheetCompare.WaitForProcess(processName, existing);
127+
using var found = await SpreadsheetCompare.WaitForProcess(processName, existing);
129128
if (found != null)
130129
{
131130
identifiedPids.Add(found.Id);
132131
}
133132
}
134133
finally
135134
{
136-
mutex.ReleaseMutex();
135+
semaphore.Release();
137136
}
138137
}))
139138
.ToArray();
@@ -161,7 +160,7 @@ public async Task UnsynchronizedIdentification_CanYieldDuplicates()
161160
var identifiedPids = new List<int>();
162161
for (var i = 0; i < count; i++)
163162
{
164-
using var found = SpreadsheetCompare.WaitForProcess(processName, snapshot);
163+
using var found = await SpreadsheetCompare.WaitForProcess(processName, snapshot);
165164
if (found != null)
166165
{
167166
identifiedPids.Add(found.Id);

src/MsExcelDiff.Tests/LaunchTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ public class LaunchTests
22
{
33
[Test]
44
[Explicit]
5-
public void Launch() =>
5+
public Task Launch() =>
66
SpreadsheetCompare.Launch(
77
ProjectFiles.input_source_xlsx.FullPath,
88
ProjectFiles.input_target_xlsx.FullPath);

src/MsExcelDiff.Tests/TempFilesTests.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ public class TempFilesTests
33
string tempDir = null!;
44

55
[Before(Test)]
6-
public void Setup() =>
6+
public void Setup()
7+
{
78
tempDir = Path.Combine(Path.GetTempPath(), $"MsExcelDiff_Test_{Guid.NewGuid():N}");
9+
Directory.CreateDirectory(tempDir);
10+
}
811

912
[After(Test)]
1013
public void Cleanup()
@@ -23,7 +26,7 @@ public async Task Create_WritesContent()
2326
{
2427
var path = TempFiles.Create(tempDir, "some content");
2528

26-
await Assert.That(File.ReadAllText(path)).IsEqualTo("some content");
29+
await Assert.That(await File.ReadAllTextAsync(path)).IsEqualTo("some content");
2730
}
2831

2932
[Test]

src/MsExcelDiff/CompareCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public async ValueTask ExecuteAsync(IConsole console)
2323

2424
try
2525
{
26-
SpreadsheetCompare.Launch(Path1.FullName, Path2.FullName, settings.SpreadsheetComparePath);
26+
await SpreadsheetCompare.Launch(Path1.FullName, Path2.FullName, settings.SpreadsheetComparePath);
2727
}
2828
catch (Exception exception)
2929
{

src/MsExcelDiff/SpreadsheetCompare.cs

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
public static partial class SpreadsheetCompare
1+
public static class SpreadsheetCompare
22
{
33
static readonly string[] programFolders =
44
[
@@ -56,7 +56,7 @@ public static partial class SpreadsheetCompare
5656
return null;
5757
}
5858

59-
public static void Launch(string path1, string path2, string? exePath = null)
59+
public static async Task Launch(string path1, string path2, string? exePath = null)
6060
{
6161
var exe = FindExecutable(exePath);
6262
if (exe == null)
@@ -77,10 +77,10 @@ Spreadsheet Compare (SPREADSHEETCOMPARE.EXE) was not found.
7777

7878
try
7979
{
80-
using var process = LaunchProcess(exe, tempFile);
80+
using var process = await LaunchProcess(exe, tempFile);
8181

8282
JobObject.AssignProcess(job, process.Handle);
83-
process.WaitForExit();
83+
await process.WaitForExitAsync();
8484
}
8585
catch when (TempFiles.TryDelete(tempFile))
8686
{
@@ -93,7 +93,7 @@ Spreadsheet Compare (SPREADSHEETCOMPARE.EXE) was not found.
9393
}
9494
}
9595

96-
static Process LaunchProcess(string exe, string tempFile)
96+
static async Task<Process> LaunchProcess(string exe, string tempFile)
9797
{
9898
// Click-to-Run Office installs require launching via AppVLP.exe (the App-V
9999
// virtualization layer). SPREADSHEETCOMPARE.EXE crashes if launched directly.
@@ -104,7 +104,7 @@ static Process LaunchProcess(string exe, string tempFile)
104104
return LaunchDirect(exe, tempFile);
105105
}
106106

107-
return LaunchViaAppVlp(appVlp, exe, tempFile);
107+
return await LaunchViaAppVlp(appVlp, exe, tempFile);
108108
}
109109

110110
static Process LaunchDirect(string exe, string tempFile) =>
@@ -115,16 +115,18 @@ static Process LaunchDirect(string exe, string tempFile) =>
115115
})
116116
?? throw new("Failed to start Spreadsheet Compare process");
117117

118-
static Process LaunchViaAppVlp(string appVlp, string exe, string tempFile)
118+
static readonly string lockFilePath = Path.Combine(TempFiles.TempDirectory, ".lock");
119+
120+
static async Task<Process> LaunchViaAppVlp(string appVlp, string exe, string tempFile)
119121
{
120122
// Serialize the snapshot-launch-identify sequence across concurrent
121123
// diffexcel instances. Without this, concurrent instances snapshot the
122124
// same PID set, race to claim the same SPREADSHEETCOMPARE process, and
123125
// leave others orphaned (not in any job object, so they survive when
124126
// diffexcel is killed).
125-
using var mutex = new Mutex(false, @"Global\MsExcelDiff_Launch");
126-
mutex.WaitOne();
127-
try
127+
// Uses a file lock instead of a Mutex because file locks are not
128+
// thread-affine, allowing async code within the critical section.
129+
using (await AcquireFileLock())
128130
{
129131
var existingPids = GetSpreadsheetComparePids();
130132

@@ -137,15 +139,28 @@ static Process LaunchViaAppVlp(string appVlp, string exe, string tempFile)
137139

138140
// AppVLP.exe is a launcher that exits after starting the real process.
139141
// Find the actual SPREADSHEETCOMPARE process and wait on it.
140-
launcher.WaitForExit();
142+
await launcher.WaitForExitAsync();
141143

142-
return WaitForProcess(existingPids)
144+
return await WaitForProcess(existingPids)
143145
?? throw new("Spreadsheet Compare did not start. Ensure the application is installed correctly.");
144146
}
145-
finally
147+
}
148+
149+
static async Task<FileStream> AcquireFileLock()
150+
{
151+
for (var i = 0; i < 300; i++)
146152
{
147-
mutex.ReleaseMutex();
153+
try
154+
{
155+
return new(lockFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
156+
}
157+
catch (IOException)
158+
{
159+
await Task.Delay(100);
160+
}
148161
}
162+
163+
throw new IOException($"Failed to acquire lock file: {lockFilePath}");
149164
}
150165

151166
static HashSet<int> GetSpreadsheetComparePids() =>
@@ -163,10 +178,10 @@ internal static HashSet<int> GetProcessPids(string processName)
163178
return pids;
164179
}
165180

166-
static Process? WaitForProcess(HashSet<int> existingPids) =>
181+
static Task<Process?> WaitForProcess(HashSet<int> existingPids) =>
167182
WaitForProcess("SPREADSHEETCOMPARE", existingPids);
168183

169-
internal static Process? WaitForProcess(string processName, HashSet<int> existingPids, int maxAttempts = 100)
184+
internal static async Task<Process?> WaitForProcess(string processName, HashSet<int> existingPids, int maxAttempts = 100)
170185
{
171186
for (var i = 0; i < maxAttempts; i++)
172187
{
@@ -189,7 +204,7 @@ internal static HashSet<int> GetProcessPids(string processName)
189204
return result;
190205
}
191206

192-
Thread.Sleep(100);
207+
await Task.Delay(100);
193208
}
194209

195210
return null;

src/MsExcelDiff/TempFiles.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
static class TempFiles
22
{
3-
static readonly string directory = Path.Combine(Path.GetTempPath(), "MsExcelDiff");
3+
public static readonly string TempDirectory = Path.Combine(Path.GetTempPath(), "MsExcelDiff");
4+
5+
static TempFiles()
6+
{
7+
Directory.CreateDirectory(TempDirectory);
8+
CleanOld(TempDirectory);
9+
}
410

511
public static string Create(string content) =>
6-
Create(directory, content);
12+
Create(TempDirectory, content);
713

814
internal static string Create(string directory, string content)
915
{
10-
Directory.CreateDirectory(directory);
11-
CleanOld(directory);
1216
var path = Path.Combine(directory, $"{Guid.NewGuid()}.txt");
1317
File.WriteAllText(path, content);
1418
return path;

src/MsWordDiff.Tests/Test.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ public class Test
22
{
33
[Test]
44
[Explicit]
5-
public void Launch() =>
5+
public Task Launch() =>
66
Word.Launch(
77
ProjectFiles.input_temp_docx.FullPath,
88
ProjectFiles.input_target_docx.FullPath);
99

1010
[Test]
1111
[Explicit]
12-
public void LaunchQuiet() =>
12+
public Task LaunchQuiet() =>
1313
Word.Launch(
1414
ProjectFiles.input_temp_docx.FullPath,
1515
ProjectFiles.input_target_docx.FullPath,

src/MsWordDiff/CompareCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public async ValueTask ExecuteAsync(IConsole console)
2828

2929
try
3030
{
31-
Word.Launch(Path1.FullName, Path2.FullName, quiet);
31+
await Word.Launch(Path1.FullName, Path2.FullName, quiet);
3232
}
3333
catch (Exception exception)
3434
{

src/MsWordDiff/Word.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
public static partial class Word
22
{
3-
public static void Launch(string path1, string path2, bool quiet = false)
3+
public static async Task Launch(string path1, string path2, bool quiet = false)
44
{
55
var wordType = Type.GetTypeFromProgID("Word.Application");
66
if (wordType == null)
@@ -41,7 +41,7 @@ public static void Launch(string path1, string path2, bool quiet = false)
4141
// Bring Word to the foreground
4242
SetForegroundWindow(hwnd);
4343

44-
process.WaitForExit();
44+
await process.WaitForExitAsync();
4545

4646
Marshal.ReleaseComObject(compare);
4747
Marshal.ReleaseComObject(word);

0 commit comments

Comments
 (0)