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
38 changes: 38 additions & 0 deletions .github/scripts/send-quit.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Sends a process the quit Apple event macOS sends it, and prints the error number its reply
// carries, 0 for none. Used by the tray-install job on macOS, which has no logout to give the
// tray: `send-quit <pid>` is a quit from the Dock or AppleScript, and `send-quit <pid> logout` is
// the one a logout sends, the quit with kAEQuitReason set to kAELogOut.
//
// A quit the app refuses is answered userCanceledErr (-128), which is how a logout learns it was
// refused, and cancelled. A quit the app lets through ends it before it replies, so the send
// fails on a connection that has gone instead, which is printed as its error number too.

import Foundation

let arguments = CommandLine.arguments
guard arguments.count >= 2, let pid = Int32(arguments[1]) else {
FileHandle.standardError.write("usage: send-quit <pid> [logout]\n".data(using: .utf8)!)
exit(2)
}

let event = NSAppleEventDescriptor(
eventClass: AEEventClass(kCoreEventClass),
eventID: AEEventID(kAEQuitApplication),
targetDescriptor: NSAppleEventDescriptor(processIdentifier: pid),
returnID: AEReturnID(kAutoGenerateReturnID),
transactionID: AETransactionID(kAnyTransactionID))

// An attribute of the event, not a parameter, which is where loginwindow puts it and where
// TerminateGuard reads it from.
if arguments.count >= 3, arguments[2] == "logout" {
event.setAttribute(
NSAppleEventDescriptor(enumCode: OSType(kAELogOut)),
forKeyword: AEKeyword(kAEQuitReason))
}

do {
let reply = try event.sendEvent(options: [.waitForReply], timeout: 10)
print(reply.paramDescriptor(forKeyword: AEKeyword(keyErrorNumber))?.int32Value ?? 0)
} catch let error as NSError {
print(error.code)
}
74 changes: 73 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,8 @@ jobs:
# The packaging tests read them; only `dotnet tool install` choosing between them shows the
# right one is found for a machine, and only starting what it laid out shows that it runs. So
# this installs the tray from the windows job's packages, on each platform, as someone would
# from nuget.org, and waits for the installed tray to listen on the piper port.
# from nuget.org, and waits for the installed tray to listen on the piper port. On macOS it then
# quits the tray both ways macOS does, a logout's quit among them.
#
# From a source of those packages alone. Until a release, their version is the one in
# Directory.Build.props, which nuget.org already has as an older tray, and an install that could
Expand Down Expand Up @@ -437,6 +438,77 @@ jobs:
cat "$tool/logs/log.txt" || true
kill "$pid" || true
[ -n "$listening" ] || { echo "::error::The installed tray did not listen on 3492"; exit 1; }
wait "$pid" || true
echo "tray=$tray" >> "$GITHUB_ENV"
echo "tool=$tool" >> "$GITHUB_ENV"

# The quits macOS sends, by Apple event as it sends them: an ordinary one, which the tray
# refuses and turns into its own exit, and a logout's, which it has to let through or the
# logout is cancelled. CI cannot log out, so the logout's is sent by hand, with the reason a
# logout gives it. Either way the tray exits and takes its version marker with it; after a
# logout's, only the session end it ran can have done that, since AppKit ends the process
# without the loop's own way out.
- name: Quit the tray as macOS does
if: runner.os == 'macOS'
shell: bash
run: |
xcrun swiftc -O -o "$RUNNER_TEMP/send-quit" .github/scripts/send-quit.swift
marker="${TMPDIR%/}/DiffEngineTray/version.txt"

quit() {
# Listening by its own process id, since that is what the events are sent to
"$tray/DiffEngineTray" &
pid=$!
listening=
for i in $(seq 1 30); do
if lsof -nP -a -p "$pid" -iTCP:3492 -sTCP:LISTEN > /dev/null 2>&1; then
listening=1
break
fi
sleep 1
done
[ -n "$listening" ] || { cat "$tool/logs/log.txt" || true; echo "::error::The tray did not listen on 3492"; exit 1; }
if grep -qs "No status item" "$tool/logs/log.txt"; then
cat "$tool/logs/log.txt" || true
echo "::error::The tray has no status item, so nothing pumps the Apple events sent to it"
exit 1
fi
test -f "$marker" || { echo "::error::The tray did not write $marker"; exit 1; }

code=$("$RUNNER_TEMP/send-quit" "$pid" $1)
echo "A ${1:-plain} quit was answered $code"

exited=
for i in $(seq 1 20); do
state=$(ps -o stat= -p "$pid" | tr -d ' ' || true)
if [ -z "$state" ] || [[ "$state" == Z* ]]; then
exited=1
break
fi
sleep 0.5
done
if [ -z "$exited" ]; then
kill "$pid" || true
cat "$tool/logs/log.txt" || true
echo "::error::The tray was still running ten seconds after a ${1:-plain} quit"
exit 1
fi
wait "$pid" && status=0 || status=$?
[ "$status" = 0 ] || { cat "$tool/logs/log.txt" || true; echo "::error::The tray exited $status after a ${1:-plain} quit"; exit 1; }
! test -f "$marker" || { echo "::error::The tray left $marker behind after a ${1:-plain} quit"; exit 1; }
}

quit
[ "$code" = -128 ] || { echo "::error::A plain quit was answered $code, where the tray refuses it (-128) and exits by its own loop"; exit 1; }

quit logout
[ "$code" != -128 ] || { echo "::error::A logout's quit was refused (-128), which cancels the logout"; exit 1; }

cat "$tool/logs/log.txt" || true
if grep -qs "Failed while the session was ending" "$tool/logs/log.txt"; then
echo "::error::The session end failed"
exit 1
fi

# On Windows the command has to be a GUI apphost as well: a Run key naming a console one, or
# a .cmd, opens a console at every login.
Expand Down
7 changes: 6 additions & 1 deletion claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -795,7 +795,12 @@ the comment there about not caching "nothing staged" asks for.
- CI's `tray-install` job installs the tray from the `windows` job's packages on Windows, Linux
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
it to listen on 3492. On macOS it then sends the tray the quit Apple events macOS sends
(`.github/scripts/send-quit.swift`, by pid): an ordinary one, which has to be answered -128 and
end the tray by its own loop, and a logout's, `kAEQuitReason` set to `kAELogOut`, which must
not be answered -128, the refusal that cancels a logout. Both have to end the tray with status
0 and its version marker gone. CI cannot log out, so this is the nearest to one it has.
`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
Expand Down
Loading