Skip to content

Commit dd6b245

Browse files
committed
fix(macos): recover from unavailable restored page
1 parent 806e72d commit dd6b245

2 files changed

Lines changed: 125 additions & 19 deletions

File tree

‎apps/headless/Tests/macos-e2e.sh‎

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
set -euo pipefail
33
cd "${0:a:h}/.."
44

5-
for tool in node curl lsof; do
5+
for tool in node curl defaults lsof; do
66
command -v "$tool" >/dev/null 2>&1 || { echo "macOS E2E tests require $tool" >&2; exit 69; }
77
done
88

@@ -20,16 +20,34 @@ export HEADLESS_HOST_EXECUTABLE="$HOST"
2020
export HEADLESS_FIXTURE_PORT="$PORT"
2121
LOG="$(mktemp "${TMPDIR:-/tmp}/headless-macos-e2e.XXXXXX")"
2222
HOST_LOG="$(mktemp "${TMPDIR:-/tmp}/headless-macos-host.XXXXXX")"
23+
RESTORE_LOG="$(mktemp "${TMPDIR:-/tmp}/headless-macos-restore.XXXXXX")"
2324
export HEADLESS_HOST_LOG="$HOST_LOG"
2425
STEP="boot"
26+
RESTORE_PID=""
27+
DEFAULTS_DOMAIN="com.headless.app"
28+
DEFAULTS_HAD_LAST_URL=0
29+
DEFAULTS_LAST_URL=""
30+
if DEFAULTS_LAST_URL="$(defaults read "$DEFAULTS_DOMAIN" LastURL 2>/dev/null)"; then
31+
DEFAULTS_HAD_LAST_URL=1
32+
fi
33+
34+
restore_last_url() {
35+
if [[ "$DEFAULTS_HAD_LAST_URL" == 1 ]]; then
36+
defaults write "$DEFAULTS_DOMAIN" LastURL -string "$DEFAULTS_LAST_URL"
37+
else
38+
defaults delete "$DEFAULTS_DOMAIN" LastURL >/dev/null 2>&1 || true
39+
fi
40+
}
2541

2642
fail() {
2743
trap - ERR
28-
print -u2 "macOS E2E failed during: $STEP"
29-
print -u2 "---- fixture log ----"
44+
print -r -u2 -- "macOS E2E failed during: $STEP"
45+
print -r -u2 -- "---- fixture log ----"
3046
cat "$LOG" >&2 || true
31-
print -u2 "---- host log ----"
47+
print -r -u2 -- "---- host log ----"
3248
cat "$HOST_LOG" >&2 || true
49+
print -r -u2 -- "---- restore log ----"
50+
cat "$RESTORE_LOG" >&2 || true
3351
exit 1
3452
}
3553
trap 'fail' ERR
@@ -39,9 +57,13 @@ FIXTURE_PID=$!
3957

4058
cleanup() {
4159
"$CLI" stop >/dev/null 2>&1 || true
60+
if [[ -n "$RESTORE_PID" ]]; then
61+
kill "$RESTORE_PID" >/dev/null 2>&1 || true
62+
fi
4263
kill "$FIXTURE_PID" >/dev/null 2>&1 || true
64+
restore_last_url
4365
rm -rf "$HEADLESS_ARTIFACT_DIR"
44-
rm -f "$LOG" "$HOST_LOG"
66+
rm -f "$LOG" "$HOST_LOG" "$RESTORE_LOG"
4567
}
4668
trap cleanup EXIT INT TERM
4769

@@ -52,14 +74,59 @@ for _ in {1..100}; do
5274
done
5375
curl -fsS "http://127.0.0.1:$PORT/designers/dashboard" >/dev/null
5476

