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
14 changes: 12 additions & 2 deletions claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ dotnet run -c Release --project src/DiffEngineViewer.Windows.Benchmarks -- --fil

**Target Frameworks:**
- DiffEngine library: net462, net472, net48, net6.0, net7.0, net8.0, net9.0, net10.0 (Windows also includes .NET Framework targets)
- DiffEngineTray: net10.0 Windows Forms application
- DiffEngineTray: net10.0 Windows Forms application, on DiffEngineTray.Core (net10.0, every platform)
- Tests: net10.0 (net48 on Windows)

## Architecture Overview
Expand Down Expand Up @@ -677,7 +677,17 @@ the comment there about not caching "nothing staged" asks for.
`libX11` is left of them.

**DiffEngineTray (`src/DiffEngineTray/`):**
- Windows Forms tray application that handles pending file diffs
- Windows Forms tray application that handles pending file diffs, in two projects. `src/DiffEngineTray.Core`
is everything that is not WinForms - `Tracker`, both inline hosts, `PiperServer`, the settings,
`DebugReport`, `IssueLauncher` and the Windows API helpers `ProcessEx` and `FileLockKiller`, which
compile anywhere and run only on Windows until they have Unix paths - and builds on every
platform, so a macOS or Linux head can be put on it. `src/DiffEngineTray` is the Windows head:
`Program`, the menu, the forms, the hot keys (`KeyRegister`, `KeyName`, which parses into WinForms'
`Keys`), the icons and the Run key. Two seams cross between them. `LockedFilesPolicy.AlwaysKill` is
the core's, and the question a locked file asks is the head's form (`LockedFilesHandler`).
`IssueLauncher.Declined` declines in the core, which has no toolkit to ask with, and the head
hands it `IssueQuestion`'s MessageBox first thing in Main. The core is signed and named in
DiffEngine's `InternalsVisibleTo`, and names the head and the tests in its own
- `PiperServer` - TCP server (localhost, 3492) receiving move/delete payloads from DiffEngine.
Deliberately a second listener beside the viewer protocol, not debt: its format is frozen
(every stable DiffEngine embeds PiperClient, pinned in test projects while the tray updates
Expand Down
4 changes: 4 additions & 0 deletions src/DiffEngine.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@
<BuildType Solution="Release-NotWindows|*" Project="Release" />
<Build Solution="Release-NotWindows|*" Project="false" />
</Project>
<!-- Builds everywhere: the tray's non-WinForms half, which a macOS or Linux head shares. -->
<Project Path="DiffEngineTray.Core/DiffEngineTray.Core.csproj">
<BuildType Solution="Release-NotWindows|*" Project="Release" />
</Project>
<Project Path="FakeDiffTool/FakeDiffTool.csproj">
<BuildType Solution="Release-NotWindows|*" Project="Release" />
<!--
Expand Down
3 changes: 2 additions & 1 deletion src/DiffEngine/InternalsVisibleTo.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[assembly: InternalsVisibleTo("DiffEngine.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001000f0a8e4bf1639dce01be6592384e7dfc621915b7759fb5cee42ec5d351bcc43460432da1659ee618ca6cab6b8b8e56a5deb5d4ee1a49783d5c2690752502d31ccbfee9b2c697e20359b55ad100cc9370c8e983fd9496f01d761a060d0435bac7243b1832ba95757aa5adbb67df38c213d717b6751e1217cea9fa5c61e9b799dd")]
[assembly: InternalsVisibleTo("DiffEngine.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001000f0a8e4bf1639dce01be6592384e7dfc621915b7759fb5cee42ec5d351bcc43460432da1659ee618ca6cab6b8b8e56a5deb5d4ee1a49783d5c2690752502d31ccbfee9b2c697e20359b55ad100cc9370c8e983fd9496f01d761a060d0435bac7243b1832ba95757aa5adbb67df38c213d717b6751e1217cea9fa5c61e9b799dd")]
[assembly: InternalsVisibleTo("VersionTests, PublicKey=00240000048000009400000006020000002400005253413100040000010001000f0a8e4bf1639dce01be6592384e7dfc621915b7759fb5cee42ec5d351bcc43460432da1659ee618ca6cab6b8b8e56a5deb5d4ee1a49783d5c2690752502d31ccbfee9b2c697e20359b55ad100cc9370c8e983fd9496f01d761a060d0435bac7243b1832ba95757aa5adbb67df38c213d717b6751e1217cea9fa5c61e9b799dd")]
[assembly: InternalsVisibleTo("DiffEngineTray, PublicKey=00240000048000009400000006020000002400005253413100040000010001000f0a8e4bf1639dce01be6592384e7dfc621915b7759fb5cee42ec5d351bcc43460432da1659ee618ca6cab6b8b8e56a5deb5d4ee1a49783d5c2690752502d31ccbfee9b2c697e20359b55ad100cc9370c8e983fd9496f01d761a060d0435bac7243b1832ba95757aa5adbb67df38c213d717b6751e1217cea9fa5c61e9b799dd")]
[assembly: InternalsVisibleTo("DiffEngineTray.Core, PublicKey=00240000048000009400000006020000002400005253413100040000010001000f0a8e4bf1639dce01be6592384e7dfc621915b7759fb5cee42ec5d351bcc43460432da1659ee618ca6cab6b8b8e56a5deb5d4ee1a49783d5c2690752502d31ccbfee9b2c697e20359b55ad100cc9370c8e983fd9496f01d761a060d0435bac7243b1832ba95757aa5adbb67df38c213d717b6751e1217cea9fa5c61e9b799dd")]
[assembly: InternalsVisibleTo("DiffEngineTray.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001000f0a8e4bf1639dce01be6592384e7dfc621915b7759fb5cee42ec5d351bcc43460432da1659ee618ca6cab6b8b8e56a5deb5d4ee1a49783d5c2690752502d31ccbfee9b2c697e20359b55ad100cc9370c8e983fd9496f01d761a060d0435bac7243b1832ba95757aa5adbb67df38c213d717b6751e1217cea9fa5c61e9b799dd")]
[assembly: InternalsVisibleTo("DiffEngineViewer.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001000f0a8e4bf1639dce01be6592384e7dfc621915b7759fb5cee42ec5d351bcc43460432da1659ee618ca6cab6b8b8e56a5deb5d4ee1a49783d5c2690752502d31ccbfee9b2c697e20359b55ad100cc9370c8e983fd9496f01d761a060d0435bac7243b1832ba95757aa5adbb67df38c213d717b6751e1217cea9fa5c61e9b799dd")]
[assembly: InternalsVisibleTo("DiffEngine.Benchmarks, PublicKey=00240000048000009400000006020000002400005253413100040000010001000f0a8e4bf1639dce01be6592384e7dfc621915b7759fb5cee42ec5d351bcc43460432da1659ee618ca6cab6b8b8e56a5deb5d4ee1a49783d5c2690752502d31ccbfee9b2c697e20359b55ad100cc9370c8e983fd9496f01d761a060d0435bac7243b1832ba95757aa5adbb67df38c213d717b6751e1217cea9fa5c61e9b799dd")]
File renamed without changes.
25 changes: 25 additions & 0 deletions src/DiffEngineTray.Core/DiffEngineTray.Core.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">

<!--
Everything the tray does that is not WinForms: the tracked moves and deletes, the inline queue it
owns or drives, the piper port, the settings and the debug report. Built and tested on every
platform, so a head for macOS and Linux can be put on it. ProcessEx and FileLockKiller call
Windows APIs, which compile anywhere and run only on Windows until they have Unix paths of their
own.
-->
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
</PropertyGroup>

<ItemGroup>
<Using Include="DiffEngine" />
<Using Include="PlatformHeads" />
<PackageReference Include="PlatformHeads.Core" />
<PackageReference Include="Serilog" />
<PackageReference Include="Serilog.Sinks.File" />
<PackageReference Include="ProjectDefaults" PrivateAssets="all" />
<ProjectReference Include="..\DiffEngine\DiffEngine.csproj" />
</ItemGroup>
</Project>
File renamed without changes.
9 changes: 9 additions & 0 deletions src/DiffEngineTray.Core/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
global using System.ComponentModel;
global using Microsoft.Win32.SafeHandles;
global using Serilog;
global using System.Text.Json;
global using System.Text.Json.Serialization;
global using System.Diagnostics.CodeAnalysis;
global using System.Net;
global using System.Net.Sockets;
global using System.Reflection;
2 changes: 2 additions & 0 deletions src/DiffEngineTray.Core/InternalsVisibleTo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[assembly: InternalsVisibleTo("DiffEngineTray, PublicKey=00240000048000009400000006020000002400005253413100040000010001000f0a8e4bf1639dce01be6592384e7dfc621915b7759fb5cee42ec5d351bcc43460432da1659ee618ca6cab6b8b8e56a5deb5d4ee1a49783d5c2690752502d31ccbfee9b2c697e20359b55ad100cc9370c8e983fd9496f01d761a060d0435bac7243b1832ba95757aa5adbb67df38c213d717b6751e1217cea9fa5c61e9b799dd")]
[assembly: InternalsVisibleTo("DiffEngineTray.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001000f0a8e4bf1639dce01be6592384e7dfc621915b7759fb5cee42ec5d351bcc43460432da1659ee618ca6cab6b8b8e56a5deb5d4ee1a49783d5c2690752502d31ccbfee9b2c697e20359b55ad100cc9370c8e983fd9496f01d761a060d0435bac7243b1832ba95757aa5adbb67df38c213d717b6751e1217cea9fa5c61e9b799dd")]
Original file line number Diff line number Diff line change
Expand Up @@ -87,19 +87,11 @@ Open an issue on GitHub?
}

