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
2 changes: 1 addition & 1 deletion .github/workflows/build-native.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ concurrency:

jobs:
native:
uses: SimonCropp/PlatformHeads/.github/workflows/build-native.yml@0.8.0
uses: SimonCropp/PlatformHeads/.github/workflows/build-native.yml@0.9.0
permissions:
contents: write
pull-requests: write
Expand Down
17 changes: 16 additions & 1 deletion claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,7 @@ the comment there about not caching "nothing staged" asks for.
`native/CMakeLists.txt`, `PlatformHeads`, `PlatformHeads.Core`, `PlatformHeads.Linux` (the
Linux tray's) and `PlatformHeads.WindowsForms` in `Directory.Packages.props`, which move
together, and the `PlatformHeads` package
in `native/swift/Package.swift`, all at 0.8.0. So is the native runtime under both heads' drawing: the C++
in `native/swift/Package.swift`, all at 0.9.0. So is the native runtime under both heads' drawing: the C++
one `deview.cpp` calls as `ph::` (the window, GLFW's input as it happened, which frames are
built and drawn, the renderer, the fonts and fontconfig, and the label helpers) and the Swift
`PlatformHeadsMac` the AppKit head imports (`HeadWindow`, `EventPump`, `TerminateGuard`,
Expand Down Expand Up @@ -772,6 +772,21 @@ the comment there about not caching "nothing staged" asks for.
and macOS, from a source of those alone, checks the viewer and documents folder are where the
lookups expect and (on Windows) that the command is a GUI apphost, then starts it and waits for
it to listen on 3492. `publish` waits for it
- Update is PlatformHeads' `ToolUpdate` on every head, through the core's `TrayUpdate`, which
finds the shim from where the tray runs (`ToolUpdate.ShimFor`, null for a build's output) and
keeps the outcome beside the settings, out of the store the update replaces. A shell waits for
the tray to exit, runs `dotnet tool update DiffEngineTray --tool-path {the shim's folder}
--prerelease` with the dotnet the tray runs on, and starts the shim; the tray it starts says how
that went once (`TrayUpdate.Report`), after the single instance lock, as a balloon on Windows
and a desktop notification elsewhere. The Windows head keeps it on the options window's button,
which says so when there is no installed tool rather than going grey, since the form's pictures
are snapshot tested; the others put it on the menu (`TrayActions.Update`). It was a Windows
PowerShell line that did not wait for the tray and updated the global install whichever tray
asked. On Windows the shell stops what runs from the store first, which includes a viewer the
tray started: one that owns the queue is ended without staging it. Seen in a `dotnet/sdk:10.0`
container on a private `dbus-daemon` with a stand-in watcher: Update chosen from the menu, the
tray gone, the install moved from one version to the next, and the new tray registered,
listening and logging what the update said
- The core's tests are `DiffEngineTray.Core.Tests` (net10.0), which runs in the unix CI job as
well as on Windows. What the menu says and does is `TrayMenuTests` there, as text.
`DiffEngineTray.Tests` keeps what needs the head: how the menu is drawn, the forms, the
Expand Down
2 changes: 1 addition & 1 deletion native/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ include(FetchContent)
FetchContent_Declare(
platformheads
GIT_REPOSITORY https://github.com/SimonCropp/PlatformHeads.git
GIT_TAG 0.8.0
GIT_TAG 0.9.0
GIT_SHALLOW ON)
FetchContent_MakeAvailable(platformheads)
include(${platformheads_SOURCE_DIR}/cmake/PlatformHeads.cmake)
Expand Down
2 changes: 1 addition & 1 deletion native/swift/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ let package = Package(
dependencies: [
// What every AppKit head does underneath its drawing, shared with BuildMonitor. The sixth
// pin of PlatformHeads, moved with the other five.
.package(url: "https://github.com/SimonCropp/PlatformHeads", exact: "0.8.0")
.package(url: "https://github.com/SimonCropp/PlatformHeads", exact: "0.9.0")
],
targets: [
// Exists only to import the ABI. Swift does not guarantee struct layout, so the structs
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
Exit [glyph: exit]
Run at startup [unchecked]
Update
Debug view [glyph: options]
Open logs [glyph: folder]
Raise issue [glyph: link]
22 changes: 19 additions & 3 deletions src/DiffEngineTray.Core.Tests/TrayMenuTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -351,13 +351,28 @@ static void Render(StringBuilder builder, TrayMenu menu, IReadOnlyList<TrayMenuI
}

/// <summary>
/// A head with no options window and no folder picker, as on Linux: run at login is on the
/// menu, checked as the desktop says, and there is no purge.
/// A head with no options window and no folder picker, as on Linux: run at login and the
/// update are on the menu, run at login checked as the desktop says, and there is no purge.
/// </summary>
[Test]
public async Task FixedForAHeadWithNoWindows() =>
await Verify(Render(TrayMenu.Fixed(head.Windowless)));

/// <summary>
/// A tray that is not an installed tool has no update to offer, which is what the head is told
/// by being handed none.
/// </summary>
[Test]
public async Task UpdateIsOnlyOnTheMenuWhenThereIsOne()
{
var menu = TrayMenu.Fixed(head.Windowless);
await Assert.That(menu.Invoke(Find(menu, "Update").Id)).IsTrue();
await Assert.That(head.Calls).IsEquivalentTo(["Update"]);

var withNone = TrayMenu.Fixed(head.Windowless with {Update = null});
await Assert.That(withNone.Items.Any(_ => _.Label == "Update")).IsFalse();
}

/// <summary>
/// What the desktop has, read again for each menu, rather than a setting that can say
/// otherwise.
Expand Down Expand Up @@ -397,7 +412,8 @@ sealed class Head
DebugView: () => Calls.Add("DebugView"),
Purge: null,
OpenDiffTool: Opened.Add,
RunAtLogin: Login);
RunAtLogin: Login,
Update: () => Calls.Add("Update"));
}

sealed class StandInLogin : IRunAtLogin
Expand Down
85 changes: 85 additions & 0 deletions src/DiffEngineTray.Core.Tests/TrayUpdateTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/// <summary>
/// The tray's side of PlatformHeads' update: which install it updates, and what it says after.
/// How the update runs is PlatformHeads' and tested there.
/// </summary>
public class TrayUpdateTests
{
/// <summary>
/// A tray installed as a RID specific tool runs from its RID's package in the store, and the
/// command is beside the store, whether it went in with --global or --tool-path.
/// </summary>
[Test]
public async Task AnInstalledTrayUpdatesTheToolPathItIsIn()
{
var tools = Path.Combine(Path.GetTempPath(), "home", ".dotnet", "tools");
var directory = Path.Combine(tools, ".store", "diffenginetray", "21.0.0", "diffenginetray.linux-x64", "21.0.0", "tools", "net10.0", "linux-x64");

var update = TrayUpdate.Find(directory)!;

await Assert.That(update.PackageId).IsEqualTo("DiffEngineTray");
await Assert.That(update.ToolPath).IsEqualTo(tools);
await Assert.That(update.Shim).IsEqualTo(Path.Combine(tools, OperatingSystem.IsWindows() ? "DiffEngineTray.exe" : "DiffEngineTray"));
await Assert.That(update.Outcome).IsEqualTo(TrayUpdate.OutcomePath);
}

/// <summary>
/// A build's output, as every test run is, has no tool to update.
/// </summary>
[Test]
public async Task ABuildHasNothingToUpdate()
{
await Assert.That(TrayUpdate.Find()).IsNull();
// Asked as a bool: handed an Action, That asserts on running it
await Assert.That(TrayUpdate.MenuAction(() => { }) is null).IsTrue();
}

[Test]
public async Task AnUpdateThatWorkedSaysWhatIsRunningNow()
{
using var file = new TempOutcome("ok\nTool 'diffenginetray' was successfully updated from version '21.0.0' to version '21.1.0'.\n");

var report = TrayUpdate.Report(file.Path)!;

await Assert.That(report.Title).IsEqualTo("DiffEngineTray updated");
await Assert.That(report.Message).IsEqualTo($"Now running {VersionReader.VersionString}.");
await Assert.That(report.Kind).IsEqualTo(TrayNotificationKind.Info);
// Once: the next start is not another update
await Assert.That(TrayUpdate.Report(file.Path)).IsNull();
}

[Test]
public async Task AnUpdateThatFailedSaysWhy()
{
using var file = new TempOutcome("failed\nTool 'diffenginetray' failed to update due to the following:\nThe network is down.\n");

var report = TrayUpdate.Report(file.Path)!;

await Assert.That(report.Title).IsEqualTo("DiffEngineTray did not update");
await Assert.That(report.Message).IsEqualTo("The network is down.");
await Assert.That(report.Kind).IsEqualTo(TrayNotificationKind.Error);
}

[Test]
public async Task NoUpdateNothingToSay()
{
using var file = new TempOutcome(null);

await Assert.That(TrayUpdate.Report(file.Path)).IsNull();
}

sealed class TempOutcome : IDisposable
{
public string Path { get; } = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"TrayUpdateTests.{Guid.NewGuid():N}.log");

public TempOutcome(string? text)
{
if (text is not null)
{
File.WriteAllText(Path, text);
}
}

public void Dispose() =>
File.Delete(Path);
}
}
8 changes: 7 additions & 1 deletion src/DiffEngineTray.Core/TrayActions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,16 @@
/// option that does anything off Windows: the hot keys are Windows', and target on left and the
/// launch limit are user environment variables, which .NET sets only there.
/// </param>
/// <param name="Update">
/// For a head with no options window, which holds the button on Windows, the update as an item on
/// the menu (<see cref="TrayUpdate.MenuAction" />). Null there too where the tray is not an
/// installed tool.
/// </param>
record TrayActions(
Action Exit,
Action? Options,
Action DebugView,
Action? Purge,
Action<TrackedMove> OpenDiffTool,
IRunAtLogin? RunAtLogin = null);
IRunAtLogin? RunAtLogin = null,
Action? Update = null);
5 changes: 5 additions & 0 deletions src/DiffEngineTray.Core/TrayMenu.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ public static TrayMenu Fixed(TrayActions head)
menu.items.Add(menu.Toggle("startup", "Run at startup", runAtLogin.Exists(), () => runAtLogin.Set(!runAtLogin.Exists())));
}

