Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions docs/mdsource/tray.source.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,18 @@ Clicking "file1" or "file2" will delete file1 or file2 respectively. The drop do
"Accept all" will accept all pending moves and all pending deletes.


### Locked files

If accepting a move fails because the files are locked by another process (for example the snapshot is open in Microsoft Word), a prompt is shown listing the locked files and the locking processes:

<img src="..\src\DiffEngineTray.Tests\LockedFilesFormTests.Default.verified.png">

* "Ignore" leaves the move pending so it can be accepted later.
* "Kill [process] and accept" kills the locking processes and accepts the move.
* "Kill and accept all pending" kills the locking processes and accepts all pending moves, killing any other locking processes without further prompts.
* "Always kill" kills the locking processes and accepts the move. The choice is stored in settings, so future locked files are killed without prompting. It can be toggled in the Options dialog.


### Discard

Discard will clear all currently tracked items.
Expand Down Expand Up @@ -79,6 +91,11 @@ By default, when a diff is opened, the temp file will be on the left and the tar
Control the [max instances to launch setting](docs/diff-tool.md#maxinstancestolaunch).


#### Always kill locking processes

When accepting a move with [locked files](#locked-files), kill the locking processes without prompting.


#### Discard all HotKey

Registers a system wide HotKey to discard pending:
Expand Down
17 changes: 17 additions & 0 deletions docs/tray.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ Clicking "file1" or "file2" will delete file1 or file2 respectively. The drop do
"Accept all" will accept all pending moves and all pending deletes.


### Locked files

If accepting a move fails because the files are locked by another process (for example the snapshot is open in Microsoft Word), a prompt is shown listing the locked files and the locking processes:

<img src="..\src\DiffEngineTray.Tests\LockedFilesFormTests.Default.verified.png">

* "Ignore" leaves the move pending so it can be accepted later.
* "Kill [process] and accept" kills the locking processes and accepts the move.
* "Kill and accept all pending" kills the locking processes and accepts all pending moves, killing any other locking processes without further prompts.
* "Always kill" kills the locking processes and accepts the move. The choice is stored in settings, so future locked files are killed without prompting. It can be toggled in the Options dialog.


### Discard

Discard will clear all currently tracked items.
Expand Down Expand Up @@ -86,6 +98,11 @@ By default, when a diff is opened, the temp file will be on the left and the tar
Control the [max instances to launch setting](docs/diff-tool.md#maxinstancestolaunch).


#### Always kill locking processes

When accepting a move with [locked files](#locked-files), kill the locking processes without prompting.


#### Discard all HotKey

Registers a system wide HotKey to discard pending:
Expand Down
102 changes: 46 additions & 56 deletions src/DiffEngineTray.Tests/FileLockKillerTest.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,44 @@
public class FileLockKillerTest
{
[Test]
public async Task GetLockingProcesses_WhenFileNotLocked_ReturnsEmpty()
{
var file = Path.Combine(Path.GetTempPath(), $"FileLockKillerTest_{Guid.NewGuid()}.txt");
try
{
File.WriteAllText(file, "content");
var result = FileLockKiller.GetLockingProcesses(file);
await Assert.That(result).IsEmpty();
}
finally
{
File.Delete(file);
}
}

[Test]
public async Task GetLockingProcesses_WhenFileLocked_ReturnsProcess()
{
var file = Path.Combine(Path.GetTempPath(), $"FileLockKillerTest_{Guid.NewGuid()}.txt");
File.WriteAllText(file, "content");

var lockProcess = FileLockUtils.StartFileLockProcess(file);

try
{
await Assert.That(FileLockUtils.IsFileLocked(file)).IsTrue();

var result = FileLockKiller.GetLockingProcesses(file);

await Assert.That(result.Select(_ => _.ProcessId)).Contains(lockProcess.Id);
}
finally
{
FileLockUtils.Cleanup(lockProcess);
File.Delete(file);
}
}

[Test]
public async Task KillLockingProcesses_WhenFileNotLocked_ReturnsFalse()
{
Expand Down Expand Up @@ -30,11 +69,11 @@ public async Task KillLockingProcesses_WhenFileLocked_KillsProcess()
var file = Path.Combine(Path.GetTempPath(), $"FileLockKillerTest_{Guid.NewGuid()}.txt");
File.WriteAllText(file, "content");

var lockProcess = StartFileLockProcess(file);
var lockProcess = FileLockUtils.StartFileLockProcess(file);

try
{
await Assert.That(IsFileLocked(file)).IsTrue();
await Assert.That(FileLockUtils.IsFileLocked(file)).IsTrue();

var result = FileLockKiller.KillLockingProcesses(file);

Expand All @@ -45,12 +84,7 @@ public async Task KillLockingProcesses_WhenFileLocked_KillsProcess()
}
finally
{
if (!lockProcess.HasExited)
{
lockProcess.Kill();
}

lockProcess.Dispose();
FileLockUtils.Cleanup(lockProcess);
File.Delete(file);
}
}
Expand All @@ -63,11 +97,11 @@ public async Task MoveSucceedsAfterKillingLockingProcess()
File.WriteAllText(file, "content");
File.WriteAllText(tempFile, "new content");

var lockProcess = StartFileLockProcess(file);
var lockProcess = FileLockUtils.StartFileLockProcess(file);

try
{
await Assert.That(IsFileLocked(file)).IsTrue();
await Assert.That(FileLockUtils.IsFileLocked(file)).IsTrue();
await Assert.That(FileEx.SafeMove(tempFile, file)).IsFalse();

FileLockKiller.KillLockingProcesses(file);
Expand All @@ -77,53 +111,9 @@ public async Task MoveSucceedsAfterKillingLockingProcess()
}
finally
{
if (!lockProcess.HasExited)
{
lockProcess.Kill();
}

lockProcess.Dispose();
FileLockUtils.Cleanup(lockProcess);
File.Delete(file);
File.Delete(tempFile);
}
}

static Process StartFileLockProcess(string path)
{
var script = $"$f = [System.IO.File]::Open('{path.Replace("'", "''")}', 'Open', 'ReadWrite', 'None'); [Console]::WriteLine('locked'); Start-Sleep -Seconds 60";
var process = new Process
{
StartInfo = new()
{
FileName = "powershell.exe",
Arguments = $"-NoProfile -Command \"{script}\"",
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true
}
};
process.Start();

// Wait for the process to signal that it has acquired the lock
var line = process.StandardOutput.ReadLine();
if (line != "locked")
{
throw new InvalidOperationException($"Expected 'locked' but got '{line}'");
}

return process;
}

static bool IsFileLocked(string path)
{
try
{
using var stream = File.Open(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None);
return false;
}
catch (IOException)
{
return true;
}
}
}
}
52 changes: 52 additions & 0 deletions src/DiffEngineTray.Tests/FileLockUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
static class FileLockUtils
{
public static Process StartFileLockProcess(string path)
{
var script = $"$f = [System.IO.File]::Open('{path.Replace("'", "''")}', 'Open', 'ReadWrite', 'None'); [Console]::WriteLine('locked'); Start-Sleep -Seconds 60";
var process = new Process
{
StartInfo = new()
{
FileName = "powershell.exe",
Arguments = $"-NoProfile -Command \"{script}\"",
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true
}
};
process.Start();

// Wait for the process to signal that it has acquired the lock
var line = process.StandardOutput.ReadLine();
if (line != "locked")
{
throw new InvalidOperationException($"Expected 'locked' but got '{line}'");
}

return process;
}

public static bool IsFileLocked(string path)
{
try
{
using var stream = File.Open(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None);
return false;
}
catch (IOException)
{
return true;
}
}

public static void Cleanup(Process process)
{
if (!process.HasExited)
{
process.Kill();
process.WaitForExit(5000);
}

process.Dispose();
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 38 additions & 0 deletions src/DiffEngineTray.Tests/LockedFilesFormTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#if DEBUG
public class LockedFilesFormTests
{
//[Test]
//[Explicit]
//public void Launch()
//{
// using var form = new LockedFilesForm(BuildMove(), BuildLocked());
// form.ShowDialog();
//}

[Test]
public async Task Default()
{
using var form = new LockedFilesForm(BuildMove(), BuildLocked());
await Verify(form);
}

static TrackedMove BuildMove() =>
new(
@"C:\tests\AdviceSummaryTests.BuildWord.received.docx",
@"C:\tests\AdviceSummaryTests.BuildWord.verified.docx",
null,
null,
false,
null,
null,
"docx");

static LockedFiles BuildLocked() =>
new(
[
@"C:\tests\AdviceSummaryTests.BuildWord.received.docx",
@"C:\tests\AdviceSummaryTests.BuildWord.verified.docx"
],
[new(1234, "Microsoft Word")]);
}
#endif
Binary file modified src/DiffEngineTray.Tests/OptionsFormTests.Default.verified.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src/DiffEngineTray.Tests/OptionsFormTests.WithKeys.verified.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 4 additions & 3 deletions src/DiffEngineTray.Tests/RecordingTracker.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
class RecordingTracker() :
class RecordingTracker(LockedFilesResolver? lockedFilesResolver = null) :
Tracker(
() =>
{
},
() =>
{
})
},
lockedFilesResolver)
{
public async Task AssertEmpty()
{
await Assert.That(Deletes).IsEmpty();
await Assert.That(Moves).IsEmpty();
await Assert.That(TrackingAny).IsFalse();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
Key: T
},
RunAtStartup: false,
AlwaysKillLockingProcesses: true,
TargetOnLeft: false,
MaxInstancesToLaunch: 5
}
3 changes: 2 additions & 1 deletion src/DiffEngineTray.Tests/SettingsHelperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ await SettingsHelper.Write(
Key = "T"
},
MaxInstancesToLaunch = 5,
TargetOnLeft = false
TargetOnLeft = false,
AlwaysKillLockingProcesses = true
});

var result = await SettingsHelper.Read();
Expand Down
Loading
Loading