77+
STEP="restored-url-fallback"
78+
UNAVAILABLE_PORT=$((PORT + 2000))
79+
if lsof -nP -iTCP:"$UNAVAILABLE_PORT" -sTCP:LISTEN 2>/dev/null | grep -q LISTEN; then
80+
echo "macOS E2E restore port is unexpectedly in use: $UNAVAILABLE_PORT" >&2
81+
fail
82+
fi
83+
RESTORED_URL="http://127.0.0.1:$UNAVAILABLE_PORT/restored"
84+
defaults write "$DEFAULTS_DOMAIN" LastURL -string "$RESTORED_URL"
85+
HEADLESS_AGENT_HOST=0 "$HOST" >"$RESTORE_LOG" 2>&1 &
86+
RESTORE_PID=$!
87+
for _ in {1..100}; do
88+
"$CLI" status >/dev/null 2>&1 && break
89+
sleep 0.05
90+
done
91+
"$CLI" status | grep -q '"ready":true'
92+
RESTORED_URL_CLEARED=0
93+
for _ in {1..300}; do
94+
if ! defaults read "$DEFAULTS_DOMAIN" LastURL >/dev/null 2>&1; then
95+
RESTORED_URL_CLEARED=1
96+
break
97+
fi
98+
sleep 0.05
99+
done
100+
if [[ "$RESTORED_URL_CLEARED" != 1 ]]; then
101+
echo "failed restored URL was not cleared" >&2
102+
"$CLI" qa report >&2 || true
103+
fail
104+
fi
105+
sleep 1
106+
RESTORED_SNAPSHOT="$("$CLI" inspect --text)"
107+
echo "$RESTORED_SNAPSHOT" | grep -q 'search or enter a url'
108+
"$CLI" stop >/dev/null
109+
for _ in {1..100}; do
110+
! kill -0 "$RESTORE_PID" >/dev/null 2>&1 && break
111+
sleep 0.05
112+
done
113+
if kill -0 "$RESTORE_PID" >/dev/null 2>&1; then
114+
echo "manual restore-test host did not stop" >&2
115+
fail
116+
fi
117+
wait "$RESTORE_PID" || true
118+
RESTORE_PID=""
119+
restore_last_url
120+
echo "▸ unavailable restored URL fell back to the start page"
121+
55122
STEP="start-host"
56123
START_RESULT="$("$CLI" start)" || {
57-
print -u2 "headless start failed:"
58-
print -u2 "$START_RESULT"
124+
print -r -u2 -- "headless start failed:"
125+
print -r -u2 -- "$START_RESULT"
59126
fail
60127
}
61128
echo "$START_RESULT" | grep -q '"ready":true' || {
62-
print -u2 "host did not become ready: $START_RESULT"
129+
print -r -u2 -- "host did not become ready: $START_RESULT"
63130
fail
64131
}
65132
echo "▸ host ready"

‎apps/headless/main.swift‎

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -216,10 +216,13 @@ final class BrowserWindowController: NSWindowController, NSWindowDelegate,
216216
private var toastHide: DispatchWorkItem?
217217
private var lastProgress: CGFloat = 0
218218
private var onStartPage = false
219+
private var pendingRestoredStartupURL: URL?
219220
private var agentControlEnabled = false
220221
var onClose: (() -> Void)?
221222

222-
init(url: URL?, size: NSSize?, snap: SnapJob?, isPrimary: Bool) {
223+
init(
224+
url: URL?, restoredStartupURL: URL? = nil, size: NSSize?, snap: SnapJob?, isPrimary: Bool
225+
) {
223226
let diagnosticsBridge = WebKitQABridge()
224227
qaBridge = diagnosticsBridge
225228
let conf = WKWebViewConfiguration()
@@ -300,7 +303,12 @@ final class BrowserWindowController: NSWindowController, NSWindowDelegate,
300303

301304
installMouseMonitor()
302305

303-
if let url { navigate(to: url) } else { loadStartPage() }
306+
if let url {
307+
pendingRestoredStartupURL = restoredStartupURL
308+
load(url)
309+
} else {
310+
loadStartPage()
311+
}
304312
}
305313

306314
required init?(coder: NSCoder) { fatalError("not used") }
@@ -434,6 +442,11 @@ final class BrowserWindowController: NSWindowController, NSWindowDelegate,
434442
// MARK: Navigation
435443

436444
func navigate(to url: URL) {
445+
pendingRestoredStartupURL = nil
446+
load(url)
447+
}
448+
449+
private func load(_ url: URL) {
437450
onStartPage = false
438451
if url.isFileURL {
439452
webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent())
@@ -458,6 +471,7 @@ final class BrowserWindowController: NSWindowController, NSWindowDelegate,
458471
}
459472

460473
func loadStartPage() {
474+
pendingRestoredStartupURL = nil
461475
onStartPage = true
462476
webView.loadHTMLString(startPageHTML, baseURL: nil)
463477
}
@@ -638,7 +652,10 @@ final class BrowserWindowController: NSWindowController, NSWindowDelegate,
638652

639653
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
640654
let u = webView.url?.absoluteString
641-
if u != nil && u != "about:blank" { onStartPage = false }
655+
if u != nil && u != "about:blank" {
656+
pendingRestoredStartupURL = nil
657+
onStartPage = false
658+
}
642659
}
643660