if (head.Update is { } update)
{
menu.items.Add(menu.Button("update", "Update", update));
}

menu.items.Add(menu.Button("debug", "Debug view", head.DebugView, "options"));
menu.items.Add(menu.Button("logs", "Open logs", Logging.OpenDirectory, "folder"));
if (head.Purge is { } purge)
Expand Down
108 changes: 108 additions & 0 deletions src/DiffEngineTray.Core/TrayUpdate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/// <summary>
/// The tray's Update, on every head: PlatformHeads' <see cref="ToolUpdate" /> for the DiffEngineTray
/// this tray was installed as. It waits for this tray to exit, runs <c>dotnet tool update</c> by the
/// tool path the tray is in, and starts it again. It was a Windows PowerShell line, on Windows only,
/// which did not wait for the tray to go and updated the global install whichever tray asked.
/// </summary>
static class TrayUpdate
{
const string package = "DiffEngineTray";

/// <summary>
/// Beside the settings rather than the logs, which are in the store the update replaces.
/// </summary>
public static string OutcomePath { get; } = Path.Combine(Path.GetDirectoryName(SettingsHelper.FilePath)!, "update-outcome.log");

/// <summary>
/// For this tray, or null when it is not an installed tool, as a build's output is not, which
/// has nothing to update.
/// </summary>
public static ToolUpdate? Find() =>
Find(AssemblyLocation.CurrentDirectory);

internal static ToolUpdate? Find(string directory)
{
var shim = ToolUpdate.ShimFor(directory, package);
if (shim is null)
{
return null;
}

return new(package, shim, OutcomePath);
}

/// <summary>
/// The menu's Update, for a head with no options window to hold the button, or null where
/// there is nothing to update.
/// </summary>
public static Action? MenuAction(Action exit)
{
var update = Find();
if (update is null)
{
return null;
}

return () => Start(update, exit);
}

/// <summary>
/// Starts the update, then exits the tray, which the update waits for. A shell that would not
/// start leaves the tray running, since nothing would start it again.
/// </summary>
public static void Start(ToolUpdate update, Action exit)
{
try
{
Log.Information("Updating {Shim} by {Dotnet}", update.Shim, update.Dotnet);
update.Start();
}
catch (Exception exception)
{
ExceptionHandler.Handle("Failed to start the update", exception);
return;
}

exit();
}

/// <summary>
/// What to say about the update that started this tray, or null where none did, which is every
/// start but the one after an update. Said once, since reading it removes it, and only by a
/// tray that got past the single instance lock, so one started beside it and leaving at once
/// does not take it away. The whole of what the update said goes to the log.
/// </summary>
public static TrayNotification? Report() =>
Report(OutcomePath);

internal static TrayNotification? Report(string path)
{
ToolUpdateOutcome? outcome;
try
{
outcome = ToolUpdateOutcome.Take(path);
}
catch (Exception exception)
when (exception is IOException or UnauthorizedAccessException)
{
Log.Warning(exception, "Could not read how the update went, at {Path}", path);
return null;
}

if (outcome is null)
{
return null;
}

if (outcome.Succeeded)
{
Log.Information("Updated. dotnet tool update said:\n{Output}", outcome.Output);
// The version rather than what the tool said: this is the tray the update started, so
// it is the installed one, whether the update moved it or found it already current
return new("DiffEngineTray updated", $"Now running {VersionReader.VersionString}.", Kind: TrayNotificationKind.Info);
}

Log.Warning("The update failed, so {Version} is still the one installed. dotnet tool update said:\n{Output}", VersionReader.VersionString, outcome.Output);
return new("DiffEngineTray did not update", outcome.Reason);
}
}
8 changes: 7 additions & 1 deletion src/DiffEngineTray.Linux/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ static async Task Inner()
Warn($"There is nowhere to show DiffEngineTray's menu, so it is holding the snapshot queue and tracking what test runs send it without one. {trayError}");
}