/// <summary>
/// Puts the question, and answers whether it was declined. A field so that a test process can
/// decline without anybody being asked: the box is modal, on the desktop of whoever is at the
/// machine, and a test run that reached it waited there for a click.
/// Puts the question, and answers whether it was declined. Declined until a head says how to
/// ask, since asking is a box of a toolkit's own: the Windows head hands over a MessageBox
/// (<c>IssueQuestion</c>) as it starts. A field so that a test process can decline without
/// anybody being asked: the box is modal, on the desktop of whoever is at the machine, and a
/// test run that reached it waited there for a click.
/// </summary>
internal static Func<string, bool> Declined = AskIfOpenIssue;

static bool AskIfOpenIssue(string text)
{
var result = MessageBox.Show(
text,
"DiffEngineTray Error",
MessageBoxButtons.YesNo,
MessageBoxIcon.Error);
return result == DialogResult.No;
}
internal static Func<string, bool> Declined = _ => true;
}
10 changes: 10 additions & 0 deletions src/DiffEngineTray.Core/LockedFilesPolicy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/// <summary>
/// Whether a move whose target is locked kills what holds it without asking: the
/// AlwaysKillLockingProcesses setting, set at startup and from the Options form, and by the
/// question's own "always" box. Apart from the question, which is a WinForms form in the head, so
/// the tracker can ask this of a head that has none.
/// </summary>
static class LockedFilesPolicy
{
public static bool AlwaysKill { get; set; }
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -1115,7 +1115,7 @@ bool ShouldKill(TrackedMove move, LockedFiles locked, AcceptBatch batch)
// as locked with "accept from the tray menu" - where the same accept killed without asking.
if (move.KillLockingProcess ||
batch.KillWithoutPrompt ||
LockedFilesHandler.AlwaysKill)
LockedFilesPolicy.AlwaysKill)
{
return true;
}
Expand Down
3 changes: 2 additions & 1 deletion src/DiffEngineTray.Tests/ProgramEntryPointTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ public class ProgramEntryPointTest
[Test]
public async Task TheEntryPointStartsOnAnStaThread()
{
var entryPoint = typeof(Tracker).Assembly.EntryPoint!;
// The Windows head's own assembly. Tracker is the core's, a library with no entry point
var entryPoint = typeof(MenuBuilder).Assembly.EntryPoint!;

await Assert.That(entryPoint.GetCustomAttribute<STAThreadAttribute>()).IsNotNull();
}
Expand Down
6 changes: 3 additions & 3 deletions src/DiffEngineTray.Tests/TrackerTrackedFilesTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -380,9 +380,9 @@ public TrackerTrackedFilesTest()
[Test]
public async Task AlwaysKillAppliesToAnAcceptArrivingOverTheSocket()
{
var previous = LockedFilesHandler.AlwaysKill;
var previous = LockedFilesPolicy.AlwaysKill;
// The stored preference, which Program loads into this at startup
LockedFilesHandler.AlwaysKill = true;
LockedFilesPolicy.AlwaysKill = true;
try
{
await File.WriteAllTextAsync(temp, "new");
Expand All @@ -409,7 +409,7 @@ public async Task AlwaysKillAppliesToAnAcceptArrivingOverTheSocket()
}
finally
{
LockedFilesHandler.AlwaysKill = previous;
LockedFilesPolicy.AlwaysKill = previous;
}
}
}
2 changes: 2 additions & 0 deletions src/DiffEngineTray/DiffEngineTray.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
<PackageReference Include="Serilog.Sinks.File" />
<PackageReference Include="ProjectDefaults" PrivateAssets="all" />
<ProjectReference Include="..\DiffEngine\DiffEngine.csproj" />
<!-- Everything that is not WinForms, which a head for another platform shares. -->
<ProjectReference Include="..\DiffEngineTray.Core\DiffEngineTray.Core.csproj" />
</ItemGroup>