644661
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
@@ -667,6 +684,15 @@ final class BrowserWindowController: NSWindowController, NSWindowDelegate,
667684
if e.code == NSURLErrorCancelled || e.code == 102 { return }
668685
qaBridge.store.append(kind: "request-failed", message: e.localizedDescription,
669686
url: webView.url?.absoluteString)
687+
if let restoredURL = pendingRestoredStartupURL {
688+
pendingRestoredStartupURL = nil
689+
if UserDefaults.standard.string(forKey: "LastURL") == restoredURL.absoluteString {
690+
UserDefaults.standard.removeObject(forKey: "LastURL")
691+
}
692+
loadStartPage()
693+
showToast("Previous page unavailable")
694+
return
695+
}
670696
if launchOptions.snap != nil {
671697
fputs("headless: load failed: \(e.localizedDescription)\n", stderr)
672698
exit(1)
@@ -793,14 +819,20 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
793819
NSApp.setActivationPolicy(.regular)
794820
buildMenu()
795821

796-
let url: URL? = {
797-
if isAgentHost { return nil }
798-
if let u = launchOptions.url { return u }
799-
if launchOptions.snap != nil { return nil }
800-
if let s = UserDefaults.standard.string(forKey: "LastURL") { return URL(string: s) }
801-
return nil
822+
let restoredStartupURL: URL? = {
823+
guard !isAgentHost, launchOptions.url == nil, launchOptions.snap == nil,
824+
let value = UserDefaults.standard.string(forKey: "LastURL") else { return nil }
825+
return URL(string: value)
802826
}()
803-
openWindow(url: url, size: launchOptions.size, snap: launchOptions.snap, isPrimary: true, sessionName: "default")
827+
let url = isAgentHost ? nil : launchOptions.url ?? restoredStartupURL
828+
openWindow(
829+
url: url,
830+
restoredStartupURL: restoredStartupURL,
831+
size: launchOptions.size,
832+
snap: launchOptions.snap,
833+
isPrimary: true,
834+
sessionName: "default"
835+
)
804836
do {
805837
try agentServer.start { [weak self] request in
806838
self?.handleAgentRequest(request) ?? CommandResponse.failure(
@@ -823,12 +855,19 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
823855
@discardableResult
824856
func openWindow(
825857
url: URL?,
858+
restoredStartupURL: URL? = nil,
826859
size: NSSize? = nil,
827860
snap: SnapJob? = nil,
828861
isPrimary: Bool = false,
829862
sessionName: String? = nil
830863
) -> BrowserWindowController {
831-
let controller = BrowserWindowController(url: url, size: size, snap: snap, isPrimary: isPrimary)
864+
let controller = BrowserWindowController(
865+
url: url,
866+
restoredStartupURL: restoredStartupURL,
867+
size: size,
868+
snap: snap,
869+
isPrimary: isPrimary
870+
)
832871
controller.onClose = { [weak self, weak controller] in
833872
guard let self, let controller else { return }
834873
self.controllers.removeAll { $0 === controller }

0 commit comments

Comments
 (0)