if (TrayUpdate.Report() is { } report)
{
DesktopNotification.Show("DiffEngineTray", report);
}

// Ownership of the inline queue is decided here, once, by whether the bind succeeds, and
// never transfers. See the Windows head's Program
await using var owned = OwnedInlineHost.TryOwn(Warn);
Expand Down Expand Up @@ -100,7 +105,8 @@ static async Task Inner()
DebugView: () => DebugReportFile.Open(tracker),
Purge: null,
OpenDiffTool: DiffToolLauncher.Launch,
RunAtLogin: RunAtLogin.ForPlatform(TrayLogin.Item));
RunAtLogin: RunAtLogin.ForPlatform(TrayLogin.Item),
Update: TrayUpdate.MenuAction(tokenSource.Cancel));
try
{
await TrayLoop.Run(tray, tracker, actions, () => icon, cancel);
Expand Down
8 changes: 7 additions & 1 deletion src/DiffEngineTray.Mac/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ static async Task Inner()
Warn($"DiffEngineTray could not put its icon in the menu bar, so it is holding the snapshot queue and tracking what test runs send it without one. {trayError}");
}

if (TrayUpdate.Report() is { } report)
{
DesktopNotification.Show("DiffEngineTray", report);
}

// Ownership of the inline queue is decided here, once, by whether the bind succeeds, and
// never transfers. See the Windows head's Program
await using var owned = OwnedInlineHost.TryOwn(Warn);
Expand Down Expand Up @@ -104,7 +109,8 @@ static async Task Inner()
DebugView: () => DebugReportFile.Open(tracker),
Purge: null,
OpenDiffTool: DiffToolLauncher.Launch,
RunAtLogin: RunAtLogin.ForPlatform(TrayLogin.Item));
RunAtLogin: RunAtLogin.ForPlatform(TrayLogin.Item),
Update: TrayUpdate.MenuAction(tokenSource.Cancel));
try
{
if (tray is null)
Expand Down
6 changes: 6 additions & 0 deletions src/DiffEngineTray/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ static async Task Inner()
void Warn(string message) =>
icon.ShowBalloonTip(10000, "DiffEngineTray", message, ToolTipIcon.Warning);

if (TrayUpdate.Report() is { } report)
{
var kind = report.Kind == TrayNotificationKind.Info ? ToolTipIcon.Info : ToolTipIcon.Warning;
icon.ShowBalloonTip(10000, report.Title, report.Message, kind);
}

var settings = GetSettings();
if (settings == null)
{
Expand Down
Loading
Loading