<!--
Expand Down
17 changes: 17 additions & 0 deletions src/DiffEngineTray/IssueQuestion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/// <summary>
/// How the Windows head asks whether to open an issue for a failure: a modal box, which the core's
/// <see cref="IssueLauncher" /> has no toolkit to show. Handed over in <see cref="Program" />'s
/// Main, before anything can fail.
/// </summary>
static class IssueQuestion
{
public static bool Declined(string text)
{
var result = MessageBox.Show(
text,
"DiffEngineTray Error",
MessageBoxButtons.YesNo,
MessageBoxIcon.Error);
return result == DialogResult.No;
}
}
6 changes: 2 additions & 4 deletions src/DiffEngineTray/LockedFilesHandler.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
static class LockedFilesHandler
{
public static bool AlwaysKill { get; set; }

public static LockedFilesResponse Resolve(TrackedMove move, LockedFiles locked)
{
if (AlwaysKill)
if (LockedFilesPolicy.AlwaysKill)
{
return LockedFilesResponse.Kill;
}
Expand All @@ -14,7 +12,7 @@ public static LockedFilesResponse Resolve(TrackedMove move, LockedFiles locked)

if (form.AlwaysKill)
{
AlwaysKill = true;
LockedFilesPolicy.AlwaysKill = true;
Persist();
}

Expand Down
3 changes: 2 additions & 1 deletion src/DiffEngineTray/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ static class Program
[STAThread]
static void Main()
{
IssueLauncher.Declined = IssueQuestion.Declined;
TrayViewerDirectory.Register();
Logging.Init();
Application.EnableVisualStyles();
Expand Down Expand Up @@ -75,7 +76,7 @@ void Warn(string message) =>
return;
}

LockedFilesHandler.AlwaysKill = settings.AlwaysKillLockingProcesses;
LockedFilesPolicy.AlwaysKill = settings.AlwaysKillLockingProcesses;

// Ownership of the inline queue is decided here, once, by whether the bind succeeds, and
// never transfers. Usually the tray wins, because it starts at login. A viewer that was
Expand Down
2 changes: 1 addition & 1 deletion src/DiffEngineTray/Settings/OptionsFormLauncher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ static async Task<IReadOnlyCollection<string>> Save(KeyRegister keyRegister, Tra
}

await SettingsHelper.Write(settings);
LockedFilesHandler.AlwaysKill = settings.AlwaysKillLockingProcesses;
LockedFilesPolicy.AlwaysKill = settings.AlwaysKillLockingProcesses;
return [];
}

Expand Down
1 change: 1 addition & 0 deletions src/Packaging.Tests/Package.DiffEngineTray.verified.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ nuget.md
package/services/metadata/core-properties/{guid}.psmdcp
tools/net10.0/any/DiffEngine.dll
tools/net10.0/any/DiffEngine.xml
tools/net10.0/any/DiffEngineTray.Core.dll
tools/net10.0/any/DiffEngineTray.deps.json
tools/net10.0/any/DiffEngineTray.dll
tools/net10.0/any/DiffEngineTray.runtimeconfig.json
Expand Down
Loading