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.12.0
uses: SimonCropp/PlatformHeads/.github/workflows/build-native.yml@0.13.0
permissions:
contents: write
pull-requests: write
Expand Down
21 changes: 14 additions & 7 deletions 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.12.0. So is the native runtime under both heads' drawing: the C++
in `native/swift/Package.swift`, all at 0.13.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 @@ -655,12 +655,16 @@ the comment there about not caching "nothing staged" asks for.
minimised on Windows, through `ViewerInput.Unseen`). `ViewerProgram.Loop` keeps its own `hidden`
and the head's `unseen` apart and slows `OwnerLink`, `TrackedWatch` and `DocumentWatch` on either.
Version 13 is `deview_on_display_lost`, which the Linux shim hands to PlatformHeads'
`ph::OnDisplayLost`, and the Swift one exports and never calls.
`ph::OnDisplayLost`, and the Swift one hands to PlatformHeadsMac's `TerminateGuard` as its
`sessionEnding`, with no bump, since the managed side already called it on every platform.
- A logoff stages an owned queue on every platform, as a clean exit does, since none reaches the
loop's own shutdown. `ViewerProgram.SessionEnding` does it, once, touching no window, and is
heard three ways: on Windows through `HeadForm.SessionEnding`, which saves the placement after
heard four ways: on Windows through `HeadForm.SessionEnding`, which saves the placement after
it; on Linux as the X server goes, through `deview_on_display_lost`, which `NativeViewerWindow`
takes as `ILoopHooks.SessionEnding` and from inside which Xlib then calls `exit(1)`; and through
takes as `ILoopHooks.SessionEnding` and from inside which Xlib then calls `exit(1)`; on macOS
through the same callback, at a logout, restart or shutdown, from inside the present that pumped
AppKit's quit, after which AppKit ends the process: refused, as it was, the quit cancelled the
logout, and a quit from the Dock still is, reported as a close; and through
SIGTERM and SIGHUP, which `ViewerProgram` holds PlatformHeads.Core's `SessionEndSignals` for,
since .NET ends the process on either with no `finally` run. The signals do not save the
placement, because they come on a thread of their own. A display lost inside a mutation cannot
Expand Down Expand Up @@ -741,7 +745,7 @@ the comment there about not caching "nothing staged" asks for.
native library of its own, `diffengine_tray` (decision G), so a tray change never moves the
viewer's ABI version. It is a second product of `native/swift`, built by the same `build-native`
run, which collects it into the head's `runtimes` through PlatformHeads' `mac-libraries`, and
committed as the viewer's is. The ABI is `native/include/detray.h`, `DETRAY_VERSION` 1: the
committed as the viewer's is. The ABI is `native/include/detray.h`, `DETRAY_VERSION` 2: the
menu goes over as one array of `DetrayItem`s, each with its parent by index, since a C struct
cannot hold a list of itself, and the Swift side (`Sources/Detray`) makes it a tree again. Its
managed half is in the core (`Detray`, `MacTray`), as ProcessEx's native calls are, so the
Expand All @@ -751,8 +755,11 @@ the comment there about not caching "nothing staged" asks for.
AppKit is driven from the main thread only, so
the loop is `TrayLoop.RunPumped`, on the thread `Main` starts on, with the library's pump as
its wait; nothing in `Program.Inner` awaits before the loop returns. The app is an accessory,
with no Dock icon, App Nap is off (`NapGuard`), and a logout is `TerminateGuard`'s, which
refuses the quit and reports it, for the loop to end as Exit does. "Run at startup" is a
with no Dock icon, App Nap is off (`NapGuard`), and a quit is `TerminateGuard`'s. A logout,
restart or shutdown runs `TraySessionEnd.For` from inside the pump (`detray_on_session_end`,
`MacTray.SessionEnding`, set before `detray_init` makes the guard) and is let through, since
refusing it cancelled the logout; any other quit is refused and reported (`detray_quitting`),
for the loop to end as Exit does. "Run at startup" is a
launch agent. Packed as `DiffEngineTray.osx-x64` and `.osx-arm64`
- The tray ships as .NET 10 RID specific tool packages (decision F), by
`src/DiffEngineTray.Core/TrayHead.targets`, which every head imports after naming its RIDs in
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.12.0
GIT_TAG 0.13.0
GIT_SHALLOW ON)
FetchContent_MakeAvailable(platformheads)
include(${platformheads_SOURCE_DIR}/cmake/PlatformHeads.cmake)
Expand Down
26 changes: 23 additions & 3 deletions native/include/detray.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* A library of its own beside the viewer's (diffengine_viewer), built from the same Swift package,
* so a change to either never moves the other's version.
*
* Followed by native/swift/Sources/Detray and src/DiffEngineTray.Mac/Detray.cs. Bump
* Followed by native/swift/Sources/Detray and src/DiffEngineTray.Core/Detray.cs. Bump
* DETRAY_VERSION whenever a struct changes or a field changes meaning: the managed side refuses a
* library whose version is not the one it was built for.
*
Expand All @@ -20,7 +20,15 @@
extern "C" {
#endif

#define DETRAY_VERSION 1
/*
* 1: the status item and its menu.
* 2: detray_on_session_end, and a logout, restart or shutdown no longer reported by
* detray_quitting. A library from before cancels the logout that asked it to quit.
*/
#define DETRAY_VERSION 2

/* What detray_on_session_end calls. */
typedef void (*DetraySessionEnd)(void);

/* PlatformHeads' TrayMenuFlags and PlatformHeadsMac's StatusMenuFlags, which write and read them. */
#define DETRAY_ENABLED 1
Expand Down Expand Up @@ -70,9 +78,21 @@ DETRAY_API void detray_pump(void);
*/
DETRAY_API int32_t detray_poll(char* buffer, int32_t capacity);

/* 1 once AppKit has been asked to quit, which for an app with no Dock icon is a logout. */
/*
* 1 once AppKit has been asked to quit other than by the session ending: with no Dock icon, by
* something that sent the app a quit event, such as AppleScript's quit. Refused, for the managed
* side to end its loop the way Exit does.
*/
DETRAY_API int32_t detray_quitting(void);

/*
* Has ended called at a logout, restart or shutdown, from inside the detray_pump that dispatched
* AppKit's quit, on the main thread. AppKit ends the process as it returns, so it is the one
* chance to stage the queue: refused, the quit would cancel the logout. Read by detray_init, so
* set before it; with none, a session's end is refused and reported by detray_quitting.
*/
DETRAY_API void detray_on_session_end(DetraySessionEnd ended);

/*
* The menu as the last apply left it, a line an item, as text, written to buffer with its whole
* length returned. For tests, which can make no status item: neither this nor detray_apply needs
Expand Down
10 changes: 7 additions & 3 deletions native/include/deview.h
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,10 @@ typedef struct DeviewPlacement {
* they shipped together. And panX and panY are kept inside what either pane can show, where
* they were kept inside what the dragged one can.
* 13: deview_on_display_lost. No struct moved: a new entry point, which a library from before
* does not have, and which the managed side calls on every native window.
* does not have, and which the managed side calls on every native window. The macOS library
* came to call it at a logout, restart or shutdown with no bump: the managed side already
* handed it the same session end on every platform, and either side with the other of a
* different age does what it did before.
*/
#define DEVIEW_VERSION 13

Expand Down Expand Up @@ -520,8 +523,9 @@ DEVIEW_API void deview_shutdown(void);
* Has lost called once as the connection to the X server is lost, which is how a logoff ends the
* Linux viewer: Xlib ends the process with exit(1) as it returns, so it is the one chance to stage
* an owned queue. Called on the thread inside deview_present or deview_poll_input, from inside that
* call. NULL stops it. macOS never calls it: a logout's quit there is the app's own, which reaches
* the clean exit.
* call. NULL stops it. macOS calls it at a logout, restart or shutdown, from inside the
* deview_present that pumped AppKit's quit, and AppKit ends the process as it returns: refused,
* the quit would cancel the logout. A quit from the Dock is still reported as a close.
*/
DEVIEW_API void deview_on_display_lost(DeviewDisplayLost lost);

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.12.0")
.package(url: "https://github.com/SimonCropp/PlatformHeads", exact: "0.13.0")
],
targets: [
// Exists only to import the ABI. Swift does not guarantee struct layout, so the structs
Expand Down
5 changes: 5 additions & 0 deletions native/swift/Sources/Detray/Exports.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ public func detrayQuitting() -> Int32 {
Tray.shared.quitting ? 1 : 0
}

@_cdecl("detray_on_session_end")
public func detrayOnSessionEnd(_ ended: DetraySessionEnd?) {
Tray.shared.sessionEnded = ended
}

@_cdecl("detray_describe")
public func detrayDescribe(_ buffer: UnsafeMutablePointer<CChar>?, _ capacity: Int32) -> Int32 {
Utf8Buffer.write(StatusMenuItem.describe(Tray.shared.item.items), buffer, capacity)
Expand Down
25 changes: 19 additions & 6 deletions native/swift/Sources/Detray/Tray.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import AppKit
import CDetray
import PlatformHeadsMac

/// The status item, and what has come of it since the managed side last asked: the items chosen,
Expand All @@ -11,11 +12,17 @@ final class Tray {
private var chosen: [String] = []
private(set) var quitting = false

/// A logout, for an app with no Dock icon to quit from, turned into the managed side's own
/// quit, which stages the queue on its way out. Held here: AppKit keeps its delegate weakly.
private lazy var terminateGuard = TerminateGuard { [unowned self] in
self.quitting = true
}
/// What `detray_on_session_end` was handed: the managed side's session end, which stages the
/// queue it owns and removes the version marker.
var sessionEnded: DetraySessionEnd?

/// A quit sent to the app, which has no Dock icon to quit from, turned into the managed side's
/// own quit, which stages the queue on its way out. A logout, restart or shutdown runs
/// `sessionEnded` there and then instead, and AppKit ends the process once it returns. Made in
/// `start`, from whatever `sessionEnded` is by then: with none, a session's end is refused as a
/// quit is, which cancels the logout but loses nothing. Held here: AppKit keeps its delegate
/// weakly.
private var terminateGuard: TerminateGuard?

private init() {
item.chose = { [unowned self] entry in
Expand All @@ -31,7 +38,13 @@ final class Tray {
func start() -> Bool {
let application = NSApplication.shared
application.setActivationPolicy(.accessory)
application.delegate = terminateGuard
let terminate = TerminateGuard(
{ [unowned self] in
self.quitting = true
},
sessionEnding: sessionEnded.map { ended -> () -> Void in { ended() } })
terminateGuard = terminate
application.delegate = terminate
application.finishLaunching()
napGuard.begin(reason: "Tracking what test runs leave pending")
return item.start()
Expand Down
5 changes: 3 additions & 2 deletions native/swift/Sources/Deview/Exports.swift
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,11 @@ public func deviewShutdown() {
}
}

/// Never called here: a logout's quit is the app's own (TerminateGuard), which reaches the clean
/// exit. Only the Linux shim can lose its display out from under it.
/// Here there is no display to lose: the session's end is a quit from AppKit, at a logout, restart
/// or shutdown, which TerminateGuard hands to `lost` from inside the pump and then lets through.
@_cdecl("deview_on_display_lost")
public func deviewOnDisplayLost(_ lost: DeviewDisplayLost?) {
Runtime.shared.sessionEnding = lost
}

/// Renders into a bitmap of this side's own making rather than asking the view for one, with
Expand Down
18 changes: 14 additions & 4 deletions native/swift/Sources/Deview/Runtime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,24 @@ final class Runtime {

private var delegate: WindowDelegate?

/// A quit from the Dock or a logout, reported as a close, which the managed side answers the
/// way it answers the window's close button: it leaves the loop, persists the queue it owns,
/// and exits. macOS has no tray to hold the queue instead. Held here because
/// A quit from the Dock, reported as a close, which the managed side answers the way it answers
/// the window's close button: it leaves the loop, persists the queue it owns, and exits. macOS
/// has no tray to hold the queue instead.
///
/// A logout, restart or shutdown runs `sessionEnding` instead, there and then, and AppKit ends
/// the process once it returns: refused, the quit would cancel the logout. Held here because
/// `NSApplication.delegate` does not keep it alive.
private lazy var terminateGuard = TerminateGuard { [unowned self] in
private lazy var terminateGuard = TerminateGuard({ [unowned self] in
self.input.closeRequested = 1
}) { [unowned self] in
self.sessionEnding?()
}

/// What `deview_on_display_lost` was handed: the managed side's session end, which stages the
/// queue it owns and saves the placement. It hands one over on every window, before the first
/// present, so before any event that could be a logout is dispatched.
var sessionEnding: DeviewDisplayLost?

private let napGuard = NapGuard()
private var size = CGSize(width: 1100, height: 700)
private var title = "DiffEngineViewer"
Expand Down
5 changes: 4 additions & 1 deletion src/DiffEngineTray.Core/Detray.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ static class Detray
/// Must match DETRAY_VERSION in native/include/detray.h, so a stale library is reported rather
/// than read as garbage.
/// </summary>
public const int ExpectedVersion = 1;
public const int ExpectedVersion = 2;

/// <summary>
/// Has PlatformHeads' resolver find <see cref="Library" /> under runtimes/{rid}/native for these
Expand Down Expand Up @@ -44,6 +44,9 @@ public static void Resolve() =>
[DllImport(Library, EntryPoint = "detray_quitting")]
public static extern int Quitting();

[DllImport(Library, EntryPoint = "detray_on_session_end")]
public static extern unsafe void OnSessionEnd(delegate* unmanaged<void> ended);

[DllImport(Library, EntryPoint = "detray_describe")]
public static extern int Describe(byte[] buffer, int capacity);

Expand Down
2 changes: 2 additions & 0 deletions src/DiffEngineTray.Core/DiffEngineTray.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
-->
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<!-- For MacTray's session end, which the macOS library calls back through a function pointer -->
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<IsPackable>false</IsPackable>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
</PropertyGroup>
Expand Down
36 changes: 33 additions & 3 deletions src/DiffEngineTray.Core/MacTray.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,42 @@ sealed class MacTray : ITray<TrayIcon>
// An id is a path or a snapshot's key with a word either side, which this holds with room over
readonly byte[] chosen = new byte[64 * 1024];

/// <summary>
/// What a logout, restart or shutdown runs: <see cref="TraySessionEnd.For" />, which stages the
/// queue and removes the version marker. Called from inside <see cref="Pump" />, on the thread
/// the loop runs on, and AppKit ends the process as it returns, so nothing after the pump runs.
/// Static, since the library calls back through a plain function pointer, which carries no
/// instance to find it on, and settable after <see cref="Open" />, since what it stages is
/// made after the tray.
/// </summary>
public static Action? SessionEnding { get; set; }

MacTray()
{
}

/// <summary>
/// Nothing may be thrown back into the library, which is inside AppKit's quit.
/// </summary>
[UnmanagedCallersOnly]
static void SessionEnded()
{
try
{
SessionEnding?.Invoke();
}
catch (Exception exception)
{
Log.Error(exception, "Failed while the session was ending");
}
}

/// <summary>
/// The item in the status bar, with every icon and glyph handed over. Null with an
/// <paramref name="error" /> when there is no library for this machine, one of another version,
/// or no status bar to put the item in.
/// </summary>
public static MacTray? Open(out string? error)
public static unsafe MacTray? Open(out string? error)
{
try
{
Expand All @@ -32,6 +58,8 @@ sealed class MacTray : ITray<TrayIcon>
return null;
}

// Before Init, which reads it: with none, a logout is refused and cancelled
Detray.OnSessionEnd(&SessionEnded);
if (Detray.Init() == 0)
{
error = "AppKit gave the status item no button to draw in.";
Expand Down Expand Up @@ -127,8 +155,10 @@ public static void Pump() =>
Detray.Pump();

/// <summary>
/// AppKit was asked to quit. With no Dock icon, that is a logout, which the library turned
/// into this, for the loop to end the way Exit does, staging the queue on its way out.
/// AppKit was asked to quit other than by the session ending. With no Dock icon, that is a quit
/// event sent to the app, such as AppleScript's, which the library refused and turned into
/// this, for the loop to end the way Exit does, staging the queue on its way out. A session's
/// end is <see cref="SessionEnding" />.
/// </summary>
public static bool Quitting =>
Detray.Quitting() != 0;
Expand Down
11 changes: 7 additions & 4 deletions src/DiffEngineTray.Mac/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,14 @@ static async Task Inner()
owned.Start();
}

// A logout reaches the loop as a quit (MacTray.Quitting), and ends it the way Exit does.
// These are a session's processes being ended otherwise, which .NET answers by ending the
// process with nothing below run
// A logout, restart or shutdown is heard inside the pump (MacTray.SessionEnding), and AppKit
// ends the process as it returns, since refusing the quit would cancel the logout. The
// signals are a session's processes being ended otherwise, which .NET answers by ending
// the process with nothing below run
var sessionEnding = TraySessionEnd.For(owned, TrayVersionFile.Delete);
MacTray.SessionEnding = sessionEnding;
using var sessionEnd = new SessionEndSignals(
TraySessionEnd.For(owned, TrayVersionFile.Delete),
sessionEnding,
_ => Log.Error(_, "Failed while the session was ending"));

var listener = PiperServer.TryBind(out var bindError);
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading
Loading