From de3d294ca53cc4c744b2aa6fdc060a1a21d9f6ca Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 03:21:11 -0700 Subject: [PATCH 01/64] test: cover Cloud display ownership and readiness gaps --- cmuxTests/CloudClosedPanelRestoreTests.swift | 34 ++++++++++++ cmuxTests/CloudDesktopAccessTests.swift | 35 ++++++++++++ .../CloudSurfaceMoveOwnershipTests.swift | 28 ++++++++++ cmuxTests/CloudSurfaceOwnershipTests.swift | 53 +++++++++++++++++++ 4 files changed, 150 insertions(+) diff --git a/cmuxTests/CloudClosedPanelRestoreTests.swift b/cmuxTests/CloudClosedPanelRestoreTests.swift index 87c8ecbe5289..19c388a59df7 100644 --- a/cmuxTests/CloudClosedPanelRestoreTests.swift +++ b/cmuxTests/CloudClosedPanelRestoreTests.swift @@ -12,6 +12,40 @@ import Testing @MainActor @Suite(.serialized) struct CloudClosedPanelRestoreTests { + @Test("Foreign and unowned displays cannot restore into a Cloud workspace", arguments: ["a", "b"]) + func rejectsDisplayRestoreBeforeLayoutMutation(owner: String) throws { + try withManager { manager in + let workspace = manager.addWorkspace(initialSurface: .browser, autoWelcomeIfNeeded: false) + workspace.cloudVMBinding = WorkspaceCloudVMBinding(vmID: owner, isBase: false) + let panelID = try #require(workspace.focusedPanelId) + let paneID = try #require(workspace.paneId(forPanelId: panelID)) + let before = Set(workspace.panels.keys) + let layout = workspace.bonsplitController.treeSnapshot() + var snapshot = workspace.sessionSnapshot(includeScrollback: false) + snapshot.panels[0].browser?.urlString = "http://10.0.0.7:6901/vnc.html" + let foreign = SurfaceResourceID(machine: .cloud(owner == "a" ? "b" : "a"), kind: .display, key: "display:1") + for source in [foreign, SurfaceResourceID(machine: .local, kind: .display, key: "display:1")] { + let record = SurfaceProjectionRecord(panelID: panelID, resource: source) + snapshot.surfaceProjections = [record] + #expect(workspace.restoreSessionSnapshot(snapshot).isEmpty) + #expect(Set(workspace.panels.keys) == before) + #expect(workspace.bonsplitController.treeSnapshot() == layout) + #expect(workspace.cloudVMID == owner) + #expect(workspace.createPanel(from: snapshot.panels[0], inPane: paneID, + snapshotWorkspaceId: nil, shouldRestoreSingleDefaultCloudTerminal: false, + cloudProjectionRecord: record) == nil) + let closed = ClosedPanelHistoryEntry(workspaceId: workspace.id, paneId: UUID(), tabIndex: 0, + snapshot: snapshot.panels[0], projection: record) + #expect(workspace.restoreClosedPanel(closed) == nil) + #expect(Set(workspace.panels.keys) == before) + } + snapshot.surfaceProjections = nil + #expect(workspace.restoreSessionSnapshot(snapshot).isEmpty, + "A noVNC URL without provenance must not become a local browser") + #expect(Set(workspace.panels.keys) == before) + } + } + @Test("Restoring a Cloud terminal reserves a manual mirror instead of a local shell") func cloudTerminalRestoreDoesNotBrieflyBecomeLocal() throws { try withManager { manager in diff --git a/cmuxTests/CloudDesktopAccessTests.swift b/cmuxTests/CloudDesktopAccessTests.swift index 02fdbced6f4c..c99b76bcfd5d 100644 --- a/cmuxTests/CloudDesktopAccessTests.swift +++ b/cmuxTests/CloudDesktopAccessTests.swift @@ -14,6 +14,41 @@ import WebKit @MainActor @Suite(.serialized, .timeLimit(.minutes(1))) struct CloudDesktopAccessTests { + @Test("A connected noVNC document is ready even before WebKit's finish callback", arguments: [6901, 6902]) + func desktopConnectionCompletesReadiness(port: Int) async throws { + let model = CloudPortAccessModel(target: .init(host: "10.0.0.7", port: port), coordinator: nil, + wake: {}, startForward: { _ in UInt16(40_000 + port) }, stopForward: {}, route: .loopback) + let state = CloudBrowserAccessState() + let remote = try #require(URL(string: "http://10.0.0.7:\(port)/vnc.html")) + state.configure(model: model, url: remote) + model.connect() + #expect(await wait { model.isReady }) + let url = try #require(state.nextURL()) + state.didCommit(url: url) + state.desktopConnectionDidChange(url: url, isConnected: true) + #expect(state.showsPage, "A live RFB connection is stronger evidence than document finish") + state.desktopConnectionDidChange(url: url, isConnected: false) + #expect(state.showsFailureAlert) + state.desktopConnectionDidChange(url: url, isConnected: true) + #expect(!state.showsFailureAlert && state.showsPage) + await model.retire() + } + + @Test("A stale finish cannot complete the requested Cloud document") + func ignoresForeignFinish() async throws { + let model = CloudPortAccessModel(target: .init(host: "10.0.0.7", port: 6901), coordinator: nil, + wake: {}, startForward: { _ in 46901 }, stopForward: {}, route: .loopback) + let state = CloudBrowserAccessState() + state.configure(model: model, url: URL(string: "http://10.0.0.7:6901/vnc.html")!) + model.connect() + #expect(await wait { model.isReady }) + let url = try #require(state.nextURL()) + state.didCommit(url: url) + state.didFinish(url: URL(string: "http://10.0.0.8:6901/vnc.html")) + #expect(!state.showsPage) + await model.retire() + } + @Test("Desktop failure can be dismissed and Retry re-establishes the shared route") func desktopFailureRecovery() async throws { var starts = 0 diff --git a/cmuxTests/CloudSurfaceMoveOwnershipTests.swift b/cmuxTests/CloudSurfaceMoveOwnershipTests.swift index f2738e968142..476519054cac 100644 --- a/cmuxTests/CloudSurfaceMoveOwnershipTests.swift +++ b/cmuxTests/CloudSurfaceMoveOwnershipTests.swift @@ -13,6 +13,34 @@ import Testing @MainActor @Suite("Cloud surface mutation boundaries", .serialized) struct CloudSurfaceMoveOwnershipTests { + @Test("Duplicating a display preserves its VM and independent view identity", arguments: [false, true]) + func displayDuplicationRetainsOwner(offline: Bool) async throws { + try await AppContextSerialGate.withExclusiveAppContext { + let fixture = try VaultPaneAppFixture() + defer { fixture.tearDown() } + let workspace = fixture.workspace + let pane = try #require(workspace.bonsplitController.allPaneIds.first) + let browser = try #require(workspace.newBrowserSurface(inPane: pane, focus: false)) + workspace.cloudVMBinding = WorkspaceCloudVMBinding(vmID: "a", isBase: false) + let catalog = SurfaceCatalog.shared + var display = resource(machine: "a", kind: .display) + display.id.key = "display:1" + if !offline { catalog.upsert(display) } + catalog.restore([SurfaceProjectionRecord(panelID: browser.id, resource: display.id)], workspaceID: workspace.id) + defer { catalog.remove(display.id) } + let duplicate = try #require(workspace.duplicateBrowserToRight(panelId: browser.id, focus: false)) + #expect(duplicate.id != browser.id) + #expect(catalog.projectionRecord(forPanel: duplicate.id)?.resource == display.id) + #expect(workspace.machineOwningSurface(duplicate.id) == .cloud("a")) + #expect(workspace.panels[browser.id] === browser) + let foreign = fixture.manager.addWorkspace(title: "same name", select: false) + foreign.cloudVMBinding = WorkspaceCloudVMBinding(vmID: "b", isBase: false) + #expect(!fixture.appDelegate.moveSurface(panelId: duplicate.id, toWorkspace: foreign.id, + focus: false, focusWindow: false)) + #expect(workspace.panels[duplicate.id] != nil) + } + } + @Test("Foreign Cloud terminal, browser and display moves leave both workspaces intact", arguments: SurfaceResourceKind.allCases) func foreignCloudMove(kind: SurfaceResourceKind) async throws { try await AppContextSerialGate.withExclusiveAppContext { diff --git a/cmuxTests/CloudSurfaceOwnershipTests.swift b/cmuxTests/CloudSurfaceOwnershipTests.swift index 2ddc460f2530..c3f5ca1f9472 100644 --- a/cmuxTests/CloudSurfaceOwnershipTests.swift +++ b/cmuxTests/CloudSurfaceOwnershipTests.swift @@ -14,6 +14,59 @@ import Testing struct CloudSurfaceOwnershipTests { private let machine = SurfaceMachineID.cloud("ownership-b") + @Test("Saved display identities are checked before catalog mutation") + func rejectsForeignCatalogRestore() throws { + let workspace = cloudWorkspace() + defer { workspace.teardownAllPanels() } + let catalog = catalog(for: workspace) + let panelID = try #require(workspace.focusedPanelId) + for owner in [SurfaceMachineID.cloud("ownership-a"), .local] { + let record = SurfaceProjectionRecord(panelID: panelID, + resource: SurfaceResourceID(machine: owner, kind: .display, key: "display:1")) + catalog.restore([record], workspaceID: workspace.id) + #expect(catalog.projectionRecords(forWorkspace: workspace.id).isEmpty) + #expect(catalog.machineOwningPanel(panelID) == nil) + #expect(workspace.cloudVMID == machine.rawValue) + } + } + + @Test("A delayed display materialization cannot commit after the destination changes owner", + arguments: [false, true]) + func checksOwnerAfterMaterialization(reuseExisting: Bool) async throws { + let workspace = cloudWorkspace() + defer { workspace.teardownAllPanels() } + let catalog = catalog(for: workspace) + let provider = CloudPlacementTestProvider(machine: machine) + provider.beforeMaterialization = { + workspace.cloudVMBinding = WorkspaceCloudVMBinding(vmID: "ownership-a", isBase: false) + } + catalog.register(provider) + let display = resource(machine, kind: .display) + catalog.upsert(display) + do { + _ = try await catalog.project(display.id, into: .workspace(id: workspace.id, placement: .tab), + focus: false, reuseExisting: reuseExisting) + Issue.record("A delayed projection committed into a differently owned workspace") + } catch {} + #expect(catalog.snapshot.projections.isEmpty) + #expect(workspace.cloudVMID == "ownership-a") + } + + @Test("An offline display retains provenance without materializing into a rebound workspace") + func checksOwnerWhenPendingRestoreResolves() throws { + let workspace = cloudWorkspace() + defer { workspace.teardownAllPanels() } + let catalog = catalog(for: workspace) + let panelID = try #require(workspace.focusedPanelId) + let display = resource(machine, kind: .display) + catalog.restore([SurfaceProjectionRecord(panelID: panelID, resource: display.id)], workspaceID: workspace.id) + workspace.cloudVMBinding = WorkspaceCloudVMBinding(vmID: "ownership-a", isBase: false) + catalog.upsert(display) + #expect(catalog.projection(forPanel: panelID) == nil) + #expect(catalog.machineOwningPanel(panelID) == machine) + #expect(workspace.cloudVMID == "ownership-a") + } + @Test("Cloud pane hover rejects local and foreign resources", arguments: SurfaceResourceKind.allCases) func rejectsForeignResourceHover(kind: SurfaceResourceKind) throws { let workspace = cloudWorkspace() From eb3edd7f9843c1d4395783ee147b509cc437cd1c Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 04:04:30 -0700 Subject: [PATCH 02/64] Enforce Cloud display provenance and independent guest displays --- Resources/Localizable.xcstrings | 590 ++++++++++++++++++ Sources/AppDelegate+DockSurfaceMove.swift | 6 +- Sources/Cloud/CloudDisplayCoordinator.swift | 73 +++ Sources/Cloud/CloudGuestDisplaySnapshot.swift | 44 ++ Sources/Cloud/CloudTreeNode.swift | 8 +- Sources/Cloud/CloudTreeNodeActions.swift | 10 +- .../Cloud/CloudTreeOutlineView+Displays.swift | 14 + Sources/Cloud/CloudTreeOutlineView.swift | 6 +- Sources/Cloud/CloudTreeRowContentView.swift | 10 +- .../PortForward/CloudBrowserAccessState.swift | 125 +++- .../PortForward/CloudBrowserAccessView.swift | 25 +- .../CloudBrowserConnectionCard.swift | 7 +- .../CloudDesktopConnectionObserver.swift | 15 +- .../PortForward/CloudPortRoutePlan.swift | 4 +- Sources/DockSplitStore+SessionRestore.swift | 4 +- Sources/DockSplitStore+SessionSnapshot.swift | 2 +- Sources/DockSplitStore+SurfaceTransfer.swift | 4 +- Sources/DockSplitStore.swift | 4 +- .../Panels/BrowserPanel+CloudConnection.swift | 58 +- Sources/Panels/BrowserPanel.swift | 46 +- Sources/SessionBrowserPanelSnapshot.swift | 91 +++ Sources/SessionPersistence.swift | 83 --- .../CmuxTuiSnapshotParser+Displays.swift | 18 + Sources/Surfaces/CmuxTuiSnapshotParser.swift | 10 +- .../CmuxTuiSurfaceProvider+Displays.swift | 57 ++ .../CmuxTuiSurfaceProvider+PortForward.swift | 23 +- .../CmuxTuiSurfaceProvider+Refresh.swift | 1 + .../Surfaces/CmuxTuiSurfaceProviders.swift | 42 +- .../DockSplitStore+SurfaceOwnership.swift | 38 ++ .../Surfaces/SurfaceCatalog+Displays.swift | 21 + .../Surfaces/SurfaceCatalog+Ownership.swift | 20 + .../SurfaceCatalog+ProjectionQueries.swift | 24 + .../Surfaces/SurfaceCatalog+Snapshot.swift | 6 +- Sources/Surfaces/SurfaceCatalog.swift | 35 +- Sources/Surfaces/SurfaceCatalogSnapshot.swift | 4 +- Sources/Surfaces/SurfacePaneFactory.swift | 2 +- .../SurfaceProjectionRestoreStore.swift | 5 +- .../Workspace+BrowserDuplication.swift | 46 ++ .../Workspace+CloudDisplayOwnership.swift | 38 ++ .../Surfaces/Workspace+SurfaceCatalog.swift | 8 +- .../Surfaces/Workspace+SurfaceOwnership.swift | 1 + Sources/Workspace.swift | 39 +- cmux.xcodeproj/project.pbxproj | 40 ++ cmuxTests/CloudDesktopAccessTests.swift | 73 +++ cmuxTests/CloudDisplayCatalogTests.swift | 121 ++++ cmuxTests/CloudSurfaceDragFeedbackTests.swift | 6 +- .../CloudSurfaceMoveOwnershipTests.swift | 45 +- ...oudTreeOneMachineManyWorkspacesTests.swift | 2 +- tests/test_cloud_display_catalog.py | 132 ++++ web/scripts/devbox-image-common.ts | 2 + web/services/vms/images/devbox/Dockerfile | 3 +- .../images/devbox/desktop/cmux-desktop-boot | 5 + .../vms/images/devbox/desktop/cmux-display | 255 ++++++++ .../vms/images/devbox/desktop/start-vnc.sh | 40 +- web/tests/vm-devbox-desktop.test.ts | 3 +- web/tests/vm-display-catalog.test.ts | 17 + 56 files changed, 2124 insertions(+), 287 deletions(-) create mode 100644 Sources/Cloud/CloudDisplayCoordinator.swift create mode 100644 Sources/Cloud/CloudGuestDisplaySnapshot.swift create mode 100644 Sources/Cloud/CloudTreeOutlineView+Displays.swift create mode 100644 Sources/SessionBrowserPanelSnapshot.swift create mode 100644 Sources/Surfaces/CmuxTuiSnapshotParser+Displays.swift create mode 100644 Sources/Surfaces/CmuxTuiSurfaceProvider+Displays.swift create mode 100644 Sources/Surfaces/SurfaceCatalog+Displays.swift create mode 100644 Sources/Surfaces/Workspace+BrowserDuplication.swift create mode 100644 Sources/Surfaces/Workspace+CloudDisplayOwnership.swift create mode 100644 cmuxTests/CloudDisplayCatalogTests.swift create mode 100644 tests/test_cloud_display_catalog.py create mode 100755 web/services/vms/images/devbox/desktop/cmux-display create mode 100644 web/tests/vm-display-catalog.test.ts diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index 23d2ec6fb5bc..e4d778c820f4 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -1,6 +1,596 @@ { "sourceLanguage": "en", "strings": { + "cloud.display.restoreUnavailable": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This Cloud display or browser is unavailable. Refresh its machine to reconnect." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Diese Cloud-Arbeitsfläche oder dieser Browser ist nicht verfügbar. Aktualisiere die Maschine, um die Verbindung wiederherzustellen." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cet écran ou navigateur Cloud est indisponible. Actualisez sa machine pour vous reconnecter." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "شاشة السحابة أو متصفحها غير متاح. حدّث جهازها لإعادة الاتصال." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Esta pantalla o navegador de Cloud no está disponible. Actualiza su máquina para volver a conectar." + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "此雲端顯示器或瀏覽器無法使用。請重新整理其機器以重新連線。" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "此云端显示器或浏览器不可用。请刷新其机器以重新连接。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "이 클라우드 디스플레이 또는 브라우저를 사용할 수 없습니다. 해당 머신을 새로 고쳐 다시 연결하세요." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "このクラウドディスプレイまたはブラウザは利用できません。マシンを更新して再接続してください。" + } + } + } + }, + "cloud.display.connectionTimedOut": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The Cloud display did not connect within 45 seconds. Retry to reconnect." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Cloud-Arbeitsfläche hat innerhalb von 45 Sekunden keine Verbindung hergestellt. Versuche es erneut." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La connexion à l’écran Cloud n’a pas abouti dans les 45 secondes. Réessayez." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "لم تتصل شاشة السحابة خلال 45 ثانية. أعد المحاولة للاتصال." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La pantalla de Cloud no se conectó en 45 segundos. Vuelve a intentarlo." + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "雲端顯示器未在 45 秒內連線。請重試以重新連線。" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "云端显示器未在 45 秒内连接。请重试以重新连接。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "클라우드 디스플레이가 45초 이내에 연결되지 않았습니다. 다시 시도하세요." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "45秒以内にクラウドディスプレイに接続できませんでした。再試行してください。" + } + } + } + }, + "cloud.display.connectionCancelled": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The Cloud page connection was cancelled. Retry to connect." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Verbindung zur Cloud-Seite wurde abgebrochen. Versuche es erneut." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La connexion à la page Cloud a été annulée. Réessayez pour vous connecter." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "أُلغي الاتصال بصفحة السحابة. أعد المحاولة للاتصال." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Se canceló la conexión a la página de Cloud. Vuelve a intentarlo." + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "雲端頁面連線已取消。請重試以連線。" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "云端页面连接已取消。请重试以连接。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "클라우드 페이지 연결이 취소되었습니다. 다시 시도하세요." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "クラウドページへの接続がキャンセルされました。再試行してください。" + } + } + } + }, + "cloud.display.connecting": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Connecting to Cloud display…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cloud-Arbeitsfläche wird verbunden…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connexion à l’écran Cloud…" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "جارٍ الاتصال بشاشة السحابة…" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Conectando a la pantalla de Cloud…" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "正在連線至雲端顯示器…" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "正在连接到云端显示器…" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "클라우드 디스플레이에 연결 중…" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "クラウドディスプレイに接続中…" + } + } + } + }, + "cloud.display.connectTitle": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Connect to this Cloud display" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Mit dieser Cloud-Arbeitsfläche verbinden" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Se connecter à cet écran Cloud" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "الاتصال بشاشة السحابة هذه" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Conectar a esta pantalla de Cloud" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "連線至此雲端顯示器" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "连接到此云端显示器" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "이 클라우드 디스플레이에 연결" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "このクラウドディスプレイに接続" + } + } + } + }, + "cloud.display.numberedTitle": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Desktop %d" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Arbeitsfläche %d" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bureau %d" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "سطح المكتب %d" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Escritorio %d" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "桌面 %d" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "桌面 %d" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "데스크톱 %d" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "デスクトップ %d" + } + } + } + }, + "cloud.display.creationUnavailable": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Additional displays are unavailable on this machine. Use a desktop image with display creation support, then refresh Displays." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Zusätzliche Arbeitsflächen sind auf dieser Maschine nicht verfügbar. Verwende ein Desktop-Abbild, das neue Arbeitsflächen unterstützt, und aktualisiere dann die Anzeigeübersicht." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les écrans supplémentaires ne sont pas disponibles sur cette machine. Utilisez une image de bureau prenant en charge leur création, puis actualisez les écrans." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "الشاشات الإضافية غير متاحة على هذا الجهاز. استخدم صورة سطح مكتب تدعم إنشاء الشاشات، ثم حدّث قسم الشاشات." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Esta máquina no admite pantallas adicionales. Usa una imagen de escritorio que permita crearlas y actualiza la sección Pantallas." + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "此機器無法使用其他顯示器。請使用支援建立顯示器的桌面映像,然後重新整理「顯示器」。" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "此机器不支持额外的显示器。请使用支持创建显示器的桌面镜像,然后刷新「显示器」。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "이 머신에서는 추가 디스플레이를 사용할 수 없습니다. 디스플레이 생성을 지원하는 데스크톱 이미지를 사용한 후 디스플레이 목록을 새로 고치세요." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "このマシンではディスプレイを追加できません。ディスプレイ作成に対応したデスクトップイメージを使用し、「ディスプレイ」を更新してください。" + } + } + } + }, + "cloud.display.creationFailed": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The new display could not start. Refresh Displays, then retry. Existing displays are unchanged." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die neue Arbeitsfläche konnte nicht gestartet werden. Aktualisiere die Anzeigeübersicht und versuche es erneut. Bestehende Arbeitsflächen bleiben unverändert." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le nouvel écran n’a pas pu démarrer. Actualisez les écrans, puis réessayez. Les écrans existants restent inchangés." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تعذّر تشغيل الشاشة الجديدة. حدّث قسم الشاشات ثم أعد المحاولة. لم تتغير الشاشات الحالية." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se pudo iniciar la nueva pantalla. Actualiza Pantallas y vuelve a intentarlo. Las pantallas existentes no han cambiado." + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "無法啟動新顯示器。請重新整理「顯示器」後重試。現有顯示器保持不變。" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "无法启动新显示器。请刷新「显示器」后重试。现有显示器保持不变。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "새 디스플레이를 시작할 수 없습니다. 디스플레이 목록을 새로 고친 후 다시 시도하세요. 기존 디스플레이는 변경되지 않았습니다." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "新しいディスプレイを起動できませんでした。「ディスプレイ」を更新して再試行してください。既存のディスプレイは変更されていません。" + } + } + } + }, + "cloud.display.creating": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Creating a display on %@…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Arbeitsfläche auf %@ wird erstellt…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Création d’un écran sur %@…" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "جارٍ إنشاء شاشة على %@…" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Creando una pantalla en %@…" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "正在 %@ 上建立顯示器…" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "正在 %@ 上创建显示器…" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "%@에서 디스플레이 생성 중…" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "%@ にディスプレイを作成中…" + } + } + } + }, + "cloudTree.menu.newDisplay": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "New Display" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neue Arbeitsfläche" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nouvel écran" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "شاشة جديدة" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Nueva pantalla" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "新增顯示器" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "新建显示器" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "새 디스플레이" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "新規ディスプレイ" + } + } + } + }, "debug.cloudSidebarSpacing.preset.compact": { "extractionState": "manual", "localizations": { diff --git a/Sources/AppDelegate+DockSurfaceMove.swift b/Sources/AppDelegate+DockSurfaceMove.swift index 7c6f6956065f..b2387a4e23f8 100644 --- a/Sources/AppDelegate+DockSurfaceMove.swift +++ b/Sources/AppDelegate+DockSurfaceMove.swift @@ -39,9 +39,9 @@ extension AppDelegate { /// Whether a live surface can leave its current owner and be driven from /// `destinationDock`. - func canMoveSurfaceIntoDock(sourceTabId: UUID, destinationDock _: DockSplitStore) -> Bool { + func canMoveSurfaceIntoDock(sourceTabId: UUID, destinationDock: DockSplitStore) -> Bool { guard let source = locateContainerSurface(tabId: sourceTabId) else { return false } - return canMoveSurfaceIntoDock(source) + return destinationDock.surfaceOwnershipPolicy.rejection(for: machineOwningBonsplitTab(sourceTabId)) == nil && canMoveSurfaceIntoDock(source) } /// Whether the right sidebar (Files / Find / Dock) currently owns input @@ -93,7 +93,7 @@ extension AppDelegate { destination: BonsplitController.ExternalTabDropRequest.Destination ) -> Bool { guard let source = locateContainerSurface(tabId: sourceTabId) else { return false } - guard canMoveSurfaceIntoDock(source) else { return false } + guard canMoveSurfaceIntoDock(sourceTabId: sourceTabId, destinationDock: destinationDock) else { return false } let shouldPreserveSourceWorkspace = shouldPreserveSourceWorkspaceAfterDockMove( source, destinationDock: destinationDock diff --git a/Sources/Cloud/CloudDisplayCoordinator.swift b/Sources/Cloud/CloudDisplayCoordinator.swift new file mode 100644 index 000000000000..6d5cd673335b --- /dev/null +++ b/Sources/Cloud/CloudDisplayCoordinator.swift @@ -0,0 +1,73 @@ +import Foundation +import Observation + +/// Explicit display discovery/creation through the existing VM-authorized exec +/// route. Routine terminal catalog refreshes never execute guest commands. +@MainActor +@Observable +final class CloudDisplayCoordinator { + private let execute: @MainActor (String, Int) async throws -> VMExecResult + private(set) var snapshot: CloudGuestDisplaySnapshot? + private(set) var isAvailable = false + private var generation: UInt64 = 0 + private var requestID: UUID? + private var creation: Task? + + init(execute: @escaping @MainActor (String, Int) async throws -> VMExecResult) { + self.execute = execute + } + + var canCreate: Bool { isAvailable && (snapshot?.canCreate == true || requestID != nil) && creation == nil } + + func refresh() async { + guard creation == nil else { return } + generation &+= 1 + let token = generation + do { + let response = try await execute("/usr/local/bin/cmux-display list", 10_000) + let snapshot = try CloudGuestDisplaySnapshot(data: Data(response.stdout.utf8)) + guard token == generation, !Task.isCancelled else { return } + self.snapshot = snapshot + isAvailable = response.exitCode == 0 + } catch { + guard token == generation else { return } + isAvailable = false + } + } + + func create() async throws -> CloudGuestDisplaySnapshot { + if let creation { return try await creation.value } + guard isAvailable, snapshot?.canCreate == true || requestID != nil else { + throw SurfaceCatalogError.unsupported(CloudGuestDisplaySnapshot.unavailableMessage) + } + generation &+= 1 + let token = generation + let request = requestID ?? UUID() + requestID = request + // The UUID is generated here and retained after failures. A retry cannot + // create a second guest display when the first receipt was lost. + let task = Task { [weak self, execute] in + try Task.checkCancellation() + let response = try await execute("/usr/local/bin/cmux-display create --request-id \(request.uuidString.lowercased())", 65_000) + let snapshot = try CloudGuestDisplaySnapshot(data: Data(response.stdout.utf8)) + try Task.checkCancellation() + guard let self, self.generation == token else { throw CancellationError() } + self.snapshot = snapshot + guard response.exitCode == 0, snapshot.error == nil, snapshot.created != nil else { + throw SurfaceCatalogError.unsupported(String(localized: "cloud.display.creationFailed", defaultValue: "The new display could not start. Refresh Displays, then retry. Existing displays are unchanged.")) + } + self.requestID = nil + return snapshot + } + creation = task + defer { if generation == token { creation = nil } } + return try await task.value + } + + func stop() { + generation &+= 1 + creation?.cancel() + creation = nil + isAvailable = false + } +} diff --git a/Sources/Cloud/CloudGuestDisplaySnapshot.swift b/Sources/Cloud/CloudGuestDisplaySnapshot.swift new file mode 100644 index 000000000000..b4f54c713868 --- /dev/null +++ b/Sources/Cloud/CloudGuestDisplaySnapshot.swift @@ -0,0 +1,44 @@ +import Foundation + +/// A guest-issued catalog. Connection targets are derived from validated guest +/// slots and the authenticated provider's VM address, never from a guest URL. +struct CloudGuestDisplaySnapshot: Decodable, Sendable { + struct Display: Decodable, Sendable { + let id: String + let number: Int + let port: Int + let state: SurfaceLifecycle + + func resource(on machine: SurfaceMachineID, address: String?) -> SurfaceResource { + SurfaceResource( + id: SurfaceResourceID(machine: machine, kind: .display, key: id), + title: number == 1 + ? String(localized: "cloudTree.node.desktop", defaultValue: "Desktop") + : String(format: String(localized: "cloud.display.numberedTitle", defaultValue: "Desktop %d"), number), + detail: "noVNC", lifecycle: state, agent: nil, remoteWorkspace: nil, + port: port, url: address.map { CmuxTuiSurfaceProvider.privateDesktopURL(privateAddress: $0, port: port) } + ) + } + } + + let version: Int + let canCreate: Bool + let displays: [Display] + let created: String? + let error: String? + + init(data: Data) throws { + self = try JSONDecoder().decode(Self.self, from: data) + guard version == 1, !displays.isEmpty, displays.count <= 16, + Set(displays.map(\.id)).count == displays.count, + displays.contains(where: { $0.number == 1 }), + displays.allSatisfy({ (1...16).contains($0.number) && $0.id == "display:\($0.number)" && $0.port == 6900 + $0.number }), + created == nil || displays.contains(where: { $0.id == created }) else { + throw SurfaceCatalogError.unsupported(Self.unavailableMessage) + } + } + + static var unavailableMessage: String { + String(localized: "cloud.display.creationUnavailable", defaultValue: "Additional displays are unavailable on this machine. Use a desktop image with display creation support, then refresh Displays.") + } +} diff --git a/Sources/Cloud/CloudTreeNode.swift b/Sources/Cloud/CloudTreeNode.swift index 3e41154c1af9..156888506543 100644 --- a/Sources/Cloud/CloudTreeNode.swift +++ b/Sources/Cloud/CloudTreeNode.swift @@ -24,7 +24,7 @@ final class CloudTreeNode: NSObject { /// row per identity, whatever workspaces (zero or more) show it. case terminalsPool(machine: SurfaceMachineID, count: Int) /// "Displays" group under a cloud machine: one row per VNC screen it exposes. - case displaysPool(machine: SurfaceMachineID, count: Int) + case displaysPool(machine: SurfaceMachineID, count: Int, canCreate: Bool = false) /// "Workspaces" group under a machine. case workspacesGroup(machine: SurfaceMachineID) /// A cmux-tui workspace on a cloud machine; its children are flat pointer rows @@ -59,7 +59,7 @@ final class CloudTreeNode: NSObject { /// A single explanatory line (asleep, connecting, link error, empty). case placeholder(machine: SurfaceMachineID, CloudTreePlaceholder) /// Port discovery is demand-driven when the user opens the Ports group. - var refreshesOnExpansion: Bool { if case .portsGroup = self { true } else { false } } + var refreshesOnExpansion: Bool { switch self { case .portsGroup, .displaysPool: true; default: false } } } let id: String private(set) var kind: Kind @@ -130,7 +130,7 @@ final class CloudTreeNode: NSObject { case .localMachine: return .local case .workspacesGroup(let machine), .browsersGroup(let machine), .portsGroup(let machine), .resourcesPool(let machine, _): return machine - case .terminalsPool(let machine, _), .displaysPool(let machine, _): + case .terminalsPool(let machine, _), .displaysPool(let machine, _, _): return machine case .resource(let machine, _): return machine @@ -846,7 +846,7 @@ enum CloudTreeNodeBuilder { if info.linkState == .connected || info.linkState == .notApplicable || !displays.isEmpty { children.append(CloudTreeNode( id: nodeID(displaysPool: machine), - kind: .displaysPool(machine: machine, count: displays.count), + kind: .displaysPool(machine: machine, count: displays.count, canCreate: snapshot.displayCreationMachines?.contains(machine) == true), children: displays.isEmpty ? [CloudMachineSurfacePresentation.emptyDisplays(info: info)] : displays.map { diff --git a/Sources/Cloud/CloudTreeNodeActions.swift b/Sources/Cloud/CloudTreeNodeActions.swift index 10400c944dcd..23850194fd9f 100644 --- a/Sources/Cloud/CloudTreeNodeActions.swift +++ b/Sources/Cloud/CloudTreeNodeActions.swift @@ -47,6 +47,7 @@ struct CloudTreeNodeActions { let copyPortLink: @MainActor (_ resource: SurfaceResourceID) -> Void let refresh: @MainActor () -> Void var refreshMachine: @MainActor (_ machine: SurfaceMachineID) -> Void = { _ in } + var newDisplay: @MainActor (_ machine: SurfaceMachineID) -> Void = { _ in } var organize: @MainActor (CloudSidebarOrganizationAction, String, [CloudTreeNode]) -> Bool = { _, _, _ in false } /// Navigates a nested terminal through its owning Cloud workspace. var openRemoteTerminal: @MainActor (_ machine: SurfaceMachineID, _ group: SurfaceResourceGroup, _ resource: SurfaceResourceID, _ view: SurfaceRemoteView?, _ openIn: UUID?) -> Void = { _, _, _, _, _ in } @@ -157,10 +158,11 @@ struct CloudTreeNodeActions { } }, projectRemoteView: { resource, view, placement, reuseExisting in + let target = Result { try destination(placement) } run(openingLabel(resource.machine)) { catalog in _ = try await catalog.project( resource, - into: try destination(placement), + into: try target.get(), focus: true, reuseExisting: reuseExisting, remoteView: view @@ -404,6 +406,12 @@ struct CloudTreeNodeActions { ) actions.organize = { action, id, _ in catalog().organizeSidebar(action, nodeID: id) } actions.refreshMachine = refreshMachine + actions.newDisplay = { machine in + let target = Result { try destination(.split) } + run(String(format: String(localized: "cloud.display.creating", defaultValue: "Creating a display on %@…"), machineName(machine))) { catalog in + try await catalog.createDisplay(on: machine, into: target.get()) + } + } let navigationRun: CloudTreeTerminalNavigationCoordinator.Run = run let navigation = CloudTreeTerminalNavigationCoordinator( machineName: machineName, diff --git a/Sources/Cloud/CloudTreeOutlineView+Displays.swift b/Sources/Cloud/CloudTreeOutlineView+Displays.swift new file mode 100644 index 000000000000..f92e5f249fe8 --- /dev/null +++ b/Sources/Cloud/CloudTreeOutlineView+Displays.swift @@ -0,0 +1,14 @@ +import AppKit + +extension CloudTreeOutlineView.Coordinator { + func displayMenuItems(machine: SurfaceMachineID, canCreate: Bool) -> [NSMenuItem] { + let create = item(String(localized: "cloudTree.menu.newDisplay", defaultValue: "New Display")) { [nodeActions] in + nodeActions.newDisplay(machine) + } + create.isEnabled = canCreate + if !canCreate { create.toolTip = CloudGuestDisplaySnapshot.unavailableMessage } + return [create, item(String(localized: "cloudTree.menu.refresh", defaultValue: "Refresh")) { [nodeActions] in + nodeActions.refreshMachine(machine) + }] + } +} diff --git a/Sources/Cloud/CloudTreeOutlineView.swift b/Sources/Cloud/CloudTreeOutlineView.swift index 10ca53ea9f9e..1d089bf022bf 100644 --- a/Sources/Cloud/CloudTreeOutlineView.swift +++ b/Sources/Cloud/CloudTreeOutlineView.swift @@ -624,10 +624,8 @@ struct CloudTreeOutlineView: NSViewRepresentable { item(String(localized: "cloudTree.menu.newTerminal", defaultValue: "New Terminal")) { [nodeActions] in nodeActions.newTerminal(machine, nil) }, item(String(localized: "cloudTree.menu.refresh", defaultValue: "Refresh")) { [nodeActions] in nodeActions.refresh() }, ] - case .displaysPool: - return [ - item(String(localized: "cloudTree.menu.refresh", defaultValue: "Refresh")) { [nodeActions] in nodeActions.refresh() }, - ] + case .displaysPool(let machine, _, let canCreate): + return displayMenuItems(machine: machine, canCreate: canCreate) case .workspacesGroup(let machine): return [ item(String(localized: "cloudTree.menu.newWorkspace", defaultValue: "New Workspace")) { [nodeActions] in nodeActions.newWorkspace(machine) }, diff --git a/Sources/Cloud/CloudTreeRowContentView.swift b/Sources/Cloud/CloudTreeRowContentView.swift index f92cc61558ef..07f904cd195b 100644 --- a/Sources/Cloud/CloudTreeRowContentView.swift +++ b/Sources/Cloud/CloudTreeRowContentView.swift @@ -45,7 +45,7 @@ struct CloudTreeRowContentView: View { CloudTreeLocalMachineRowContent(row: row, style: style) case .terminalsPool(_, let count): CloudTreeGroupRowContent(title: String(localized: "cloudTree.group.terminals", defaultValue: "Terminals"), count: count, style: style) - case .displaysPool(_, let count): + case .displaysPool(_, let count, _): CloudTreeGroupRowContent(title: String(localized: "cloudTree.group.displays", defaultValue: "Displays"), count: count, style: style) case .workspacesGroup: CloudTreeGroupRowContent(title: String(localized: "cloudTree.group.workspaces", defaultValue: "Workspaces"), count: nil, style: style) @@ -424,8 +424,12 @@ struct CloudTreeRowHoverButtons: View { plus(String(localized: "cloudTree.menu.newTerminal", defaultValue: "New Terminal")) { nodeActions.newTerminal(machine, nil) } - case .displaysPool: - EmptyView() + case .displaysPool(let machine, _, let canCreate): + plus(String(localized: "cloudTree.menu.newDisplay", defaultValue: "New Display")) { + nodeActions.newDisplay(machine) + } + .disabled(!canCreate) + .help(canCreate ? String(localized: "cloudTree.menu.newDisplay", defaultValue: "New Display") : CloudGuestDisplaySnapshot.unavailableMessage) case .workspacesGroup(let machine): plus(String(localized: "cloudTree.menu.newWorkspace", defaultValue: "New Workspace")) { nodeActions.newWorkspace(machine) diff --git a/Sources/Cloud/PortForward/CloudBrowserAccessState.swift b/Sources/Cloud/PortForward/CloudBrowserAccessState.swift index 6af16ce50701..088b5d66a4d7 100644 --- a/Sources/Cloud/PortForward/CloudBrowserAccessState.swift +++ b/Sources/Cloud/PortForward/CloudBrowserAccessState.swift @@ -1,6 +1,10 @@ import Foundation import CmuxCore +import CmuxFoundation import Observation +import OSLog + +private let cloudDisplayLogger = Logger(subsystem: "com.cmuxterm.app", category: "CloudDisplayConnection") /// Browser-owned navigation state, separate from the shared VM-port choice. /// Failed loads keep the native connection controls visible in the same pane. @@ -8,6 +12,7 @@ import Observation @Observable final class CloudBrowserAccessState { var model: CloudPortAccessModel? + private(set) var resourceID: SurfaceResourceID? private(set) var remoteURL: URL? private(set) var navigationURL: URL? private(set) var hasCommittedNavigation = false @@ -17,6 +22,56 @@ final class CloudBrowserAccessState { private var dismissedFailure: String? var showsPorts = true private(set) var unavailable: String? + private(set) var desktopConnected = false + @ObservationIgnored private let connectionDeadline: MainActorDeferredActionScheduler + @ObservationIgnored private var navigate: (@MainActor (URL) -> Void)? + @ObservationIgnored private var observationGeneration: UInt64 = 0 + private var activeNavigationID: ObjectIdentifier? + @ObservationIgnored private let logID = UUID().uuidString + @ObservationIgnored private var attempt = 0 + + init(clock: any Clock = ContinuousClock()) { + connectionDeadline = MainActorDeferredActionScheduler(clock: clock) + } + + /// Route readiness belongs to the browser, including while its SwiftUI host + /// is hidden. Observe the current value again after every transition so a + /// cached retry cannot lose a connecting → ready change to view coalescing. + func automaticallyNavigate(_ action: @escaping @MainActor (URL) -> Void) { + navigate = action + observeRoute() + } + + func routeDidConfigure() { observeRoute() } + func retainResource(_ resource: SurfaceResourceID) { resourceID = resource } + + private func observeRoute() { + observationGeneration &+= 1 + let generation = observationGeneration + guard let model, navigate != nil else { return } + withObservationTracking { + _ = model.phase + } onChange: { [weak self] in + Task { @MainActor in + guard let self, self.observationGeneration == generation else { return } + self.observeRoute() + } + } + if let url = nextURL() { navigate?(url) } + } + + private func trace(_ event: String) { + cloudDisplayLogger.debug("Display state: id=\(self.logID, privacy: .public) attempt=\(self.attempt) event=\(event, privacy: .public) ready=\(self.model?.isReady == true) committed=\(self.hasCommittedNavigation) loaded=\(self.loaded) connected=\(self.desktopConnected)") + } + + private func startDeadline() { + guard isDesktop else { return } + connectionDeadline.schedule(after: .seconds(45)) { [weak self] in + guard let self, self.isDesktop, !self.desktopConnected, self.failureMessage == nil else { return } + self.desktopFailure = String(localized: "cloud.display.connectionTimedOut", defaultValue: "The Cloud display did not connect within 45 seconds. Retry to reconnect.") + self.trace("deadline") + } + } func showUnavailable(_ message: String) { leave() @@ -31,7 +86,7 @@ final class CloudBrowserAccessState { var isPreparingDocument: Bool { model != nil && !loaded && failureMessage == nil } var isDesktop: Bool { - model?.target.port == CmuxTuiSnapshotParser.desktopPort && remoteURL?.path == "/vnc.html" + (resourceID?.kind == .display && remoteURL == nil) || (model != nil && remoteURL?.path == "/vnc.html") } var failureMessage: String? { @@ -52,11 +107,25 @@ final class CloudBrowserAccessState { guard isDesktop, hasCommittedNavigation, let navigationURL, url == navigationURL else { return } if isConnected { + connectionDeadline.cancel() + loaded = true + error = nil desktopFailure = nil dismissedFailure = nil } else { + connectionDeadline.cancel() desktopFailure = String(localized: "cloud.portAccess.desktopDisconnected", defaultValue: "The Cloud desktop connection failed. Retry to reconnect to the machine.") } + desktopConnected = isConnected + trace(isConnected ? "rfb_connected" : "rfb_failed") + } + + func desktopConnectionIsConnecting(url: URL) { + guard isDesktop, hasCommittedNavigation, url == navigationURL else { return } + desktopConnected = false + desktopFailure = nil + dismissedFailure = nil + startDeadline() } /// Persist the service identity; the local listener only lives for this app run. @@ -79,8 +148,10 @@ final class CloudBrowserAccessState { return remoteURL } - func configure(model: CloudPortAccessModel, url: URL) { + func configure(model: CloudPortAccessModel, url: URL, resourceID: SurfaceResourceID? = nil) { + observationGeneration &+= 1 unavailable = nil + if let resourceID { self.resourceID = resourceID } self.model = model remoteURL = url navigationURL = nil @@ -89,10 +160,21 @@ final class CloudBrowserAccessState { error = nil desktopFailure = nil dismissedFailure = nil + desktopConnected = false + activeNavigationID = nil + connectionDeadline.cancel() + startDeadline() + attempt += 1 + trace("configured") } func nextURL() -> URL? { guard let remoteURL, let url = model?.url(for: remoteURL) else { + if navigationURL != nil { + hasCommittedNavigation = false + desktopConnected = false + startDeadline() + } navigationURL = nil loaded = false return nil @@ -102,19 +184,25 @@ final class CloudBrowserAccessState { hasCommittedNavigation = false error = nil loaded = false + trace("route_ready") return url } - func didStart(url: URL?) { + func didStart(url: URL?, navigationID: ObjectIdentifier? = nil) { guard let url, owns(url), navigationURL != nil else { return } + activeNavigationID = navigationID hasCommittedNavigation = false loaded = false error = nil desktopFailure = nil dismissedFailure = nil + desktopConnected = false + startDeadline() + trace("navigation_started") } - func didCommit(url: URL?) { + func didCommit(url: URL?, navigationID: ObjectIdentifier? = nil) { + guard navigationID == nil || navigationID == activeNavigationID else { return } guard let url, navigationURL != nil else { return } guard owns(url) else { if ["http", "https"].contains(url.scheme?.lowercased() ?? "") { leave() } @@ -125,28 +213,47 @@ final class CloudBrowserAccessState { navigationURL = url } hasCommittedNavigation = true + trace("navigation_committed") } func didFinish(url: URL?) { - guard let url, navigationURL != nil, hasCommittedNavigation, url.scheme != "about", error == nil else { return } + guard let url, navigationURL != nil, hasCommittedNavigation, owns(url), url.scheme != "about", error == nil else { return } loaded = true error = nil + trace("navigation_finished") } - func didFail(url: URL?, message: String) { + func didFail(url: URL?, message: String, navigationID: ObjectIdentifier? = nil) { + guard navigationID == nil || navigationID == activeNavigationID else { return } guard let url, navigationURL != nil, owns(url) else { return } loaded = false error = message + connectionDeadline.cancel() + trace("navigation_failed") + } + + func didCancel(navigationID: ObjectIdentifier? = nil) { + guard model != nil, !loaded, navigationURL != nil, + navigationID == nil || navigationID == activeNavigationID else { return } + connectionDeadline.cancel() + error = String(localized: "cloud.display.connectionCancelled", defaultValue: "The Cloud page connection was cancelled. Retry to connect.") + trace("navigation_cancelled") } func retry() { + attempt += 1 + trace("retry") navigationURL = nil hasCommittedNavigation = false loaded = false error = nil desktopFailure = nil dismissedFailure = nil + desktopConnected = false + activeNavigationID = nil + startDeadline() model?.retry() + observeRoute() } func owns(_ url: URL) -> Bool { @@ -166,6 +273,12 @@ final class CloudBrowserAccessState { } func leave() { + observationGeneration &+= 1 + navigate = nil + connectionDeadline.cancel() + desktopConnected = false + activeNavigationID = nil + hasCommittedNavigation = false unavailable = nil model = nil remoteURL = nil diff --git a/Sources/Cloud/PortForward/CloudBrowserAccessView.swift b/Sources/Cloud/PortForward/CloudBrowserAccessView.swift index e8c3e0bde0be..b96d6fabb64a 100644 --- a/Sources/Cloud/PortForward/CloudBrowserAccessView.swift +++ b/Sources/Cloud/PortForward/CloudBrowserAccessView.swift @@ -13,21 +13,32 @@ struct CloudBrowserAccessView: View { Group { if let model = state.model { Group { - if state.showsPage || state.failureMessage == nil { content() } else { + if state.showsPage || state.failureMessage == nil { + VStack(spacing: 0) { + if state.isDesktop && !state.desktopConnected && state.failureMessage == nil { + ProgressView(String(localized: "cloud.display.connecting", defaultValue: "Connecting to Cloud display…")) + .controlSize(.small).padding(12) + .accessibilityIdentifier("CloudDisplayConnecting") + } + content() + } + } else { CloudBrowserConnectionCard( address: state.remoteURL?.absoluteString ?? "", - message: state.error ?? model.failureMessage, + message: state.failureMessage, onRetry: { _ = panel.reload() navigateIfReady() - } + }, + isDesktop: state.isDesktop ) } } - .task(id: model.phase) { navigateIfReady() } - .task(id: state.remoteURL) { navigateIfReady() } + .task(id: ObjectIdentifier(model)) { + state.automaticallyNavigate { [weak panel] url in _ = panel?.navigate(to: url) } + } } else if let message = state.unavailable { - CloudBrowserConnectionCard(address: "", message: message, onRetry: nil) + CloudBrowserConnectionCard(address: "", message: message, onRetry: nil, isDesktop: state.isDesktop) } else { content() } @@ -58,7 +69,7 @@ struct CloudBrowserAccessView: View { } private var showsNativeContent: Bool { - panel.cloudAccess.unavailable != nil || panel.cloudAccess.failureMessage != nil + panel.cloudAccess.unavailable != nil || (!panel.cloudAccess.showsPage && panel.cloudAccess.failureMessage != nil) } private func navigateIfReady() { diff --git a/Sources/Cloud/PortForward/CloudBrowserConnectionCard.swift b/Sources/Cloud/PortForward/CloudBrowserConnectionCard.swift index edee93bffed6..50bb427168c3 100644 --- a/Sources/Cloud/PortForward/CloudBrowserConnectionCard.swift +++ b/Sources/Cloud/PortForward/CloudBrowserConnectionCard.swift @@ -5,12 +5,15 @@ struct CloudBrowserConnectionCard: View { let address: String let message: String? let onRetry: (() -> Void)? + var isDesktop = false var body: some View { ScrollView { VStack(alignment: .leading, spacing: 16) { - Image(systemName: "network").font(.system(size: 28)).foregroundStyle(.secondary) - Text(String(localized: "cloud.ports.accessTitle", defaultValue: "Connect to this Cloud port")) + Image(systemName: isDesktop ? "display" : "network").font(.system(size: 28)).foregroundStyle(.secondary) + Text(isDesktop + ? String(localized: "cloud.display.connectTitle", defaultValue: "Connect to this Cloud display") + : String(localized: "cloud.ports.accessTitle", defaultValue: "Connect to this Cloud port")) .font(.title2.weight(.semibold)) Text(verbatim: address).font(.system(.body, design: .monospaced)).textSelection(.enabled) if let message { diff --git a/Sources/Cloud/PortForward/CloudDesktopConnectionObserver.swift b/Sources/Cloud/PortForward/CloudDesktopConnectionObserver.swift index 2c2c4745e6e4..fe13fbff4240 100644 --- a/Sources/Cloud/PortForward/CloudDesktopConnectionObserver.swift +++ b/Sources/Cloud/PortForward/CloudDesktopConnectionObserver.swift @@ -18,7 +18,9 @@ final class CloudDesktopConnectionObserver: NSObject, WKScriptMessageHandler { const connected = document.documentElement.classList.contains('noVNC_connected'); const failed = status.classList.contains('noVNC_status_error') && status.classList.contains('noVNC_open'); - const value = connected ? 'connected' : failed ? 'failed' : null; + const connecting = document.documentElement.classList.contains('noVNC_connecting') || + document.documentElement.classList.contains('noVNC_reconnecting'); + const value = connected ? 'connected' : connecting ? 'connecting' : failed ? 'failed' : null; if (value && value !== last) { last = value; window.webkit.messageHandlers['\(name)'].postMessage(value); @@ -37,16 +39,18 @@ final class CloudDesktopConnectionObserver: NSObject, WKScriptMessageHandler { private weak var webView: WKWebView? private let onChange: @MainActor (URL, Bool) -> Void + private let onConnecting: (@MainActor (URL) -> Void)? - init(webView: WKWebView, onChange: @escaping @MainActor (URL, Bool) -> Void) { + init(webView: WKWebView, onConnecting: (@MainActor (URL) -> Void)?, onChange: @escaping @MainActor (URL, Bool) -> Void) { self.webView = webView self.onChange = onChange + self.onConnecting = onConnecting } - static func install(on webView: WKWebView, onChange: @escaping @MainActor (URL, Bool) -> Void) { + static func install(on webView: WKWebView, onConnecting: (@MainActor (URL) -> Void)? = nil, onChange: @escaping @MainActor (URL, Bool) -> Void) { let controller = webView.configuration.userContentController controller.removeScriptMessageHandler(forName: name, contentWorld: contentWorld) - controller.add(CloudDesktopConnectionObserver(webView: webView, onChange: onChange), contentWorld: contentWorld, name: name) + controller.add(CloudDesktopConnectionObserver(webView: webView, onConnecting: onConnecting, onChange: onChange), contentWorld: contentWorld, name: name) if !controller.userScripts.contains(where: { $0.source == userScript.source }) { controller.addUserScript(userScript) } @@ -55,7 +59,8 @@ final class CloudDesktopConnectionObserver: NSObject, WKScriptMessageHandler { func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { guard message.name == Self.name, message.frameInfo.isMainFrame, message.webView === webView, let url = message.frameInfo.request.url, - let value = message.body as? String, ["connected", "failed"].contains(value) else { return } + let value = message.body as? String, ["connected", "connecting", "failed"].contains(value) else { return } + if value == "connecting" { onConnecting?(url); return } onChange(url, value == "connected") } } diff --git a/Sources/Cloud/PortForward/CloudPortRoutePlan.swift b/Sources/Cloud/PortForward/CloudPortRoutePlan.swift index 384efaf6709f..6dc3cad25b5c 100644 --- a/Sources/Cloud/PortForward/CloudPortRoutePlan.swift +++ b/Sources/Cloud/PortForward/CloudPortRoutePlan.swift @@ -10,7 +10,7 @@ enum CloudPortRoutePlan: Equatable, Sendable { static func plan(resource: SurfaceResource, privateAddress: String?) -> CloudPortRoutePlan { let desktop = resource.kind == .display - guard let port = resource.id.forwardedPort ?? resource.port ?? (desktop ? CmuxTuiSnapshotParser.desktopPort : nil), + guard let port = resource.id.forwardedPort ?? resource.port, (1...65_535).contains(port) else { return .unsupported(String(format: String(localized: "cloudTree.port.noPort", defaultValue: "%@ has no port to open."), resource.id.rawValue)) } @@ -18,7 +18,7 @@ enum CloudPortRoutePlan: Equatable, Sendable { return .unsupported(String(format: String(localized: "cloudTree.port.noPrivateAddress", defaultValue: "%@ has no private network address yet; refresh the machine list and retry."), resource.machine.rawValue)) } let raw = resource.url ?? (desktop - ? CmuxTuiSurfaceProvider.privateDesktopURL(privateAddress: address) + ? CmuxTuiSurfaceProvider.privateDesktopURL(privateAddress: address, port: port) : CmuxInternalHostnames.directPortURL(privateAddress: address, port: port)) guard let url = privateURL(raw, address: address) else { return .unsupported(String(localized: "cloud.portAccess.invalidURL", defaultValue: "This port does not have a valid HTTP or HTTPS address.")) diff --git a/Sources/DockSplitStore+SessionRestore.swift b/Sources/DockSplitStore+SessionRestore.swift index babc20355817..9f5bf4f4dfaf 100644 --- a/Sources/DockSplitStore+SessionRestore.swift +++ b/Sources/DockSplitStore+SessionRestore.swift @@ -6,7 +6,6 @@ import Foundation extension DockSplitStore { /// Restores Dock topology and panel state from a persisted split snapshot. - /// /// Browser panels can remain lightweight until their host reports visibility; /// terminal restore ownership is still validated through the supplied agent index. @discardableResult @@ -16,7 +15,7 @@ extension DockSplitStore { deferBrowserPanels: Bool = false, sourceWorkspaceResolver: (UUID) -> Workspace? = { _ in nil } ) -> [UUID: UUID] { - guard !isRetired else { return [:] } + guard !isRetired, snapshot.panels.allSatisfy(acceptsRestoredDisplay) else { return [:] } sessionRestoreDepth += 1 defer { sessionRestoreDepth = max(sessionRestoreDepth - 1, 0) @@ -146,6 +145,7 @@ extension DockSplitStore { deferBrowserPanel: Bool = false, restorableAgentIndex: RestorableAgentSessionIndex? = nil ) -> UUID? { + guard acceptsRestoredDisplay(snapshot) else { return nil } if (!deferBrowserPanel || snapshot.type != .browser), let sourceWorkspaceId, let sourceWorkspace = sourceWorkspaceResolver(sourceWorkspaceId), diff --git a/Sources/DockSplitStore+SessionSnapshot.swift b/Sources/DockSplitStore+SessionSnapshot.swift index 6b0c586a7c99..a1904876c8ef 100644 --- a/Sources/DockSplitStore+SessionSnapshot.swift +++ b/Sources/DockSplitStore+SessionSnapshot.swift @@ -381,7 +381,7 @@ extension DockSplitStore { forwardHistoryURLStrings: history.forwardHistoryURLStrings, transparentBackground: browser.sessionSnapshotTransparentBackground, diffViewerToken: diffViewer?.token, - diffViewerRequestPath: diffViewer?.requestPath + diffViewerRequestPath: diffViewer?.requestPath, cloudResource: browser.cloudResourceForSession ) } else if let deferred = panel as? DeferredBrowserPanel { browserSnapshot = deferred.sessionPanelSnapshot.browser diff --git a/Sources/DockSplitStore+SurfaceTransfer.swift b/Sources/DockSplitStore+SurfaceTransfer.swift index 874c265d7b12..fac01f8b7ad0 100644 --- a/Sources/DockSplitStore+SurfaceTransfer.swift +++ b/Sources/DockSplitStore+SurfaceTransfer.swift @@ -377,7 +377,7 @@ extension DockSplitStore { atIndex index: Int? = nil, focus: Bool = true ) -> UUID? { - guard !isRetired else { return nil } + guard !isRetired, acceptsDetachedSurface(detached) else { return nil } guard containsPane(paneId.id), panels[detached.panelId] == nil else { return nil } let panel = detached.panel panel.retainTransferredSurfaceMachine(detached.surfaceMachine) @@ -471,7 +471,7 @@ extension DockSplitStore { insertFirst: Bool, focus: Bool = true ) -> UUID? { - guard !isRetired else { return nil } + guard !isRetired, acceptsDetachedSurface(detached) else { return nil } guard containsPane(paneId.id), panels[detached.panelId] == nil else { return nil } diff --git a/Sources/DockSplitStore.swift b/Sources/DockSplitStore.swift index 30b5fcadafb5..ff66f841b67a 100644 --- a/Sources/DockSplitStore.swift +++ b/Sources/DockSplitStore.swift @@ -623,7 +623,7 @@ final class DockSplitStore: BonsplitDelegate, FilePreviewTabMetadataHost { allowsExternalBrowserFallback: Bool = true, websiteDataStore: WKWebsiteDataStore? = nil ) -> UUID? { - guard !isRetired else { return nil } + guard !isRetired, kind != .browser || acceptsUnownedBrowserURL(initialRequest?.url ?? url) else { return nil } ensureLoaded() let source = resolveSourcePanelId(sourcePanelId, preferredPaneId: paneId) let resolvedBrowserProfileID = kind == .browser @@ -706,7 +706,7 @@ final class DockSplitStore: BonsplitDelegate, FilePreviewTabMetadataHost { websiteDataStore: WKWebsiteDataStore? = nil, focus: Bool = true ) -> UUID? { - guard !isRetired else { return nil } + guard !isRetired, kind != .browser || acceptsUnownedBrowserURL(initialRequest?.url ?? url) else { return nil } ensureLoaded() let source = resolveSourcePanelId(sourcePanelId) let resolvedBrowserProfileID = kind == .browser diff --git a/Sources/Panels/BrowserPanel+CloudConnection.swift b/Sources/Panels/BrowserPanel+CloudConnection.swift index 3fbbd492f0ce..361b9540cd47 100644 --- a/Sources/Panels/BrowserPanel+CloudConnection.swift +++ b/Sources/Panels/BrowserPanel+CloudConnection.swift @@ -2,9 +2,65 @@ import Foundation import WebKit extension BrowserPanel { + var cloudResourceForSession: SurfaceResourceID? { + let resource = SurfaceCatalog.shared.projectionRecord(forPanel: id)?.resource + return resource?.machine.isLocal == false ? resource : cloudAccess.resourceID + } + + /// Restore by stable resource identity before loading any saved address. + /// A stale/unknown provider leaves an owned placeholder, never a local page. + func restoreCloudResource(_ resource: SurfaceResourceID) { + let catalog = SurfaceCatalog.shared + do { try catalog.validateOwnership(of: [resource], at: .workspace(id: workspaceId, placement: .tab)) } + catch { cloudAccess.showUnavailable(SurfaceTransferRejection.cloudMachineMismatch.message); return } + cloudAccess.retainResource(resource) + retainTransferredSurfaceMachine(resource.machine) + catalog.restore([SurfaceProjectionRecord(panelID: id, resource: resource)], workspaceID: workspaceId) + guard let provider = catalog.provider(for: resource.machine) as? CmuxTuiSurfaceProvider, + let known = catalog.resources[resource] else { + cloudAccess.showUnavailable(String(localized: "cloud.display.restoreUnavailable", defaultValue: "This Cloud display or browser is unavailable. Refresh its machine to reconnect.")) + return + } + switch CloudPortRoutePlan.plan(resource: known, privateAddress: provider.info.privateAddress) { + case .privateDirect(let raw): + if let url = URL(string: raw) { provider.configureBrowser(self, url: url, resourceID: resource) } + case .unsupported(let message): cloudAccess.showUnavailable(message) + } + } + + /// Cloud panes use their own persistent data store so configuring one VM cannot reroute another. + func prepareCloudBrowserStore(machineID: String) { + let identifier = CloudBrowserRouting.storeID(panelID: id, profileID: profileID, machineID: machineID) + guard cloudBrowserStoreIdentity != identifier else { return } + cloudBrowserMachineID = machineID + cloudBrowserStoreIdentity = identifier + cloudBrowserProxyEndpoint = nil + websiteDataStore = preservesExplicitEphemeralWebsiteDataStore + ? .nonPersistent() : WKWebsiteDataStore(forIdentifier: identifier) + // The route may still be connecting. Do not construct its WebView with + // an unconfigured store: its first network session must own the proxy. + } + + /// Apply proxy credentials before the first request, with no system-network fallback. + func prepareCloudBrowserNavigation() { + guard let endpoint = cloudAccess.model?.browserProxy, + let address = cloudAccess.model?.target.host else { return } + guard endpoint != cloudBrowserProxyEndpoint else { return } + cloudBrowserProxyEndpoint = endpoint + websiteDataStore.proxyConfigurations = [CloudBrowserRouting.configuration(endpoint: endpoint, address: address)] + CloudBrowserRouting.installWebSocketBridge(endpoint: endpoint, address: address, on: webView) + if webView.configuration.websiteDataStore !== websiteDataStore { + replaceWebViewPreservingState(from: webView, websiteDataStore: websiteDataStore, + reason: "cloud_browser_route", restoreAfterReplacement: false) + } + } + func installCloudDesktopConnectionObserver(on webView: WKWebView) { let isCurrent = webViewObservationValidator(for: webView) - CloudDesktopConnectionObserver.install(on: webView) { [weak self] url, isConnected in + CloudDesktopConnectionObserver.install(on: webView, onConnecting: { [weak self] url in + guard let self, isCurrent() else { return } + self.cloudAccess.desktopConnectionIsConnecting(url: url) + }) { [weak self] url, isConnected in guard let self, isCurrent() else { return } self.cloudAccess.desktopConnectionDidChange(url: url, isConnected: isConnected) } diff --git a/Sources/Panels/BrowserPanel.swift b/Sources/Panels/BrowserPanel.swift index 8d0554471ace..d2190b7c34d3 100644 --- a/Sources/Panels/BrowserPanel.swift +++ b/Sources/Panels/BrowserPanel.swift @@ -1984,38 +1984,10 @@ final class BrowserPanel: Panel, ObservableObject { let stableSurfaceIdentity = PanelStableSurfaceIdentity() let panelType: PanelType = .browser let cloudAccess = CloudBrowserAccessState() - private var cloudBrowserMachineID: String? - private var cloudBrowserStoreIdentity: UUID? - private var cloudBrowserProxyEndpoint: CloudBrowserProxyEndpoint? - - /// Cloud panes use their own persistent data store so configuring one VM cannot reroute another. - func prepareCloudBrowserStore(machineID: String) { - let identifier = CloudBrowserRouting.storeID(panelID: id, profileID: profileID, machineID: machineID) - guard cloudBrowserStoreIdentity != identifier else { return } - cloudBrowserMachineID = machineID - cloudBrowserStoreIdentity = identifier - cloudBrowserProxyEndpoint = nil - websiteDataStore = preservesExplicitEphemeralWebsiteDataStore - ? .nonPersistent() : WKWebsiteDataStore(forIdentifier: identifier) - // The route may still be connecting. Do not construct its WebView with - // an unconfigured store: its first network session must own the proxy. - } - - /// Apply proxy credentials before the first request, with no system-network fallback. - func prepareCloudBrowserNavigation() { - guard let endpoint = cloudAccess.model?.browserProxy, - let address = cloudAccess.model?.target.host else { return } - guard endpoint != cloudBrowserProxyEndpoint else { return } - cloudBrowserProxyEndpoint = endpoint - websiteDataStore.proxyConfigurations = [CloudBrowserRouting.configuration(endpoint: endpoint, address: address)] - CloudBrowserRouting.installWebSocketBridge(endpoint: endpoint, address: address, on: webView) - if webView.configuration.websiteDataStore !== websiteDataStore { - replaceWebViewPreservingState(from: webView, websiteDataStore: websiteDataStore, - reason: "cloud_browser_route", restoreAfterReplacement: false) - } - } - func showCloudAddress(_ url: URL) { currentURL = url } + var cloudBrowserMachineID: String? + var cloudBrowserStoreIdentity: UUID? + var cloudBrowserProxyEndpoint: CloudBrowserProxyEndpoint? /// The workspace ID this panel belongs to private(set) var workspaceId: UUID @@ -2032,7 +2004,7 @@ final class BrowserPanel: Panel, ObservableObject { var browserViewportHostRestorationTask: Task? var browserViewportHostRestorationPending = false var websiteDataStore: WKWebsiteDataStore - private let preservesExplicitEphemeralWebsiteDataStore: Bool + let preservesExplicitEphemeralWebsiteDataStore: Bool var browserAutomationUserScripts: [WKUserScript] = [] var browserAutomationInitScriptCount = 0 var browserAutomationStyleScriptCount = 0 @@ -3246,7 +3218,7 @@ final class BrowserPanel: Panel, ObservableObject { targetURL: Self.remoteProxyDisplayURL(for: self.navigationDelegate?.lastAttemptedURL) ?? self.navigationDelegate?.lastAttemptedURL ) - self.cloudAccess.didStart(url: self.navigationDelegate?.lastAttemptedURL) + self.cloudAccess.didStart(url: self.navigationDelegate?.lastAttemptedURL, navigationID: navigation.map(ObjectIdentifier.init)) self.isMainFrameProvisionalNavigationActive = true self.refreshBackgroundAppearance() self.applyMuteState(to: webView, reason: "navigationStart") @@ -3263,7 +3235,7 @@ final class BrowserPanel: Panel, ObservableObject { instanceID: boundWebViewInstanceID, navigationID: navigation.map { ObjectIdentifier($0) } ) - self.cloudAccess.didCommit(url: webView.url) + self.cloudAccess.didCommit(url: webView.url, navigationID: navigation.map(ObjectIdentifier.init)) // An about:blank placeholder leaves the restore-stall detector armed. if !Self.isAboutBlankURL(webView.url) { self.hasCommittedDocumentSinceWebViewReplacement = true @@ -3304,7 +3276,7 @@ final class BrowserPanel: Panel, ObservableObject { navigationDelegate.didFailNavigation = { [weak self] failedWebView, failedURL, failureMessage, failedNavigation in MainActor.assumeIsolated { guard let self, self.isCurrentWebView(failedWebView, instanceID: boundWebViewInstanceID) else { return } - self.cloudAccess.didFail(url: URL(string: failedURL), message: failureMessage) + self.cloudAccess.didFail(url: URL(string: failedURL), message: failureMessage, navigationID: failedNavigation.map(ObjectIdentifier.init)) self.automationNavigationCoordinator.didFail( instanceID: boundWebViewInstanceID, navigationID: failedNavigation.map { ObjectIdentifier($0) }, @@ -3366,6 +3338,7 @@ final class BrowserPanel: Panel, ObservableObject { MainActor.assumeIsolated { guard let self, self.isCurrentWebView(webView, instanceID: boundWebViewInstanceID) else { return } (webView as? CmuxWebView)?.diffViewerNavigationDidCancel(cancelledNavigation) + self.cloudAccess.didCancel(navigationID: cancelledNavigation.map(ObjectIdentifier.init)) self.automationNavigationCoordinator.didCancel( instanceID: boundWebViewInstanceID, navigationID: cancelledNavigation.map { ObjectIdentifier($0) } @@ -4545,7 +4518,7 @@ final class BrowserPanel: Panel, ObservableObject { ) currentURL = restoredURL - + if let resource = snapshot.cloudResource { restoreCloudResource(resource); return } guard shouldRenderRestoredWebView, let restoredURL else { shouldRenderWebView = false refreshNavigationAvailability() @@ -6438,6 +6411,7 @@ extension BrowserPanel { /// Stop loading func stopLoading() { + cloudAccess.didCancel() // Fail closed: a reveal must never blank-shell-heal over an explicit Stop. userStoppedLoadSinceWebViewReplacement = true webView.stopLoading() diff --git a/Sources/SessionBrowserPanelSnapshot.swift b/Sources/SessionBrowserPanelSnapshot.swift new file mode 100644 index 000000000000..4f07efc48ecd --- /dev/null +++ b/Sources/SessionBrowserPanelSnapshot.swift @@ -0,0 +1,91 @@ +import Foundation + +struct SessionBrowserPanelSnapshot: Codable, Sendable { + var urlString: String? + var profileID: UUID? + var shouldRenderWebView: Bool + var pageZoom: Double + var developerToolsVisible: Bool + var isMuted: Bool + var chromeVisibility: BrowserChromeVisibility? = nil + var omnibarVisible: Bool? = nil + var backHistoryURLStrings: [String]? + var forwardHistoryURLStrings: [String]? + /// True when the surface is a transparent internal cmux UI (e.g. the diff + /// viewer). Restored so the surface comes back transparent, not opaque. + var transparentBackground: Bool? = nil + /// Diff viewer token + request path, when this browser surface hosts a diff viewer. + /// Restored by re-registering the token with the app-owned `CmuxDiffViewerURLSchemeHandler` + /// and navigating via the custom scheme, independent of the (possibly-dead) local HTTP server. + var diffViewerToken: String? = nil + var diffViewerRequestPath: String? = nil + /// Per-panel provenance also survives Dock and closed-panel snapshots. + var cloudResource: SurfaceResourceID? = nil + + init( + urlString: String?, + profileID: UUID?, + shouldRenderWebView: Bool, + pageZoom: Double, + developerToolsVisible: Bool, + isMuted: Bool = false, + chromeVisibility: BrowserChromeVisibility? = nil, + omnibarVisible: Bool? = nil, + backHistoryURLStrings: [String]?, + forwardHistoryURLStrings: [String]?, + transparentBackground: Bool? = nil, + diffViewerToken: String? = nil, + diffViewerRequestPath: String? = nil, + cloudResource: SurfaceResourceID? = nil + ) { + self.urlString = urlString + self.profileID = profileID + self.shouldRenderWebView = shouldRenderWebView + self.pageZoom = pageZoom + self.developerToolsVisible = developerToolsVisible + self.isMuted = isMuted + self.chromeVisibility = chromeVisibility + self.omnibarVisible = omnibarVisible + self.backHistoryURLStrings = backHistoryURLStrings + self.forwardHistoryURLStrings = forwardHistoryURLStrings + self.transparentBackground = transparentBackground + self.diffViewerToken = diffViewerToken + self.diffViewerRequestPath = diffViewerRequestPath + self.cloudResource = cloudResource + } + + private enum CodingKeys: String, CodingKey { + case urlString + case profileID + case shouldRenderWebView + case pageZoom + case developerToolsVisible + case isMuted + case chromeVisibility + case omnibarVisible + case backHistoryURLStrings + case forwardHistoryURLStrings + case transparentBackground + case diffViewerToken + case diffViewerRequestPath + case cloudResource + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + urlString = try container.decodeIfPresent(String.self, forKey: .urlString) + profileID = try container.decodeIfPresent(UUID.self, forKey: .profileID) + shouldRenderWebView = try container.decode(Bool.self, forKey: .shouldRenderWebView) + pageZoom = try container.decode(Double.self, forKey: .pageZoom) + developerToolsVisible = try container.decode(Bool.self, forKey: .developerToolsVisible) + isMuted = try container.decodeIfPresent(Bool.self, forKey: .isMuted) ?? false + chromeVisibility = try container.decodeIfPresent(BrowserChromeVisibility.self, forKey: .chromeVisibility) + omnibarVisible = try container.decodeIfPresent(Bool.self, forKey: .omnibarVisible) + backHistoryURLStrings = try container.decodeIfPresent([String].self, forKey: .backHistoryURLStrings) + forwardHistoryURLStrings = try container.decodeIfPresent([String].self, forKey: .forwardHistoryURLStrings) + transparentBackground = try container.decodeIfPresent(Bool.self, forKey: .transparentBackground) + diffViewerToken = try container.decodeIfPresent(String.self, forKey: .diffViewerToken) + diffViewerRequestPath = try container.decodeIfPresent(String.self, forKey: .diffViewerRequestPath) + cloudResource = try container.decodeIfPresent(SurfaceResourceID.self, forKey: .cloudResource) + } +} diff --git a/Sources/SessionPersistence.swift b/Sources/SessionPersistence.swift index cf784b31fb4f..0509d6504a13 100644 --- a/Sources/SessionPersistence.swift +++ b/Sources/SessionPersistence.swift @@ -1593,89 +1593,6 @@ struct SessionTextBoxInputAttachmentSnapshot: Codable, Equatable, Sendable { var cleanupLocalPathWhenDisposed: Bool } -struct SessionBrowserPanelSnapshot: Codable, Sendable { - var urlString: String? - var profileID: UUID? - var shouldRenderWebView: Bool - var pageZoom: Double - var developerToolsVisible: Bool - var isMuted: Bool - var chromeVisibility: BrowserChromeVisibility? = nil - var omnibarVisible: Bool? = nil - var backHistoryURLStrings: [String]? - var forwardHistoryURLStrings: [String]? - /// True when the surface is a transparent internal cmux UI (e.g. the diff - /// viewer). Restored so the surface comes back transparent, not opaque. - var transparentBackground: Bool? = nil - /// Diff viewer token + request path, when this browser surface hosts a diff viewer. - /// Restored by re-registering the token with the app-owned `CmuxDiffViewerURLSchemeHandler` - /// and navigating via the custom scheme, independent of the (possibly-dead) local HTTP server. - var diffViewerToken: String? = nil - var diffViewerRequestPath: String? = nil - - init( - urlString: String?, - profileID: UUID?, - shouldRenderWebView: Bool, - pageZoom: Double, - developerToolsVisible: Bool, - isMuted: Bool = false, - chromeVisibility: BrowserChromeVisibility? = nil, - omnibarVisible: Bool? = nil, - backHistoryURLStrings: [String]?, - forwardHistoryURLStrings: [String]?, - transparentBackground: Bool? = nil, - diffViewerToken: String? = nil, - diffViewerRequestPath: String? = nil - ) { - self.urlString = urlString - self.profileID = profileID - self.shouldRenderWebView = shouldRenderWebView - self.pageZoom = pageZoom - self.developerToolsVisible = developerToolsVisible - self.isMuted = isMuted - self.chromeVisibility = chromeVisibility - self.omnibarVisible = omnibarVisible - self.backHistoryURLStrings = backHistoryURLStrings - self.forwardHistoryURLStrings = forwardHistoryURLStrings - self.transparentBackground = transparentBackground - self.diffViewerToken = diffViewerToken - self.diffViewerRequestPath = diffViewerRequestPath - } - - private enum CodingKeys: String, CodingKey { - case urlString - case profileID - case shouldRenderWebView - case pageZoom - case developerToolsVisible - case isMuted - case chromeVisibility - case omnibarVisible - case backHistoryURLStrings - case forwardHistoryURLStrings - case transparentBackground - case diffViewerToken - case diffViewerRequestPath - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - urlString = try container.decodeIfPresent(String.self, forKey: .urlString) - profileID = try container.decodeIfPresent(UUID.self, forKey: .profileID) - shouldRenderWebView = try container.decode(Bool.self, forKey: .shouldRenderWebView) - pageZoom = try container.decode(Double.self, forKey: .pageZoom) - developerToolsVisible = try container.decode(Bool.self, forKey: .developerToolsVisible) - isMuted = try container.decodeIfPresent(Bool.self, forKey: .isMuted) ?? false - chromeVisibility = try container.decodeIfPresent(BrowserChromeVisibility.self, forKey: .chromeVisibility) - omnibarVisible = try container.decodeIfPresent(Bool.self, forKey: .omnibarVisible) - backHistoryURLStrings = try container.decodeIfPresent([String].self, forKey: .backHistoryURLStrings) - forwardHistoryURLStrings = try container.decodeIfPresent([String].self, forKey: .forwardHistoryURLStrings) - transparentBackground = try container.decodeIfPresent(Bool.self, forKey: .transparentBackground) - diffViewerToken = try container.decodeIfPresent(String.self, forKey: .diffViewerToken) - diffViewerRequestPath = try container.decodeIfPresent(String.self, forKey: .diffViewerRequestPath) - } -} struct SessionMarkdownPanelSnapshot: Codable, Sendable { var filePath: String } diff --git a/Sources/Surfaces/CmuxTuiSnapshotParser+Displays.swift b/Sources/Surfaces/CmuxTuiSnapshotParser+Displays.swift new file mode 100644 index 000000000000..3c67465f243a --- /dev/null +++ b/Sources/Surfaces/CmuxTuiSnapshotParser+Displays.swift @@ -0,0 +1,18 @@ +import Foundation + +extension CmuxTuiSnapshotParser { + /// The machine's display list after a snapshot: a display the daemon's workspaces point + /// at (carrying its views) replaces the bare pool entry of the same id; every other + /// resource passes through. Pure, so the provider's refresh stays a straight line. + static func mergingDisplays(pool: [SurfaceResource], parsed: [SurfaceResource]) -> [SurfaceResource] { + let pointed = Set(parsed.filter { $0.kind == .display }.map(\.id)) + let targets = Dictionary(pool.filter { $0.kind == .display }.map { ($0.id, $0) }, uniquingKeysWith: { first, _ in first }) + return pool.filter { !($0.kind == .display && pointed.contains($0.id)) } + parsed.map { resource in + guard var target = targets[resource.id] else { return resource } + target.remoteViews = resource.remoteViews + target.remoteWorkspace = resource.remoteWorkspace + return target + } + } + +} diff --git a/Sources/Surfaces/CmuxTuiSnapshotParser.swift b/Sources/Surfaces/CmuxTuiSnapshotParser.swift index 351aaaadc71c..1774228100a3 100644 --- a/Sources/Surfaces/CmuxTuiSnapshotParser.swift +++ b/Sources/Surfaces/CmuxTuiSnapshotParser.swift @@ -1778,19 +1778,11 @@ struct CmuxTuiSnapshotParser: Sendable { lifecycle: .running, agent: nil, remoteWorkspace: nil, - port: desktopPort, + port: key == SurfaceResourceID.desktopDisplayKey ? desktopPort : nil, url: directURL ) } - /// The machine's display list after a snapshot: a display the daemon's workspaces point - /// at (carrying its views) replaces the bare pool entry of the same id; every other - /// resource passes through. Pure, so the provider's refresh stays a straight line. - static func mergingDisplays(pool: [SurfaceResource], parsed: [SurfaceResource]) -> [SurfaceResource] { - let pointed = Set(parsed.filter { $0.kind == .display }.map(\.id)) - return pool.filter { !($0.kind == .display && pointed.contains($0.id)) } + parsed - } - /// A forwarded port, shown as a browser resource. `directURL`, when /// given, is where opening it actually navigates — the machine's private /// address over the WireGuard tunnel, never a provider port-forwarding diff --git a/Sources/Surfaces/CmuxTuiSurfaceProvider+Displays.swift b/Sources/Surfaces/CmuxTuiSurfaceProvider+Displays.swift new file mode 100644 index 000000000000..84065772e193 --- /dev/null +++ b/Sources/Surfaces/CmuxTuiSurfaceProvider+Displays.swift @@ -0,0 +1,57 @@ +import Foundation +import CmuxFoundation + +extension CmuxTuiSurfaceProvider { + var supportsDisplayCreation: Bool { isAwake && isRegisteredInCatalog() && displayCoordinator.canCreate } + + var displayResources: [SurfaceResource] { + if let snapshot = displayCoordinator.snapshot { + return snapshot.displays.map { $0.resource(on: machine, address: info.privateAddress) } + } + return [CmuxTuiSnapshotParser.display(machine: machine, + directURL: info.privateAddress.map { Self.privateDesktopURL(privateAddress: $0) })] + } + + /// Only a user-requested refresh/expansion performs guest discovery. Results + /// may publish only through the same still-authorized provider instance. + func refreshDisplays() async { + guard isAwake, info.hasDesktop, isRegisteredInCatalog() else { return } + let generation = currentLifecycleGeneration + await displayCoordinator.refresh() + guard isCurrentLifecycleGeneration(generation), isRegisteredInCatalog() else { return } + publishDisplays() + } + + func createDisplay() async throws -> SurfaceResource { + guard supportsDisplayCreation else { throw SurfaceCatalogError.unsupported(CloudGuestDisplaySnapshot.unavailableMessage) } + let generation = currentLifecycleGeneration + defer { + if isCurrentLifecycleGeneration(generation), isRegisteredInCatalog() { publishDisplays() } + } + let snapshot = try await displayCoordinator.create() + guard isCurrentLifecycleGeneration(generation), isRegisteredInCatalog() else { throw CancellationError() } + guard let display = snapshot.displays.first(where: { $0.id == snapshot.created }) else { + throw SurfaceCatalogError.unsupported(CloudGuestDisplaySnapshot.unavailableMessage) + } + return display.resource(on: machine, address: info.privateAddress) + } + + private func publishDisplays() { + for var resource in displayResources { + // Guest discovery owns the connection, while the daemon/catalog + // owns existing view placements. Refresh must preserve both. + if let existing = catalog.resources[resource.id] { + resource.remoteViews = existing.remoteViews + resource.remoteWorkspace = existing.remoteWorkspace + } + catalog.upsert(resource, from: self) + } + catalog.notifyChange() + } + + /// The noVNC URL retains each display's own port across VM reconnects. + nonisolated static func privateDesktopURL(privateAddress: String, port: Int = CmuxTuiSnapshotParser.desktopPort) -> String { + let base = CmuxInternalHostnames.directPortURL(privateAddress: privateAddress, port: port) + return "\(base)/vnc.html?path=websockify&autoconnect=1&resize=remote&reconnect=1&reconnect_delay=2000" + } +} diff --git a/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift b/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift index f63be9297b71..45220abfcdfe 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift @@ -8,7 +8,7 @@ extension CmuxTuiSurfaceProvider { guard let browser = SurfacePaneFactory.browserPanel(panelID: projection.panelID, in: projection.workspaceID) else { continue } switch CloudPortRoutePlan.plan(resource: resource, privateAddress: info.privateAddress) { case .privateDirect(let raw): - if let url = URL(string: raw) { configureBrowser(browser, url: url) } + if let url = URL(string: raw) { configureBrowser(browser, url: url, resourceID: resource.id) } case .unsupported(let message): browser.cloudAccess.showUnavailable(message) } @@ -34,7 +34,7 @@ extension CmuxTuiSurfaceProvider { switch CloudPortRoutePlan.plan(resource: resource, privateAddress: info.privateAddress) { case .privateDirect(let raw): guard let url = URL(string: raw) else { throw ProviderError.localForwardURLUnavailable } - configureBrowser(browser, url: url) + configureBrowser(browser, url: url, resourceID: resource.id) case .unsupported(let message): browser.cloudAccess.showUnavailable(message) } @@ -42,7 +42,16 @@ extension CmuxTuiSurfaceProvider { } /// Bind the page to its machine proxy without activating a system VPN. - func configureBrowser(_ browser: BrowserPanel, url: URL) { + func configureBrowser(_ browser: BrowserPanel, url: URL, resourceID: SurfaceResourceID? = nil) { + let resourceID = resourceID ?? browser.cloudAccess.resourceID + ?? catalog.projectionRecord(forPanel: browser.id).flatMap { $0.resource.machine.isLocal ? nil : $0.resource } + ?? SurfaceResourceID(machine: machine, kind: .browser, key: "port:\(url.port ?? 80)") + guard resourceID.machine == machine, + browser.cloudAccess.resourceID?.machine == nil || browser.cloudAccess.resourceID?.machine == machine, + (try? catalog.validateOwnership(of: [resourceID], at: .workspace(id: browser.workspaceId, placement: .tab))) != nil else { + browser.cloudAccess.showUnavailable(SurfaceTransferRejection.cloudMachineMismatch.message) + return + } guard let address = info.privateAddress, let privateURL = CloudPortRoutePlan.privateURL(url.absoluteString, address: address) else { browser.cloudAccess.showUnavailable(String(localized: "cloud.portAccess.invalidURL", defaultValue: "This port does not have a valid HTTP or HTTPS address.")) @@ -57,10 +66,12 @@ extension CmuxTuiSurfaceProvider { let port = privateURL.port ?? (privateURL.scheme?.lowercased() == "https" ? 443 : 80) browser.webView.stopLoading() let model = accessModel(port: port, address: address, scheme: privateURL.scheme ?? "http") - browser.cloudAccess.configure(model: model, url: privateURL) + browser.retainTransferredSurfaceMachine(machine) + browser.cloudAccess.configure(model: model, url: privateURL, resourceID: resourceID) browser.prepareCloudBrowserStore(machineID: machineID) browser.showCloudAddress(privateURL) model.connect() + browser.cloudAccess.routeDidConfigure() } func accessModel(port: Int, address: String, scheme: String = "http") -> CloudPortAccessModel { @@ -119,11 +130,11 @@ extension CmuxTuiSurfaceProvider { for resource in catalog.snapshot.resources(on: machine) where resource.kind != .terminal { for projection in catalog.projections(of: resource.id) where !materializedPanels.contains(projection.panelID) { guard let browser = SurfacePaneFactory.browserPanel(panelID: projection.panelID, in: projection.workspaceID), - isCurrentLifecycleGeneration(generation) else { continue } + isCurrentLifecycleGeneration(generation), catalog.canRestoreProjection(projection) else { continue } materializedPanels.insert(projection.panelID) switch CloudPortRoutePlan.plan(resource: resource, privateAddress: info.privateAddress) { case .privateDirect(let raw): - if let url = URL(string: raw) { configureBrowser(browser, url: url) } + if let url = URL(string: raw) { configureBrowser(browser, url: url, resourceID: resource.id) } case .unsupported(let message): browser.cloudAccess.showUnavailable(message) } } diff --git a/Sources/Surfaces/CmuxTuiSurfaceProvider+Refresh.swift b/Sources/Surfaces/CmuxTuiSurfaceProvider+Refresh.swift index cf67628286b4..755ed8326271 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProvider+Refresh.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProvider+Refresh.swift @@ -36,6 +36,7 @@ extension CmuxTuiSurfaceProvider { // Matches the protocol's Void return type so existential catalog reads // preserve force instead of falling through to its legacy default. func refresh(force: Bool) async { + if force { await refreshDisplays() } await refreshCurrentGraph(force: force) } diff --git a/Sources/Surfaces/CmuxTuiSurfaceProviders.swift b/Sources/Surfaces/CmuxTuiSurfaceProviders.swift index 4ff8c2e60313..61fa1f549cef 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProviders.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProviders.swift @@ -27,6 +27,7 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { /// when the build has no hub. Owned by the registry, shared by every provider. let portForwards: CloudHubPortForwarder? let portAccessStore: CloudPortAccessStore + let displayCoordinator: CloudDisplayCoordinator let browserPolicy: @MainActor () -> BrowserURLAllowlistPolicy /// Invalidates suspended work when this provider is stopped or replaced. var isFeatureSuspended = false @@ -118,6 +119,7 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { portForwards: CloudHubPortForwarder? = nil, attachmentClock: any Clock = ContinuousClock(), portAccessStore: CloudPortAccessStore? = nil, + displayCoordinator: CloudDisplayCoordinator? = nil, browserPolicy: @escaping @MainActor () -> BrowserURLAllowlistPolicy = { BrowserURLAllowlistPolicy() } ) { machineID = summary.id @@ -128,6 +130,10 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { self.portForwards = portForwards self.portAccessStore = portAccessStore ?? CloudPortAccessStore() self.browserPolicy = browserPolicy + self.displayCoordinator = displayCoordinator ?? CloudDisplayCoordinator { command, timeout in + guard let client = VMClient.shared else { throw ProviderError.notSignedIn } + return try await client.exec(id: summary.id, command: command, timeoutMs: timeout) + } info = Self.info(from: summary, linkState: summary.status == "running" ? .connecting : .asleep, linkError: nil, stats: nil) installNotificationSync() } @@ -170,6 +176,7 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { } func suspendForFeatureFlag() { isFeatureSuspended = true + displayCoordinator.stop() guestURLService?.stop() guestURLService = nil lifecycleGeneration &+= 1 @@ -243,7 +250,7 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { let resources: [SurfaceResource] if let cloudState { let parsed = CmuxTuiSnapshotParser.mergingDisplays( - pool: hasDesktop ? [desktopDisplayResource()] : [], + pool: hasDesktop ? displayResources : [], parsed: CmuxTuiSnapshotParser.resources(from: cloudState) ) + Self.portResources( machine: machine, @@ -253,7 +260,7 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { ) resources = resourcesWithPendingCreations(parsed, state: cloudState) } else { - var fallback = hasDesktop ? [desktopDisplayResource()] : [] + var fallback = hasDesktop ? displayResources : [] fallback.append(contentsOf: Self.portResources( machine: machine, scannedPorts: scannedPorts, @@ -275,7 +282,7 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { // connect must not leave the desktop unopenable. Opening it forwards the // private noVNC port over the user-space hub (`materializeBrowserPane`). if hasDesktop, catalog.authoritativeSnapshot.resources(on: machine).isEmpty { - catalog.replaceResources([desktopDisplayResource()], on: machine, info: info, from: self) + catalog.replaceResources(displayResources, on: machine, info: info, from: self) } async let stats = try? client.stats(id: machineID) var linkState: SurfaceLinkState = .connected @@ -392,7 +399,7 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { syncNotifications(from: cloudState) } else { let resources = resourcesWithPendingCreations( - hasDesktop ? [desktopDisplayResource()] : [], + hasDesktop ? displayResources : [], state: nil ) var fallback = resources @@ -559,7 +566,7 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { var pool: [SurfaceResource] = [] // The control plane's resolved kind is authoritative. Freestyle snapshot if summary.resolvedKind.hasDesktop { - pool.append(desktopDisplayResource()) + pool.append(contentsOf: displayResources) } var resources = CmuxTuiSnapshotParser.mergingDisplays( pool: pool, @@ -601,9 +608,9 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { var resources = CmuxTuiSnapshotParser.resources(from: state, matching: affected) resources = resourcesWithPendingCreations(resources, state: state) if summary.resolvedKind.hasDesktop, - affected.contains(SurfaceResourceID(machine: machine, kind: .display, key: "display:1")) { + affected.contains(where: { $0.kind == .display }) { resources = CmuxTuiSnapshotParser.mergingDisplays( - pool: [desktopDisplayResource()], + pool: displayResources.filter { affected.contains($0.id) }, parsed: resources ) } @@ -1259,25 +1266,6 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { : "\(machineID):\(port)" } - /// The desktop row. Its URL is the machine's private noVNC address, so opening it - /// never needs a provider preview endpoint; nil until the machine has an address. - private func desktopDisplayResource() -> SurfaceResource { - CmuxTuiSnapshotParser.display( - machine: machine, - directURL: summary.preferredPrivateAddress.map { Self.privateDesktopURL(privateAddress: $0) } - ) - } - - /// The noVNC URL uses only the VM private address. The private network is - /// the access check, so no public preview token or endpoint is required. - nonisolated static func privateDesktopURL(privateAddress: String) -> String { - let base = CmuxInternalHostnames.directPortURL( - privateAddress: privateAddress, - port: CmuxTuiSnapshotParser.desktopPort - ) - return "\(base)/vnc.html?path=websockify&autoconnect=1&resize=remote&reconnect=1&reconnect_delay=2000" - } - /// Turn a VM-local browser URL into the same URL on the VM private address. /// Path, query, fragment, scheme, and port stay unchanged. nonisolated static func privateBrowserURL(_ raw: String, privateAddress: String) -> String? { @@ -1302,7 +1290,7 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { var updated = resource switch resource.kind { case .display: - updated.url = privateDesktopURL(privateAddress: privateAddress) + updated.url = resource.port.map { privateDesktopURL(privateAddress: privateAddress, port: $0) } case .browser: if resource.id.key.hasPrefix("port:"), let port = resource.port { updated.url = CmuxInternalHostnames.directPortURL( diff --git a/Sources/Surfaces/DockSplitStore+SurfaceOwnership.swift b/Sources/Surfaces/DockSplitStore+SurfaceOwnership.swift index 128d9c07d820..ac6dbfbfe570 100644 --- a/Sources/Surfaces/DockSplitStore+SurfaceOwnership.swift +++ b/Sources/Surfaces/DockSplitStore+SurfaceOwnership.swift @@ -1,9 +1,47 @@ import Foundation extension DockSplitStore { + var surfaceOwnershipPolicy: SurfaceOwnershipPolicy { + scope == .workspace ? (Workspace.liveWorkspace(id: workspaceId)?.surfaceOwnershipPolicy ?? .init(cloudMachine: nil)) : .init(cloudMachine: nil) + } + + func acceptsUnownedBrowserURL(_ url: URL?) -> Bool { + url?.path != "/vnc.html" || surfaceOwnershipPolicy.rejection(for: nil) == nil + } + + func surfaceDropRejection(_ transfer: PaneDragTransfer, source: PaneTransferSourceResolver.Source) -> SurfaceTransferRejection? { + switch source { + case .surfaceResources(let group): + return SurfaceCatalog.shared.ownershipRejection(for: group.resources, policy: surfaceOwnershipPolicy) + case .surface: + let machine = transfer.isFromCurrentProcess ? AppDelegate.shared?.machineOwningBonsplitTab(transfer.tabId) : nil + return surfaceOwnershipPolicy.rejection(for: machine) + case .vaultSession, .filePreview, .rightSidebarTool: + return surfaceOwnershipPolicy.rejection(for: .local) + } + } + + func acceptsDetachedSurface(_ transfer: Workspace.DetachedSurfaceTransfer) -> Bool { + if transfer.origin == .dock(workspaceId) { return true } + return surfaceOwnershipPolicy.rejection(for: transfer.surfaceMachine + ?? SurfaceCatalog.shared.machineOwningPanel(transfer.panelId) + ?? transfer.panel.transferredSurfaceMachine) == nil + } + + func acceptsRestoredDisplay(_ snapshot: SessionPanelSnapshot) -> Bool { + if let resource = snapshot.browser?.cloudResource { + return surfaceOwnershipPolicy.rejection(for: resource.machine) == nil + } + if let raw = snapshot.browser?.urlString, URL(string: raw)?.path == "/vnc.html" { + return surfaceOwnershipPolicy.rejection(for: nil) == nil + } + return true + } + func machineOwningSurface(_ panelID: UUID) -> SurfaceMachineID? { guard panels[panelID] != nil else { return nil } if let machine = SurfaceCatalog.shared.machineOwningPanel(panelID), !machine.isLocal { return machine } + if let resource = (panels[panelID] as? DeferredBrowserPanel)?.sessionPanelSnapshot.browser?.cloudResource { return resource.machine } return panels[panelID]?.transferredSurfaceMachine ?? detachedSurfaceTransfersByPanelId[panelID]?.surfaceMachine ?? .local diff --git a/Sources/Surfaces/SurfaceCatalog+Displays.swift b/Sources/Surfaces/SurfaceCatalog+Displays.swift new file mode 100644 index 000000000000..291934f97611 --- /dev/null +++ b/Sources/Surfaces/SurfaceCatalog+Displays.swift @@ -0,0 +1,21 @@ +import Foundation + +extension SurfaceCatalog { + /// Creation and opening share the placement policy. The guest resource stays + /// on its VM if the selected destination disappears during creation. + func createDisplay(on machine: SurfaceMachineID, into destination: SurfaceDestination) async throws { + guard Workspace.liveWorkspace(id: destination.workspaceID) != nil else { + throw SurfaceCatalogError.destinationNotFound(destination.workspaceID.uuidString) + } + let identity = SurfaceResourceID(machine: machine, kind: .display, key: "new") + try validateOwnership(of: [identity], at: destination) + guard let provider = provider(for: machine) as? CmuxTuiSurfaceProvider else { + throw SurfaceCatalogError.noProvider(machine) + } + let resource = try await provider.createDisplay() + try Task.checkCancellation() + try validateOwnership(of: [resource.id], at: destination) + guard self.provider(for: machine) === provider else { throw CancellationError() } + _ = try await project(resource.id, into: destination, focus: true, reuseExisting: false) + } +} diff --git a/Sources/Surfaces/SurfaceCatalog+Ownership.swift b/Sources/Surfaces/SurfaceCatalog+Ownership.swift index 81dd2d12f630..2f2f94105bcd 100644 --- a/Sources/Surfaces/SurfaceCatalog+Ownership.swift +++ b/Sources/Surfaces/SurfaceCatalog+Ownership.swift @@ -44,6 +44,26 @@ extension SurfaceCatalog { let rejection = ownershipRejection(for: resources, policy: policy) { throw rejection } } + /// Provider work may suspend. Check the live destination again before its + /// receipt can enter the catalog, and retire only the just-created view. + func validateMaterializationOwnership(_ projection: SurfaceProjection, provider: any SurfaceProvider) throws { + do { + try validateOwnership(of: [projection.resource], at: .workspace(id: projection.workspaceID, placement: .tab)) + } catch { + provider.discardMaterialization(projection) + throw error + } + } + + func canRestoreProjection(_ projection: SurfaceProjection) -> Bool { + do { + try validateOwnership(of: [projection.resource], at: .workspace(id: projection.workspaceID, placement: .tab)) + return true + } catch { + return false + } + } + func ownershipRejection(for resources: [SurfaceResourceID], policy: SurfaceOwnershipPolicy) -> SurfaceTransferRejection? { guard policy.cloudMachine != nil else { return nil } return policy.rejection(for: resources.map(machineOwningResource)) diff --git a/Sources/Surfaces/SurfaceCatalog+ProjectionQueries.swift b/Sources/Surfaces/SurfaceCatalog+ProjectionQueries.swift index 4ecf052e8cb0..abbd5a16af95 100644 --- a/Sources/Surfaces/SurfaceCatalog+ProjectionQueries.swift +++ b/Sources/Surfaces/SurfaceCatalog+ProjectionQueries.swift @@ -20,4 +20,28 @@ extension SurfaceCatalog { func projectedTerminalIDs(on machine: SurfaceMachineID) -> [String] { Array(Set(projections.lazy.filter { $0.resource.machine == machine && $0.resource.kind == .terminal }.map { $0.resource.key })).sorted() } + func projectionRecords(forWorkspace workspaceID: UUID) -> [SurfaceProjectionRecord] { + var records = projections + .filter { $0.workspaceID == workspaceID } + .map { + SurfaceProjectionRecord( + panelID: $0.panelID, + resource: $0.resource, + remoteWorkspaceID: $0.remoteWorkspaceID, + remoteTabID: $0.remoteTabID + ) + } + pendingRestoredProjections.mergeRecords(into: &records, for: workspaceID) + return records.sorted { $0.panelID.uuidString < $1.panelID.uuidString } + } + + /// Cloud machine IDs referenced by restored panes that are waiting for a + /// provider to report their resources. The registry uses these IDs during + /// stale-machine reconciliation so a deleted ID cannot attach old panes + /// when a different machine later receives the same ID. + var pendingRestoredMachineIDs: Set { + Set(pendingRestoredProjections.machineIDs.compactMap { $0.cloudMachineID }) + } + + } diff --git a/Sources/Surfaces/SurfaceCatalog+Snapshot.swift b/Sources/Surfaces/SurfaceCatalog+Snapshot.swift index fc2bdff60675..9cf2c1911c3a 100644 --- a/Sources/Surfaces/SurfaceCatalog+Snapshot.swift +++ b/Sources/Surfaces/SurfaceCatalog+Snapshot.swift @@ -26,14 +26,16 @@ extension SurfaceCatalog { /// enumerate destructive operations. Presentation still withholds a stale /// graph's cwd, and stale machines are flagged, exactly as `snapshot` does. var authoritativeSnapshot: SurfaceCatalogSnapshot { - SurfaceCatalogSnapshot( + let displayCreationMachines = Set(machines.keys.filter { (provider(for: $0) as? CmuxTuiSurfaceProvider)?.supportsDisplayCreation == true }) + return SurfaceCatalogSnapshot( machines: machines.values.map(authoritativeMachineInfo).sorted { if $0.id.isLocal != $1.id.isLocal { return $0.id.isLocal } return $0.name.localizedStandardCompare($1.name) == .orderedAscending }, resources: resources.values.map(resourceForPresentation).sorted { $0.catalogPrecedes($1) }, projections: projections.sorted { $0.panelID.uuidString < $1.panelID.uuidString }, - staleMachineIDs: Set(cloudStateObservations.filter { $0.value.freshness != .current }.keys) + staleMachineIDs: Set(cloudStateObservations.filter { $0.value.freshness != .current }.keys), + displayCreationMachines: displayCreationMachines.isEmpty ? nil : displayCreationMachines ) } diff --git a/Sources/Surfaces/SurfaceCatalog.swift b/Sources/Surfaces/SurfaceCatalog.swift index 323f1276e123..f6fc5419c96e 100644 --- a/Sources/Surfaces/SurfaceCatalog.swift +++ b/Sources/Surfaces/SurfaceCatalog.swift @@ -635,6 +635,7 @@ final class SurfaceCatalog { // showing another tab of the same terminal. return resolvedRemoteView == nil || $0.remoteTabID == resolvedRemoteView?.tabID }) { + try validateOwnership(of: [id], at: .workspace(id: existing.workspaceID, placement: .tab)) try claimCompletedMaterializationIfNeeded(materializationKey, projection: existing) let resolved = attachRemoteView(resolvedRemoteView, to: existing) if resource.kind != .terminal, @@ -672,6 +673,10 @@ final class SurfaceCatalog { self.cancelInFlightProjectWaiter(materializationKey, waiterID: waiterID) } } + do { try validateOwnership(of: [id], at: destination) } catch { + cancelCompletedMaterialization(materializationKey, waiterID: waiterID) + throw error + } return try finalizeMaterializationWaiter( key: materializationKey, id: id, @@ -682,6 +687,7 @@ final class SurfaceCatalog { } let projection = try await provider.materializeValidated(resource, remoteView: resolvedRemoteView, at: destination, focus: focus, adopting: reservation) + try validateMaterializationOwnership(projection, provider: provider) guard !Task.isCancelled, providers[id.machine] === provider, !isDeletingCloudResource(id, remoteWorkspaceID: resolvedRemoteView?.workspace.id) else { provider.discardMaterialization(projection) @@ -739,6 +745,7 @@ final class SurfaceCatalog { do { try self?.validateOwnership(of: [id], at: destination) let projection = try await provider.materializeValidated(resource, remoteView: remoteView, at: destination, focus: focus, adopting: reservation) + try self?.validateMaterializationOwnership(projection, provider: provider) self?.finishInFlightProject(key, token: token, provider: provider, result: .success(projection)) } catch { self?.finishInFlightProject(key, token: token, provider: provider, result: .failure(error)) @@ -1302,6 +1309,8 @@ final class SurfaceCatalog { /// after the link reconnects); local resources are re-registered by the local provider /// with the same panel-derived key, so they resolve immediately. func restore(_ records: [SurfaceProjectionRecord], workspaceID: UUID) { + do { try validateOwnership(of: records.map(\.resource), at: .workspace(id: workspaceID, placement: .tab)) } + catch { return } for record in records { if resources[record.resource] != nil { pendingRestoredProjections.remove(panelID: record.panelID) @@ -1321,29 +1330,6 @@ final class SurfaceCatalog { notifyChange() } - func projectionRecords(forWorkspace workspaceID: UUID) -> [SurfaceProjectionRecord] { - var records = projections - .filter { $0.workspaceID == workspaceID } - .map { - SurfaceProjectionRecord( - panelID: $0.panelID, - resource: $0.resource, - remoteWorkspaceID: $0.remoteWorkspaceID, - remoteTabID: $0.remoteTabID - ) - } - pendingRestoredProjections.mergeRecords(into: &records, for: workspaceID) - return records.sorted { $0.panelID.uuidString < $1.panelID.uuidString } - } - - /// Cloud machine IDs referenced by restored panes that are waiting for a - /// provider to report their resources. The registry uses these IDs during - /// stale-machine reconciliation so a deleted ID cannot attach old panes - /// when a different machine later receives the same ID. - var pendingRestoredMachineIDs: Set { - Set(pendingRestoredProjections.machineIDs.compactMap { $0.cloudMachineID }) - } - /// Returns whether at least one resource is currently published for a machine. /// This is intentionally unsorted and does not materialize a snapshot. func hasResources(on machine: SurfaceMachineID) -> Bool { @@ -1364,7 +1350,8 @@ final class SurfaceCatalog { var resolvedWorkspaceIDs = Set() let resolved = pendingRestoredProjections.takeResolvable( machine: machine, - availableResources: Set(resources.keys) + availableResources: Set(resources.keys), + isAllowed: canRestoreProjection ) for projection in resolved { insertSupersedingLocalPlaceholder(projection) diff --git a/Sources/Surfaces/SurfaceCatalogSnapshot.swift b/Sources/Surfaces/SurfaceCatalogSnapshot.swift index d9511883f13a..6a01257e8f96 100644 --- a/Sources/Surfaces/SurfaceCatalogSnapshot.swift +++ b/Sources/Surfaces/SurfaceCatalogSnapshot.swift @@ -11,6 +11,7 @@ struct SurfaceCatalogSnapshot: Hashable, Codable, Sendable { var resources: [SurfaceResource] var projections: [SurfaceProjection] var staleMachineIDs: Set = [] + var displayCreationMachines: Set? = nil static let empty = SurfaceCatalogSnapshot(machines: [], resources: [], projections: []) @@ -30,7 +31,7 @@ struct SurfaceCatalogSnapshot: Hashable, Codable, Sendable { extension SurfaceCatalogSnapshot { private enum CodingKeys: String, CodingKey { - case pendingWorkspaceDeletions, machines, resources, projections, staleMachineIDs + case pendingWorkspaceDeletions, machines, resources, projections, staleMachineIDs, displayCreationMachines } init(from decoder: Decoder) throws { @@ -40,5 +41,6 @@ extension SurfaceCatalogSnapshot { resources = try values.decode([SurfaceResource].self, forKey: .resources) projections = try values.decode([SurfaceProjection].self, forKey: .projections) staleMachineIDs = try values.decodeIfPresent(Set.self, forKey: .staleMachineIDs) ?? [] + displayCreationMachines = try values.decodeIfPresent(Set.self, forKey: .displayCreationMachines) } } diff --git a/Sources/Surfaces/SurfacePaneFactory.swift b/Sources/Surfaces/SurfacePaneFactory.swift index 31608d713781..e8d1af88dd24 100644 --- a/Sources/Surfaces/SurfacePaneFactory.swift +++ b/Sources/Surfaces/SurfacePaneFactory.swift @@ -47,7 +47,7 @@ enum SurfacePaneFactory { /// The browser pane behind a projection, if it still exists and is a browser. static func browserPanel(panelID: UUID, in workspaceID: UUID) -> BrowserPanel? { - workspace(id: workspaceID)?.panels[panelID] as? BrowserPanel + workspace(id: workspaceID)?.browserPanelIncludingDock(for: panelID) ?? DockSplitStore.liveStore(containingPanel: panelID)?.browserPanel(for: panelID) } /// Sends an existing browser pane to `url` — the optimistic pane's second step, once diff --git a/Sources/Surfaces/SurfaceProjectionRestoreStore.swift b/Sources/Surfaces/SurfaceProjectionRestoreStore.swift index 12edee361cda..6006fd4e4a05 100644 --- a/Sources/Surfaces/SurfaceProjectionRestoreStore.swift +++ b/Sources/Surfaces/SurfaceProjectionRestoreStore.swift @@ -65,10 +65,11 @@ struct SurfaceProjectionRestoreStore: Sendable { /// Returns and removes staged projections whose resources are now available. mutating func takeResolvable( machine: SurfaceMachineID, - availableResources: Set + availableResources: Set, + isAllowed: (SurfaceProjection) -> Bool = { _ in true } ) -> [SurfaceProjection] { let resolved = entriesByPanelID.values.filter { - $0.resource.machine == machine && availableResources.contains($0.resource) + $0.resource.machine == machine && availableResources.contains($0.resource) && isAllowed($0) } for entry in resolved { entriesByPanelID[entry.panelID] = nil diff --git a/Sources/Surfaces/Workspace+BrowserDuplication.swift b/Sources/Surfaces/Workspace+BrowserDuplication.swift new file mode 100644 index 000000000000..5e199c243d42 --- /dev/null +++ b/Sources/Surfaces/Workspace+BrowserDuplication.swift @@ -0,0 +1,46 @@ +import Foundation + +/// Browser view duplication retains resource provenance before navigation. +extension Workspace { + @discardableResult + func duplicateBrowserToRight(panelId: UUID, focus: Bool = true) -> BrowserPanel? { + guard let anchorTabId = surfaceIdFromPanelId(panelId), + let paneId = paneId(forPanelId: panelId), + let browser = browserPanel(for: panelId) else { return nil } + let catalog = SurfaceCatalog.shared + let record = catalog.projectionRecord(forPanel: panelId).flatMap { $0.resource.machine.isLocal ? nil : $0 } + let resource = record?.resource ?? browser.cloudAccess.resourceID + guard surfaceOwnershipPolicy.rejection(for: machineOwningSurface(panelId)) == nil else { return nil } + let isCloud = resource?.machine.isLocal == false + let targetIndex = insertionIndexToRight(of: anchorTabId, inPane: paneId) + guard let newPanel = newBrowserSurface( + inPane: paneId, + url: isCloud ? nil : browser.currentURLForTabDuplication, + focus: focus, + preferredProfileID: browser.profileID, + chromeVisibility: browser.chromeVisibility, + bypassRemoteProxy: browser.bypassesRemoteWorkspaceProxyForTabDuplication, + websiteDataStore: browser.explicitEphemeralWebsiteDataStoreForSibling + ) else { return nil } + if let resource, isCloud { + // Install the identity before the first network request. In particular, + // an offline restored display never becomes an anonymous local URL. + newPanel.retainTransferredSurfaceMachine(resource.machine) + catalog.restore([SurfaceProjectionRecord(panelID: newPanel.id, resource: resource, + remoteWorkspaceID: record?.remoteWorkspaceID)], workspaceID: id) + if let model = browser.cloudAccess.model, let url = browser.cloudAccess.remoteURL { + newPanel.prepareCloudBrowserStore(machineID: resource.machine.rawValue) + newPanel.cloudAccess.configure(model: model, url: url, resourceID: resource) + newPanel.showCloudAddress(url) + model.connect() + } else { + newPanel.restoreCloudResource(resource) + } + } + newPanel.setMuted(browser.isMuted) + syncBrowserAudioMuteStateForPanel(newPanel.id, browserPanel: newPanel) + _ = reorderSurface(panelId: newPanel.id, toIndex: targetIndex, focus: focus) + return newPanel + } + +} diff --git a/Sources/Surfaces/Workspace+CloudDisplayOwnership.swift b/Sources/Surfaces/Workspace+CloudDisplayOwnership.swift new file mode 100644 index 000000000000..9ab376b571a0 --- /dev/null +++ b/Sources/Surfaces/Workspace+CloudDisplayOwnership.swift @@ -0,0 +1,38 @@ +import Foundation + +extension Workspace { + /// URL-only entrypoints have no display provenance. Use the catalog's + /// machine/resource open path for noVNC in a VM-bound workspace. + func acceptsUnownedBrowserURL(_ url: URL?) -> Bool { + url?.path != "/vnc.html" || surfaceOwnershipPolicy.rejection(for: nil) == nil + } + + /// A saved URL is a connection target, never evidence of which VM owns it. + /// Old noVNC records without provenance may open in local workspaces only. + func acceptsRestoredPanel(_ snapshot: SessionPanelSnapshot, projection: SurfaceProjectionRecord?, policy: SurfaceOwnershipPolicy? = nil) -> Bool { + let policy = policy ?? surfaceOwnershipPolicy + if let resource = snapshot.browser?.cloudResource { + if let projection, projection.resource != resource { return false } + return policy.rejection(for: resource.machine) == nil + } + if let projection { return policy.rejection(for: projection.resource.machine) == nil } + if snapshot.type == .browser, let raw = snapshot.browser?.urlString, + URL(string: raw)?.path == "/vnc.html" { + return policy.rejection(for: nil) == nil + } + return true + } + + /// Validate the complete saved layout before clearing panels or adopting a + /// binding. Startup may adopt a saved owner; an already bound workspace may + /// never be rebound as a side effect of restoring foreign display views. + func acceptsRestoredSession(_ snapshot: SessionWorkspaceSnapshot) -> Bool { + let savedOwner = Self.restoredCloudVMBinding(from: snapshot.cloudVM).map { SurfaceMachineID.cloud($0.vmID) } + if let owner = surfaceOwnershipPolicy.cloudMachine, savedOwner != owner { return false } + let policy = SurfaceOwnershipPolicy(cloudMachine: savedOwner ?? surfaceOwnershipPolicy.cloudMachine) + let records = snapshot.surfaceProjections ?? [] + if !records.isEmpty, policy.rejection(for: records.map(\.resource)) != nil { return false } + let byPanel = Dictionary(records.map { ($0.panelID, $0) }, uniquingKeysWith: { first, _ in first }) + return snapshot.panels.allSatisfy { acceptsRestoredPanel($0, projection: byPanel[$0.id], policy: policy) } + } +} diff --git a/Sources/Surfaces/Workspace+SurfaceCatalog.swift b/Sources/Surfaces/Workspace+SurfaceCatalog.swift index 36d39d75450b..b6b716a9d657 100644 --- a/Sources/Surfaces/Workspace+SurfaceCatalog.swift +++ b/Sources/Surfaces/Workspace+SurfaceCatalog.swift @@ -31,7 +31,13 @@ extension Workspace { /// Persisted projections for this workspace: remote resources only. Local panes are /// re-registered by the hooks when the restored pane is created. var surfaceProjectionRecordsForSession: [SurfaceProjectionRecord]? { - let records = SurfaceCatalog.shared.projectionRecords(forWorkspace: id).filter { !$0.resource.machine.isLocal } + var records = SurfaceCatalog.shared.projectionRecords(forWorkspace: id).filter { !$0.resource.machine.isLocal } + let recorded = Set(records.map(\.panelID)) + for (panelID, panel) in panels where !recorded.contains(panelID) { + if let resource = (panel as? BrowserPanel)?.cloudAccess.resourceID, !resource.machine.isLocal { + records.append(SurfaceProjectionRecord(panelID: panelID, resource: resource)) + } + } return records.isEmpty ? nil : records } diff --git a/Sources/Surfaces/Workspace+SurfaceOwnership.swift b/Sources/Surfaces/Workspace+SurfaceOwnership.swift index 2d115e9923f0..0991e2efe67b 100644 --- a/Sources/Surfaces/Workspace+SurfaceOwnership.swift +++ b/Sources/Surfaces/Workspace+SurfaceOwnership.swift @@ -13,6 +13,7 @@ extension Workspace { let catalog = catalog ?? SurfaceCatalog.shared guard panels[panelID] != nil else { return nil } if let machine = catalog.machineOwningPanel(panelID), !machine.isLocal { return machine } + if let resource = (panels[panelID] as? DeferredBrowserPanel)?.sessionPanelSnapshot.browser?.cloudResource { return resource.machine } if let reservation = cloudPendingCreations[panelID] { return reservation.machine } if activeRemoteTerminalSurfaceIds.contains(panelID), let machine = remoteConfiguration?.managedCloudVMID { diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift index 7a9b2b671238..1fa3816d18b1 100644 --- a/Sources/Workspace.swift +++ b/Sources/Workspace.swift @@ -217,6 +217,7 @@ extension Workspace { startupRestoreCommitOwner: WorkspaceTerminalStartupRestoreCommitOwner = .workspaceTopology, deferBrowserPanels: Bool = false ) -> [UUID: UUID] { + guard acceptsRestoredSession(snapshot) else { return [:] } sessionRestoreLayoutSuppressionDepth += 1 defer { sessionRestoreLayoutSuppressionDepth = max(sessionRestoreLayoutSuppressionDepth - 1, 0) @@ -748,7 +749,7 @@ extension Workspace { forwardHistoryURLStrings: historySnapshot.forwardHistoryURLStrings, transparentBackground: browserPanel.sessionSnapshotTransparentBackground, diffViewerToken: diffViewerComponents?.token, - diffViewerRequestPath: diffViewerComponents?.requestPath + diffViewerRequestPath: diffViewerComponents?.requestPath, cloudResource: browserPanel.cloudResourceForSession ) } else if let deferredPanel = panel as? DeferredBrowserPanel { // A deferred panel already owns the exact persisted browser DTO; @@ -974,6 +975,7 @@ extension Workspace { } @discardableResult func restoreClosedPanel(_ entry: ClosedPanelHistoryEntry) -> UUID? { + guard acceptsRestoredPanel(entry.snapshot, projection: entry.projection) else { return nil } // Inert nested scaffolds must not enter the user-split terminal repair path. let wasProgrammaticSplit = isProgrammaticSplit isProgrammaticSplit = true @@ -1558,8 +1560,10 @@ extension Workspace { restorableAgentIndex: RestorableAgentSessionIndex? = nil, cloudProjectionRecord: SurfaceProjectionRecord? = nil ) -> UUID? { - guard !isRetiredFromOwningTabManager else { return nil } - let snapshot = Self.repairedLegacyHermesSessionPanelSnapshot(snapshot, workspaceId: snapshotWorkspaceId ?? id) + guard !isRetiredFromOwningTabManager, + acceptsRestoredPanel(snapshot, projection: cloudProjectionRecord) else { return nil } + var snapshot = Self.repairedLegacyHermesSessionPanelSnapshot(snapshot, workspaceId: snapshotWorkspaceId ?? id) + if let resource = cloudProjectionRecord?.resource, !resource.machine.isLocal { snapshot.browser?.cloudResource = resource } if cloudProjectionRecord?.resource.machine.isLocal == false, let cloudProjectionRecord, let restoredCloudPanelID = reserveRestoredCloudTerminalPane( @@ -5033,7 +5037,7 @@ final class Workspace: Identifiable, ObservableObject, FilePreviewTabMetadataHos setPreferredBrowserProfileID(browserPanel.profileID) } - private func syncBrowserAudioMuteStateForPanel(_ panelId: UUID, browserPanel: BrowserPanel? = nil) { + func syncBrowserAudioMuteStateForPanel(_ panelId: UUID, browserPanel: BrowserPanel? = nil) { guard let browserPanel = browserPanel ?? self.browserPanel(for: panelId), let tabId = surfaceIdFromPanelId(panelId), let tab = bonsplitController.tab(tabId), @@ -5462,7 +5466,7 @@ final class Workspace: Identifiable, ObservableObject, FilePreviewTabMetadataHos return true } - private func insertionIndexToRight(of anchorTabId: TabID, inPane paneId: PaneID) -> Int { + func insertionIndexToRight(of anchorTabId: TabID, inPane paneId: PaneID) -> Int { let tabs = bonsplitController.tabs(inPane: paneId) guard let anchorIndex = tabs.firstIndex(where: { $0.id == anchorTabId }) else { return tabs.count } let pinnedCount = tabs.reduce(into: 0) { count, tab in @@ -9593,7 +9597,7 @@ final class Workspace: Identifiable, ObservableObject, FilePreviewTabMetadataHos initialDividerPosition: CGFloat? = nil, websiteDataStore: WKWebsiteDataStore? = nil ) -> BrowserPanel? { - guard !isRetiredFromOwningTabManager else { return nil } + guard !isRetiredFromOwningTabManager, acceptsUnownedBrowserURL(initialRequest?.url ?? url) else { return nil } // No local browser surfaces in a remote tmux mirror workspace (it is a // 1:1 view of a tmux session). See ``newBrowserSurface(inPane:)``. if isRemoteTmuxMirror { return nil } @@ -9720,7 +9724,7 @@ final class Workspace: Identifiable, ObservableObject, FilePreviewTabMetadataHos bypassRemoteProxy: Bool = false, websiteDataStore: WKWebsiteDataStore? = nil ) -> BrowserPanel? { - guard !isRetiredFromOwningTabManager else { return nil } + guard !isRetiredFromOwningTabManager, acceptsUnownedBrowserURL(initialRequest?.url ?? url) else { return nil } // A remote tmux mirror workspace is a 1:1 view of a tmux session (which // has no browser concept). A local browser tab here would be an orphan // that the mirror's rebuild() never reconciles, breaking the 1:1 @@ -12585,27 +12589,6 @@ final class Workspace: Identifiable, ObservableObject, FilePreviewTabMetadataHos _ = reorderSurface(panelId: newPanel.id, toIndex: targetIndex) } - @discardableResult - func duplicateBrowserToRight(panelId: UUID, focus: Bool = true) -> BrowserPanel? { - guard let anchorTabId = surfaceIdFromPanelId(panelId), - let paneId = paneId(forPanelId: panelId), - let browser = browserPanel(for: panelId) else { return nil } - let targetIndex = insertionIndexToRight(of: anchorTabId, inPane: paneId) - guard let newPanel = newBrowserSurface( - inPane: paneId, - url: browser.currentURLForTabDuplication, - focus: focus, - preferredProfileID: browser.profileID, - chromeVisibility: browser.chromeVisibility, - bypassRemoteProxy: browser.bypassesRemoteWorkspaceProxyForTabDuplication, - websiteDataStore: browser.explicitEphemeralWebsiteDataStoreForSibling - ) else { return nil } - newPanel.setMuted(browser.isMuted) - syncBrowserAudioMuteStateForPanel(newPanel.id, browserPanel: newPanel) - _ = reorderSurface(panelId: newPanel.id, toIndex: targetIndex, focus: focus) - return newPanel - } - private func promptRenamePanel(tabId: TabID) { guard let panelId = panelIdFromSurfaceId(tabId), let panel = panels[panelId] else { return } diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index 3189221a633e..665685c9920b 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -740,6 +740,8 @@ 32E508E45F3E7B0832FB99EA /* CloudDiagnosticsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B0EDC90EFCD3A415400895 /* CloudDiagnosticsView.swift */; }; 5910F6B0C9704B8DB8607318 /* CloudDirectoryLifecycleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EFB28A7B5DF48ED8EF9CFB6 /* CloudDirectoryLifecycleTests.swift */; }; F3FAB983EE90414CAA90F970 /* CloudDirectoryTestFixture.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFC7BA3C7C834D20B3173D36 /* CloudDirectoryTestFixture.swift */; }; + 33E9D640ED2646A38673776D /* CloudDisplayCatalogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B7DF8AE123341DEBA066731 /* CloudDisplayCatalogTests.swift */; }; + AE0DE65C11FB4EE491A1C031 /* CloudDisplayCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F99243AE43E429485F65C00 /* CloudDisplayCoordinator.swift */; }; C10D0A200000000000000001 /* CloudDomainsCLIIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10D0A200000000000000002 /* CloudDomainsCLIIntegrationTests.swift */; }; 9D231969015CF81D75332F5E /* CloudEnvDelivery.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9E59E46A6A91958EE22451 /* CloudEnvDelivery.swift */; }; 674394DB769239AFBC7BCC09 /* CloudEnvDeliveryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52233C6D60C4E409CDF7E321 /* CloudEnvDeliveryTests.swift */; }; @@ -748,6 +750,7 @@ C3CD2FF9F6F1489C8EDD0C7B /* CloudFeatureFlagTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBF28FA7AB264C1AA7821248 /* CloudFeatureFlagTests.swift */; }; 00034889FEF18E9AE0AFDEEC /* CloudFileDelivery.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA32812C9009969F4AEA2380 /* CloudFileDelivery.swift */; }; 57BC801370636A4E2C1B31B7 /* CloudFileDeliveryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8FED5F6485C390988555CDA /* CloudFileDeliveryTests.swift */; }; + B844B6722D714B769A32A05F /* CloudGuestDisplaySnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1257ADDE94A4743AA16C593 /* CloudGuestDisplaySnapshot.swift */; }; F7925DA2C43A12723611F26E /* CloudGuestURLRoutingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 935A3D4E1C9FB4A5E94E2770 /* CloudGuestURLRoutingTests.swift */; }; 902ED2709F9B61E9E4B59A24 /* CloudGuestURLService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EAC5EBE3FB2C24E1F29B55C /* CloudGuestURLService.swift */; }; C5616E96DCEC5AD0263BB9A4 /* CloudGuestURLTestContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAA597EF5D62E0147E16DCAF /* CloudGuestURLTestContainer.swift */; }; @@ -956,6 +959,7 @@ A12481100000000000000001 /* CloudTreeNodeContentSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = A12481100000000000000002 /* CloudTreeNodeContentSnapshot.swift */; }; 722B1E308414B078A24CB62A /* CloudTreeNSOutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4C74749FA907F50D653EF65 /* CloudTreeNSOutlineView.swift */; }; 45F29E8BB64E4EE4BF204AC7 /* CloudTreeOneMachineManyWorkspacesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99BEA5821E46487A94B4E8B8 /* CloudTreeOneMachineManyWorkspacesTests.swift */; }; + 99A348B1D074417BB9DC59D1 /* CloudTreeOutlineView+Displays.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12747253A744B63A30EFA27 /* CloudTreeOutlineView+Displays.swift */; }; E639673A813D2A174CE3BAE1 /* CloudTreeOutlineView+MachineMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 412FF88FEF5819465A8157DE /* CloudTreeOutlineView+MachineMenu.swift */; }; A1308600000000000000000C /* CloudTreeOutlineView+MachineOrdering.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1308600000000000000000B /* CloudTreeOutlineView+MachineOrdering.swift */; }; FB6DB41BCE7B4EC987BF2F0B /* CloudTreeOutlineView+Organization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A6150D65B1F43F683253998 /* CloudTreeOutlineView+Organization.swift */; }; @@ -1455,11 +1459,13 @@ C7A509000000000000000002 /* CmuxTopSnapshotScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A509000000000000000001 /* CmuxTopSnapshotScopeTests.swift */; }; D9F800000000000000000001 /* CmuxTuiRemoteRouting.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9F800000000000000000002 /* CmuxTuiRemoteRouting.swift */; }; D9F800000000000000000003 /* CmuxTuiRemoteRouting.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9F800000000000000000002 /* CmuxTuiRemoteRouting.swift */; }; + 3CBCDC9381A949A7A6A239D0 /* CmuxTuiSnapshotParser+Displays.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B54F1BEB0C84A15BAA2A261 /* CmuxTuiSnapshotParser+Displays.swift */; }; 622E567166E1DC2C614E4E7F /* CmuxTuiSnapshotParser+Placement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3349CDE9CB71FA353EA004D3 /* CmuxTuiSnapshotParser+Placement.swift */; }; 3B2BDBC03043E0C3CC4A308E /* CmuxTuiSnapshotParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C28D6AEDE6B00F5C633ED2A8 /* CmuxTuiSnapshotParser.swift */; }; 3312CA2585324CF9B4CA9D9A /* CmuxTuiSurfaceProvider+AgentNaming.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3528E32A29B49E99C636F0B /* CmuxTuiSurfaceProvider+AgentNaming.swift */; }; 81A1621623F83B61D4C5FFA9 /* CmuxTuiSurfaceProvider+AttachmentRecovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D8BE7D3234A1F44E2A1C52B /* CmuxTuiSurfaceProvider+AttachmentRecovery.swift */; }; 5633045E23E44075B22C08CA /* CmuxTuiSurfaceProvider+CloseTerminal.swift in Sources */ = {isa = PBXBuildFile; fileRef = E655249E82FC412B9EC2CD21 /* CmuxTuiSurfaceProvider+CloseTerminal.swift */; }; + FB8AB3FDE68E4ABC9C72E6CB /* CmuxTuiSurfaceProvider+Displays.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED5ABA995260445394F81C79 /* CmuxTuiSurfaceProvider+Displays.swift */; }; AA92C99C529DB9A24064AF48 /* CmuxTuiSurfaceProvider+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE30231700141C6F370FA63 /* CmuxTuiSurfaceProvider+Environment.swift */; }; A12093000000000000000002 /* CmuxTuiSurfaceProvider+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = A12093000000000000000001 /* CmuxTuiSurfaceProvider+Error.swift */; }; 7F81637D3CD3867FAA502D1E /* CmuxTuiSurfaceProvider+FileDelivery.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9E053B7E9AE536377476E4B /* CmuxTuiSurfaceProvider+FileDelivery.swift */; }; @@ -2759,6 +2765,7 @@ A5001601 /* SentryHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001600 /* SentryHelper.swift */; }; FE003106 /* SessionAgentPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE003006 /* SessionAgentPresentation.swift */; }; A9F200000000000000000008 /* SessionAgentSessionPanelSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F100000000000000000008 /* SessionAgentSessionPanelSnapshot.swift */; }; + F39581155797426199C03CFF /* SessionBrowserPanelSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19FE2238F1F44DE98D6501DB /* SessionBrowserPanelSnapshot.swift */; }; A74770010000000000000005 /* SessionConfigFrameEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = A74770010000000000000006 /* SessionConfigFrameEntry.swift */; }; A74770010000000000000007 /* SessionConfigFrameRing.swift in Sources */ = {isa = PBXBuildFile; fileRef = A74770010000000000000008 /* SessionConfigFrameRing.swift */; }; C71500020000000000000001 /* SessionContentWidthModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C71500010000000000000001 /* SessionContentWidthModifier.swift */; }; @@ -3167,6 +3174,7 @@ F11347000000000000000001 /* SurfaceCatalog+CloudPorts.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11347000000000000000002 /* SurfaceCatalog+CloudPorts.swift */; }; E71A3B8404534D5699AA2E9B /* SurfaceCatalog+CloudRenameSubmission.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A4A80857B8A4530BD5BC498 /* SurfaceCatalog+CloudRenameSubmission.swift */; }; 7E02669974964356A46288EB /* SurfaceCatalog+CloudWorkspaceProjection.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB74882A29884E378E36D645 /* SurfaceCatalog+CloudWorkspaceProjection.swift */; }; + D06E330E6EE545A1868EC3BC /* SurfaceCatalog+Displays.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FAF7EE2FAF54ADDB5F47C37 /* SurfaceCatalog+Displays.swift */; }; DD3A13AD76F5196C5FC8C52A /* SurfaceCatalog+Groups.swift in Sources */ = {isa = PBXBuildFile; fileRef = 660286CE22B478E887361407 /* SurfaceCatalog+Groups.swift */; }; C2DF9D7B62A64277A656C356 /* SurfaceCatalog+NameAuthority.swift in Sources */ = {isa = PBXBuildFile; fileRef = 798894FD4FBD473BAA1D9DF6 /* SurfaceCatalog+NameAuthority.swift */; }; 0203C17ACF8348739A755953 /* SurfaceCatalog+Ownership.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F302DCC7D244E48146BA1F /* SurfaceCatalog+Ownership.swift */; }; @@ -3767,8 +3775,10 @@ A6AC73020000000000000001 /* Workspace+AgentLifecycle.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6AC73020000000000000002 /* Workspace+AgentLifecycle.swift */; }; 5A0420D5417940C6815B1F9E /* Workspace+AgentResumeOwnership.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F89ED4F392842F08A685C41 /* Workspace+AgentResumeOwnership.swift */; }; 8777E0018777E0018777E001 /* Workspace+AttentionFlashRouting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8777E0028777E0028777E002 /* Workspace+AttentionFlashRouting.swift */; }; + 078F2E4F32DC4B2AA5A84A4A /* Workspace+BrowserDuplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05CFF7107D1F48BC805549F3 /* Workspace+BrowserDuplication.swift */; }; CA52B0150000000000000000 /* Workspace+CanvasLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA52C0150000000000000000 /* Workspace+CanvasLayout.swift */; }; D86840000000000000000007 /* Workspace+ClosedPanelLayoutRestore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86840000000000000000008 /* Workspace+ClosedPanelLayoutRestore.swift */; }; + E6F61F31711B4439A8C4C4A8 /* Workspace+CloudDisplayOwnership.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14F78AF3938A4C58A82EA25C /* Workspace+CloudDisplayOwnership.swift */; }; 4F2E42F6D7164832B891F079 /* Workspace+CloudLayoutProjection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 085BB53F552B4C6A8728941E /* Workspace+CloudLayoutProjection.swift */; }; C11323010000000000000001 /* Workspace+CloudManualMirror.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11323020000000000000001 /* Workspace+CloudManualMirror.swift */; }; 65221C81A129236D52702D99 /* Workspace+CloudPaneRouting.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5797FE9CD70513E4FFAC6EB /* Workspace+CloudPaneRouting.swift */; }; @@ -4806,6 +4816,8 @@ E6B0EDC90EFCD3A415400895 /* CloudDiagnosticsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudDiagnosticsView.swift"; sourceTree = ""; }; 9EFB28A7B5DF48ED8EF9CFB6 /* CloudDirectoryLifecycleTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CloudDirectoryLifecycleTests.swift; sourceTree = ""; }; FFC7BA3C7C834D20B3173D36 /* CloudDirectoryTestFixture.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CloudDirectoryTestFixture.swift; sourceTree = ""; }; + 6B7DF8AE123341DEBA066731 /* CloudDisplayCatalogTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudDisplayCatalogTests.swift"; sourceTree = ""; }; + 2F99243AE43E429485F65C00 /* CloudDisplayCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudDisplayCoordinator.swift"; sourceTree = ""; }; C10D0A200000000000000002 /* CloudDomainsCLIIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudDomainsCLIIntegrationTests.swift; sourceTree = ""; }; DD9E59E46A6A91958EE22451 /* CloudEnvDelivery.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CloudEnvDelivery.swift; sourceTree = ""; }; 52233C6D60C4E409CDF7E321 /* CloudEnvDeliveryTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CloudEnvDeliveryTests.swift; sourceTree = ""; }; @@ -4814,6 +4826,7 @@ EBF28FA7AB264C1AA7821248 /* CloudFeatureFlagTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudFeatureFlagTests.swift; sourceTree = ""; }; DA32812C9009969F4AEA2380 /* CloudFileDelivery.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CloudFileDelivery.swift; sourceTree = ""; }; F8FED5F6485C390988555CDA /* CloudFileDeliveryTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CloudFileDeliveryTests.swift; sourceTree = ""; }; + F1257ADDE94A4743AA16C593 /* CloudGuestDisplaySnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudGuestDisplaySnapshot.swift"; sourceTree = ""; }; 935A3D4E1C9FB4A5E94E2770 /* CloudGuestURLRoutingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudGuestURLRoutingTests.swift"; sourceTree = ""; }; 8EAC5EBE3FB2C24E1F29B55C /* CloudGuestURLService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudGuestURLService.swift"; sourceTree = ""; }; DAA597EF5D62E0147E16DCAF /* CloudGuestURLTestContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudGuestURLTestContainer.swift"; sourceTree = ""; }; @@ -5024,6 +5037,7 @@ A12481100000000000000002 /* CloudTreeNodeContentSnapshot.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CloudTreeNodeContentSnapshot.swift; sourceTree = ""; }; A4C74749FA907F50D653EF65 /* CloudTreeNSOutlineView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CloudTreeNSOutlineView.swift; sourceTree = ""; }; 99BEA5821E46487A94B4E8B8 /* CloudTreeOneMachineManyWorkspacesTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CloudTreeOneMachineManyWorkspacesTests.swift; sourceTree = ""; }; + E12747253A744B63A30EFA27 /* CloudTreeOutlineView+Displays.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudTreeOutlineView+Displays.swift"; sourceTree = ""; }; 412FF88FEF5819465A8157DE /* CloudTreeOutlineView+MachineMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudTreeOutlineView+MachineMenu.swift"; sourceTree = ""; }; A1308600000000000000000B /* CloudTreeOutlineView+MachineOrdering.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sidebar/CloudTreeOutlineView+MachineOrdering.swift"; sourceTree = ""; }; 5A6150D65B1F43F683253998 /* CloudTreeOutlineView+Organization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sidebar/CloudTreeOutlineView+Organization.swift"; sourceTree = ""; }; @@ -5423,11 +5437,13 @@ C7A5090000000000000005A1 /* CmuxTopSnapshotScopeCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxTopSnapshotScopeCacheTests.swift; sourceTree = ""; }; C7A509000000000000000001 /* CmuxTopSnapshotScopeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxTopSnapshotScopeTests.swift; sourceTree = ""; }; D9F800000000000000000002 /* CmuxTuiRemoteRouting.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CmuxTuiRemoteRouting.swift; sourceTree = ""; }; + 1B54F1BEB0C84A15BAA2A261 /* CmuxTuiSnapshotParser+Displays.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CmuxTuiSnapshotParser+Displays.swift"; sourceTree = ""; }; 3349CDE9CB71FA353EA004D3 /* CmuxTuiSnapshotParser+Placement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CmuxTuiSnapshotParser+Placement.swift"; sourceTree = ""; }; C28D6AEDE6B00F5C633ED2A8 /* CmuxTuiSnapshotParser.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CmuxTuiSnapshotParser.swift; sourceTree = ""; }; E3528E32A29B49E99C636F0B /* CmuxTuiSurfaceProvider+AgentNaming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CmuxTuiSurfaceProvider+AgentNaming.swift"; sourceTree = ""; }; 4D8BE7D3234A1F44E2A1C52B /* CmuxTuiSurfaceProvider+AttachmentRecovery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CmuxTuiSurfaceProvider+AttachmentRecovery.swift"; sourceTree = ""; }; E655249E82FC412B9EC2CD21 /* CmuxTuiSurfaceProvider+CloseTerminal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CmuxTuiSurfaceProvider+CloseTerminal.swift"; sourceTree = ""; }; + ED5ABA995260445394F81C79 /* CmuxTuiSurfaceProvider+Displays.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CmuxTuiSurfaceProvider+Displays.swift"; sourceTree = ""; }; 4BE30231700141C6F370FA63 /* CmuxTuiSurfaceProvider+Environment.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "CmuxTuiSurfaceProvider+Environment.swift"; sourceTree = ""; }; A12093000000000000000001 /* CmuxTuiSurfaceProvider+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CmuxTuiSurfaceProvider+Error.swift"; sourceTree = ""; }; F9E053B7E9AE536377476E4B /* CmuxTuiSurfaceProvider+FileDelivery.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "CmuxTuiSurfaceProvider+FileDelivery.swift"; sourceTree = ""; }; @@ -6708,6 +6724,7 @@ A5001600 /* SentryHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryHelper.swift; sourceTree = ""; }; FE003006 /* SessionAgentPresentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionAgentPresentation.swift; sourceTree = ""; }; A9F100000000000000000008 /* SessionAgentSessionPanelSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionAgentSessionPanelSnapshot.swift; sourceTree = ""; }; + 19FE2238F1F44DE98D6501DB /* SessionBrowserPanelSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionBrowserPanelSnapshot.swift"; sourceTree = ""; }; A74770010000000000000006 /* SessionConfigFrameEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionConfigFrameEntry.swift; sourceTree = ""; }; A74770010000000000000008 /* SessionConfigFrameRing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionConfigFrameRing.swift; sourceTree = ""; }; C71500010000000000000001 /* SessionContentWidthModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/SessionContentWidthModifier.swift; sourceTree = ""; }; @@ -7108,6 +7125,7 @@ F11347000000000000000002 /* SurfaceCatalog+CloudPorts.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "SurfaceCatalog+CloudPorts.swift"; sourceTree = ""; }; 4A4A80857B8A4530BD5BC498 /* SurfaceCatalog+CloudRenameSubmission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SurfaceCatalog+CloudRenameSubmission.swift"; sourceTree = ""; }; AB74882A29884E378E36D645 /* SurfaceCatalog+CloudWorkspaceProjection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SurfaceCatalog+CloudWorkspaceProjection.swift"; sourceTree = ""; }; + 9FAF7EE2FAF54ADDB5F47C37 /* SurfaceCatalog+Displays.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SurfaceCatalog+Displays.swift"; sourceTree = ""; }; 660286CE22B478E887361407 /* SurfaceCatalog+Groups.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "SurfaceCatalog+Groups.swift"; sourceTree = ""; }; 798894FD4FBD473BAA1D9DF6 /* SurfaceCatalog+NameAuthority.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SurfaceCatalog+NameAuthority.swift"; sourceTree = ""; }; B3F302DCC7D244E48146BA1F /* SurfaceCatalog+Ownership.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SurfaceCatalog+Ownership.swift"; sourceTree = ""; }; @@ -7704,8 +7722,10 @@ A6AC73020000000000000002 /* Workspace+AgentLifecycle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workspace+AgentLifecycle.swift"; sourceTree = ""; }; 5F89ED4F392842F08A685C41 /* Workspace+AgentResumeOwnership.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workspace+AgentResumeOwnership.swift"; sourceTree = ""; }; 8777E0028777E0028777E002 /* Workspace+AttentionFlashRouting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workspace+AttentionFlashRouting.swift"; sourceTree = ""; }; + 05CFF7107D1F48BC805549F3 /* Workspace+BrowserDuplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workspace+BrowserDuplication.swift"; sourceTree = ""; }; CA52C0150000000000000000 /* Workspace+CanvasLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workspace+CanvasLayout.swift"; sourceTree = ""; }; D86840000000000000000008 /* Workspace+ClosedPanelLayoutRestore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workspace+ClosedPanelLayoutRestore.swift"; sourceTree = ""; }; + 14F78AF3938A4C58A82EA25C /* Workspace+CloudDisplayOwnership.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workspace+CloudDisplayOwnership.swift"; sourceTree = ""; }; 085BB53F552B4C6A8728941E /* Workspace+CloudLayoutProjection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workspace+CloudLayoutProjection.swift"; sourceTree = ""; }; C11323020000000000000001 /* Workspace+CloudManualMirror.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Workspace+CloudManualMirror.swift"; sourceTree = ""; }; C5797FE9CD70513E4FFAC6EB /* Workspace+CloudPaneRouting.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "Workspace+CloudPaneRouting.swift"; sourceTree = ""; }; @@ -8446,6 +8466,11 @@ F11347000000000000000008 /* SurfaceCatalog+CloudBinding.swift */, 7867A0020000000000000001 /* SurfaceProjectionRestoreStore.swift */, 7305B43543CF4AF29D9966C8 /* Workspace+SurfaceOwnership.swift */, + 9FAF7EE2FAF54ADDB5F47C37 /* SurfaceCatalog+Displays.swift */, + ED5ABA995260445394F81C79 /* CmuxTuiSurfaceProvider+Displays.swift */, + 1B54F1BEB0C84A15BAA2A261 /* CmuxTuiSnapshotParser+Displays.swift */, + 05CFF7107D1F48BC805549F3 /* Workspace+BrowserDuplication.swift */, + 14F78AF3938A4C58A82EA25C /* Workspace+CloudDisplayOwnership.swift */, 45B5556BE218466AB7FBA814 /* SurfaceTransferRejection.swift */, D65E70E1AE6E42F890ADD7EF /* SurfaceResourcePlacement.swift */, B7A47B5093E24AAD8DE28313 /* SurfaceOwnershipPolicy.swift */, @@ -8699,6 +8724,9 @@ 46E4DB2770E4B0CBF4C388E5 /* CloudTreeStyleGalleryWindow.swift */, 627B5FB396A1BF22407E8B45 /* SurfaceResourceDragRegistry.swift */, A1B2C3D4E5F6071827364950 /* CloudMachineSurfacePresentation.swift */, + E12747253A744B63A30EFA27 /* CloudTreeOutlineView+Displays.swift */, + F1257ADDE94A4743AA16C593 /* CloudGuestDisplaySnapshot.swift */, + 2F99243AE43E429485F65C00 /* CloudDisplayCoordinator.swift */, 1CE0A82A4E20E446A21A97BB /* CloudTreeNode.swift */, 2BBC2E2213D2FB598EC3751B /* CloudTreeNodeBuilder+Ports.swift */, 8EDC6087D41DC9E2271BC886 /* CloudMachineNotificationGate.swift */, @@ -10384,6 +10412,7 @@ A74770010000000000000008 /* SessionConfigFrameRing.swift */, A74770010000000000000004 /* SessionDisplaySnapshot.swift */, A5001611 /* SessionPersistence.swift */, + 19FE2238F1F44DE98D6501DB /* SessionBrowserPanelSnapshot.swift */, 87590000000000000000000A /* MainThreadHangCaptureStore.swift */, 87590000000000000000000C /* MainThreadHangSampleRunner.swift */, 875900000000000000000004 /* MainThreadHangWatchdog.swift */, @@ -11960,6 +11989,7 @@ A6D3896BB28B4549B3D33A82 /* CloudPortForwardAddressReuseTests.swift */, 9A1261600000000000000005 /* CloudDesktopWebSocketTests.swift */, 9A1261600000000000000003 /* CloudDesktopAccessTests.swift */, + 6B7DF8AE123341DEBA066731 /* CloudDisplayCatalogTests.swift */, A6D3896BB28B4549B3D33A83 /* CloudPortAccessLateCoordinatorTests.swift */, C51600000000000000000002 /* CLIExecInheritedSignalStateTests.swift */, ); @@ -13138,10 +13168,12 @@ 010CCC9F785F4023BAE36375 /* CloudDiagnosticFailure.swift in Sources */, E60073D256A0581591C1DC89 /* CloudDiagnosticReport.swift in Sources */, 32E508E45F3E7B0832FB99EA /* CloudDiagnosticsView.swift in Sources */, + AE0DE65C11FB4EE491A1C031 /* CloudDisplayCoordinator.swift in Sources */, 9D231969015CF81D75332F5E /* CloudEnvDelivery.swift in Sources */, F8111B8E2214BA971C8EB6E0 /* CloudErrorCopy.swift in Sources */, 5EDE6BFD28BE4BD58EF84889 /* CloudFeatureAvailabilityObserver.swift in Sources */, 00034889FEF18E9AE0AFDEEC /* CloudFileDelivery.swift in Sources */, + B844B6722D714B769A32A05F /* CloudGuestDisplaySnapshot.swift in Sources */, 902ED2709F9B61E9E4B59A24 /* CloudGuestURLService.swift in Sources */, D0110080000000000000000A /* CloudHubConnection.swift in Sources */, E58AA0A516704DF28E795531 /* CloudHubConnector.swift in Sources */, 7A0CE100000000000000070E /* CloudHubDialing.swift in Sources */, @@ -13270,6 +13302,7 @@ 2B54959C9FCE4581A68D5B5E /* CloudTreeNodeBuilder+Signatures.swift in Sources */, A12481100000000000000001 /* CloudTreeNodeContentSnapshot.swift in Sources */, 722B1E308414B078A24CB62A /* CloudTreeNSOutlineView.swift in Sources */, + 99A348B1D074417BB9DC59D1 /* CloudTreeOutlineView+Displays.swift in Sources */, E639673A813D2A174CE3BAE1 /* CloudTreeOutlineView+MachineMenu.swift in Sources */, A1308600000000000000000C /* CloudTreeOutlineView+MachineOrdering.swift in Sources */, FB6DB41BCE7B4EC987BF2F0B /* CloudTreeOutlineView+Organization.swift in Sources */, @@ -13448,11 +13481,13 @@ C7A501000000000000000002 /* CmuxTopSnapshot.swift in Sources */, C7A508000000000000000002 /* CmuxTopSnapshotScopeCache.swift in Sources */, D9F800000000000000000001 /* CmuxTuiRemoteRouting.swift in Sources */, + 3CBCDC9381A949A7A6A239D0 /* CmuxTuiSnapshotParser+Displays.swift in Sources */, 622E567166E1DC2C614E4E7F /* CmuxTuiSnapshotParser+Placement.swift in Sources */, 3B2BDBC03043E0C3CC4A308E /* CmuxTuiSnapshotParser.swift in Sources */, 3312CA2585324CF9B4CA9D9A /* CmuxTuiSurfaceProvider+AgentNaming.swift in Sources */, 81A1621623F83B61D4C5FFA9 /* CmuxTuiSurfaceProvider+AttachmentRecovery.swift in Sources */, 5633045E23E44075B22C08CA /* CmuxTuiSurfaceProvider+CloseTerminal.swift in Sources */, + FB8AB3FDE68E4ABC9C72E6CB /* CmuxTuiSurfaceProvider+Displays.swift in Sources */, AA92C99C529DB9A24064AF48 /* CmuxTuiSurfaceProvider+Environment.swift in Sources */, A12093000000000000000002 /* CmuxTuiSurfaceProvider+Error.swift in Sources */, 7F81637D3CD3867FAA502D1E /* CmuxTuiSurfaceProvider+FileDelivery.swift in Sources */, @@ -14311,6 +14346,7 @@ A5001601 /* SentryHelper.swift in Sources */, FE003106 /* SessionAgentPresentation.swift in Sources */, A9F200000000000000000008 /* SessionAgentSessionPanelSnapshot.swift in Sources */, + F39581155797426199C03CFF /* SessionBrowserPanelSnapshot.swift in Sources */, A74770010000000000000005 /* SessionConfigFrameEntry.swift in Sources */, A74770010000000000000007 /* SessionConfigFrameRing.swift in Sources */, C71500020000000000000001 /* SessionContentWidthModifier.swift in Sources */, @@ -14584,6 +14620,7 @@ F11347000000000000000001 /* SurfaceCatalog+CloudPorts.swift in Sources */, E71A3B8404534D5699AA2E9B /* SurfaceCatalog+CloudRenameSubmission.swift in Sources */, 7E02669974964356A46288EB /* SurfaceCatalog+CloudWorkspaceProjection.swift in Sources */, + D06E330E6EE545A1868EC3BC /* SurfaceCatalog+Displays.swift in Sources */, DD3A13AD76F5196C5FC8C52A /* SurfaceCatalog+Groups.swift in Sources */, C2DF9D7B62A64277A656C356 /* SurfaceCatalog+NameAuthority.swift in Sources */, 0203C17ACF8348739A755953 /* SurfaceCatalog+Ownership.swift in Sources */, @@ -15052,8 +15089,10 @@ A6AC73020000000000000001 /* Workspace+AgentLifecycle.swift in Sources */, 5A0420D5417940C6815B1F9E /* Workspace+AgentResumeOwnership.swift in Sources */, 8777E0018777E0018777E001 /* Workspace+AttentionFlashRouting.swift in Sources */, + 078F2E4F32DC4B2AA5A84A4A /* Workspace+BrowserDuplication.swift in Sources */, CA52B0150000000000000000 /* Workspace+CanvasLayout.swift in Sources */, D86840000000000000000007 /* Workspace+ClosedPanelLayoutRestore.swift in Sources */, + E6F61F31711B4439A8C4C4A8 /* Workspace+CloudDisplayOwnership.swift in Sources */, 4F2E42F6D7164832B891F079 /* Workspace+CloudLayoutProjection.swift in Sources */, C11323010000000000000001 /* Workspace+CloudManualMirror.swift in Sources */, 65221C81A129236D52702D99 /* Workspace+CloudPaneRouting.swift in Sources */, @@ -15752,6 +15791,7 @@ 9A1261600000000000000006 /* CloudDesktopWebSocketTests.swift in Sources */, 5910F6B0C9704B8DB8607318 /* CloudDirectoryLifecycleTests.swift in Sources */, F3FAB983EE90414CAA90F970 /* CloudDirectoryTestFixture.swift in Sources */, + 33E9D640ED2646A38673776D /* CloudDisplayCatalogTests.swift in Sources */, C10D0A200000000000000001 /* CloudDomainsCLIIntegrationTests.swift in Sources */, 674394DB769239AFBC7BCC09 /* CloudEnvDeliveryTests.swift in Sources */, C3CD2FF9F6F1489C8EDD0C7B /* CloudFeatureFlagTests.swift in Sources */, diff --git a/cmuxTests/CloudDesktopAccessTests.swift b/cmuxTests/CloudDesktopAccessTests.swift index c99b76bcfd5d..3c90656b5593 100644 --- a/cmuxTests/CloudDesktopAccessTests.swift +++ b/cmuxTests/CloudDesktopAccessTests.swift @@ -14,6 +14,79 @@ import WebKit @MainActor @Suite(.serialized, .timeLimit(.minutes(1))) struct CloudDesktopAccessTests { + @Test("Route readiness drives cold open and cached retry without a SwiftUI phase update") + func automaticallyNavigatesAfterRouteReadiness() async throws { + let ready = CloudLinkFirstValue() + var starts = 0 + let model = CloudPortAccessModel(target: .init(host: "10.0.0.7", port: 6901), coordinator: nil, + wake: {}, startForward: { _ in + _ = await ready.result + starts += 1 + return 46901 + }, stopForward: {}, route: .loopback) + let state = CloudBrowserAccessState() + state.configure(model: model, url: URL(string: "http://10.0.0.7:6901/vnc.html")!) + var navigations: [URL] = [] + state.automaticallyNavigate { navigations.append($0) } + model.connect() + #expect(navigations.isEmpty, "A slow route cannot invent a completed navigation") + ready.resolve(true) + #expect(await wait { navigations.count == 1 }) + let url = try #require(navigations.first) + state.didCommit(url: url) + state.didFinish(url: url) + state.desktopConnectionDidChange(url: url, isConnected: true) + state.retry() + #expect(await wait { starts == 2 && navigations.count == 2 }) + #expect(navigations[1] == url) + state.leave() + model.retry() + #expect(await wait { starts == 3 }) + #expect(navigations.count == 2, "A retired document cannot navigate after leaving Cloud") + await model.retire() + } + + @Test("A finished noVNC page without RFB readiness reaches a retryable deadline") + func desktopReadinessDeadline() async throws { + let clock = CloudCommandDeadlineClock() + let model = CloudPortAccessModel(target: .init(host: "10.0.0.7", port: 6901), coordinator: nil, + wake: {}, startForward: { _ in 46901 }, stopForward: {}, route: .loopback) + let state = CloudBrowserAccessState(clock: clock) + state.configure(model: model, url: URL(string: "http://10.0.0.7:6901/vnc.html")!) + await clock.waitUntilSleeping() + model.connect() + #expect(await wait { model.isReady }) + let url = try #require(state.nextURL()) + state.didCommit(url: url) + state.didFinish(url: url) + clock.advance(by: .seconds(46)) + #expect(await wait { state.showsFailureAlert }) + #expect(!state.desktopConnected) + state.retry() + #expect(state.failureMessage == nil) + state.leave() + await model.retire() + } + + @Test("A cancelled old navigation cannot cancel a newer display attempt") + func cancellationUsesNavigationIdentity() async throws { + let old = NSObject(), current = NSObject() + let model = CloudPortAccessModel(target: .init(host: "10.0.0.7", port: 6901), coordinator: nil, + wake: {}, startForward: { _ in 46901 }, stopForward: {}, route: .loopback) + let state = CloudBrowserAccessState() + state.configure(model: model, url: URL(string: "http://10.0.0.7:6901/vnc.html")!) + model.connect() + #expect(await wait { model.isReady }) + let url = try #require(state.nextURL()) + state.didStart(url: url, navigationID: ObjectIdentifier(current)) + state.didCancel(navigationID: ObjectIdentifier(old)) + #expect(state.error == nil) + state.didCancel(navigationID: ObjectIdentifier(current)) + #expect(state.error != nil && !state.showsPage) + state.leave() + await model.retire() + } + @Test("A connected noVNC document is ready even before WebKit's finish callback", arguments: [6901, 6902]) func desktopConnectionCompletesReadiness(port: Int) async throws { let model = CloudPortAccessModel(target: .init(host: "10.0.0.7", port: port), coordinator: nil, diff --git a/cmuxTests/CloudDisplayCatalogTests.swift b/cmuxTests/CloudDisplayCatalogTests.swift new file mode 100644 index 000000000000..40741eee3aeb --- /dev/null +++ b/cmuxTests/CloudDisplayCatalogTests.swift @@ -0,0 +1,121 @@ +import Foundation +import Testing + +#if canImport(cmux_DEV) +@testable import cmux_DEV +#elseif canImport(cmux) +@testable import cmux +#endif + +@MainActor +@Suite("Independent guest displays", .timeLimit(.minutes(1))) +struct CloudDisplayCatalogTests { + private let initial = #"{"version":1,"canCreate":true,"displays":[{"id":"display:1","number":1,"port":6901,"state":"running"}]}"# + private let created = #"{"version":1,"canCreate":true,"displays":[{"id":"display:1","number":1,"port":6901,"state":"running"},{"id":"display:2","number":2,"port":6902,"state":"running"}],"created":"display:2"}"# + + @Test("A lost creation reply replays its receipt rather than allocating another display") + func creationRetryKeepsRequestIdentity() async throws { + var creates: [String] = [] + let service = CloudDisplayCoordinator { command, _ in + if command.hasSuffix(" list") { return .init(exitCode: 0, stdout: initial, stderr: "") } + creates.append(command) + if creates.count == 1 { throw URLError(.networkConnectionLost) } + return .init(exitCode: 0, stdout: created, stderr: "") + } + await service.refresh() + #expect(service.canCreate) + do { _ = try await service.create(); Issue.record("Lost response must fail") } catch {} + let result = try await service.create() + #expect(creates.count == 2 && creates[0] == creates[1]) + #expect(result.displays.count == 2) + #expect(service.snapshot?.displays.first?.id == "display:1") + } + + @Test("Account/provider retirement prevents a delayed display reply from publishing") + func retiredCreationCannotPublish() async throws { + let started = CloudLinkFirstValue() + let response = CloudLinkFirstValue() + let service = CloudDisplayCoordinator { command, _ in + if command.hasSuffix(" list") { return .init(exitCode: 0, stdout: initial, stderr: "") } + started.resolve(true) + _ = await response.result + return .init(exitCode: 0, stdout: created, stderr: "") + } + await service.refresh() + let operation = Task { try await service.create() } + _ = await started.result + service.stop() + response.resolve(true) + do { _ = try await operation.value; Issue.record("Retired creation succeeded") } catch {} + #expect(!service.canCreate) + #expect(service.snapshot?.displays.count == 1) + } + + @Test("Unsupported images never expose a fake creation action") + func unsupportedGuest() async { + let service = CloudDisplayCoordinator { _, _ in .init(exitCode: 127, stdout: "", stderr: "missing") } + await service.refresh() + #expect(!service.canCreate && service.snapshot == nil) + } + + @Test("Display IDs and connection targets are scoped to the authenticated VM") + func independentTargets() throws { + let snapshot = try decode(""" + {"version":1,"canCreate":true,"displays":[ + {"id":"display:1","number":1,"port":6901,"state":"running"}, + {"id":"display:2","number":2,"port":6902,"state":"running"}],"created":"display:2"} + """) + let a = snapshot.displays.map { $0.resource(on: .cloud("a"), address: "10.0.0.7") } + let b = snapshot.displays.map { $0.resource(on: .cloud("b"), address: "10.0.0.7") } + #expect(a[0].id != a[1].id) + #expect(a[1].id != b[1].id, "Reused IP, labels and display IDs cannot confer VM ownership") + #expect(URL(string: try #require(a[0].url))?.port == 6901) + #expect(URL(string: try #require(a[1].url))?.port == 6902) + #expect(SurfaceOwnershipPolicy(cloudMachine: .cloud("a")).rejection(for: b.map(\.id)) != nil) + #expect(SurfaceOwnershipPolicy(cloudMachine: .cloud("b")).rejection(for: a.map(\.id)) != nil) + #expect(SurfaceOwnershipPolicy(cloudMachine: .cloud("a")).rejection(for: [a[0].id, a[0].id, a[1].id]) == nil) + let refreshed = CmuxTuiSurfaceProvider.withPrivateBrowserURL(a[1], privateAddress: "10.0.0.8") + #expect(refreshed.id == a[1].id) + #expect(URL(string: try #require(refreshed.url))?.port == 6902) + } + + @Test("Guest targets cannot alias display one or invent another host", arguments: [ + #"{"id":"display:2","number":2,"port":6901,"state":"running"}"#, + #"{"id":"display:1","number":2,"port":6902,"state":"running"}"#, + #"{"id":"display:99","number":99,"port":6999,"state":"running"}"# + ]) + func rejectsInvalidGuestIdentity(row: String) { + #expect(throws: (any Error).self) { + try decode("{\"version\":1,\"canCreate\":true,\"displays\":[\(row)]}") + } + } + + @Test("A daemon pointer without a discovered target cannot open the first desktop") + func unknownDisplayHasNoTarget() { + let resource = CmuxTuiSnapshotParser.display(machine: .cloud("a"), key: "display:2") + if case .unsupported = CloudPortRoutePlan.plan(resource: resource, privateAddress: "10.0.0.7") {} else { + Issue.record("Unknown display aliased the default desktop") + } + } + + @Test("Refreshing placements preserves each guest-issued target") + func mergingViewPointersKeepsTarget() throws { + let snapshot = try decode(""" + {"version":1,"canCreate":true,"displays":[ + {"id":"display:1","number":1,"port":6901,"state":"running"}, + {"id":"display:2","number":2,"port":6902,"state":"running"}]} + """) + let pool = snapshot.displays.map { $0.resource(on: .cloud("a"), address: "10.0.0.7") } + var pointer = CmuxTuiSnapshotParser.display(machine: .cloud("a"), key: "display:2") + pointer.remoteViews = [.init(tabID: "tab-a", workspace: .init(id: "ws-a", name: "same", index: 0, focused: false))] + let resources = CmuxTuiSnapshotParser.mergingDisplays(pool: pool, parsed: [pointer]) + #expect(resources.count == 2) + let second = try #require(resources.first { $0.id == pointer.id }) + #expect(second.port == 6902 && second.url == pool[1].url) + #expect(second.remoteViews == pointer.remoteViews) + } + + private func decode(_ raw: String) throws -> CloudGuestDisplaySnapshot { + try CloudGuestDisplaySnapshot(data: Data(raw.utf8)) + } +} diff --git a/cmuxTests/CloudSurfaceDragFeedbackTests.swift b/cmuxTests/CloudSurfaceDragFeedbackTests.swift index e27a4bd5c200..3ec477ac62c6 100644 --- a/cmuxTests/CloudSurfaceDragFeedbackTests.swift +++ b/cmuxTests/CloudSurfaceDragFeedbackTests.swift @@ -46,9 +46,9 @@ struct CloudSurfaceDragFeedbackTests { } } - @Test("Rebinding after hover is rejected before mutation") - func destinationChangesBeforeDrop() throws { - let fixture = try CloudSurfaceDragFixture(kind: .terminal) + @Test("Rebinding after hover is rejected before mutation", arguments: SurfaceResourceKind.allCases) + func destinationChangesBeforeDrop(kind: SurfaceResourceKind) throws { + let fixture = try CloudSurfaceDragFixture(kind: kind) defer { fixture.finish() } fixture.workspace.cloudVMBinding = WorkspaceCloudVMBinding(vmID: "a", isBase: false) let router = fixture.router() diff --git a/cmuxTests/CloudSurfaceMoveOwnershipTests.swift b/cmuxTests/CloudSurfaceMoveOwnershipTests.swift index 476519054cac..c8891d998005 100644 --- a/cmuxTests/CloudSurfaceMoveOwnershipTests.swift +++ b/cmuxTests/CloudSurfaceMoveOwnershipTests.swift @@ -13,6 +13,42 @@ import Testing @MainActor @Suite("Cloud surface mutation boundaries", .serialized) struct CloudSurfaceMoveOwnershipTests { + @Test("Per-workspace Docks reject foreign displays before detaching", arguments: ["a", "b"]) + func foreignDisplayDockMove(owner: String) async throws { + try await AppContextSerialGate.withExclusiveAppContext { + let fixture = try VaultPaneAppFixture() + defer { fixture.tearDown() } + let source = fixture.workspace + let pane = try #require(source.bonsplitController.allPaneIds.first) + let browser = try #require(source.newBrowserSurface(inPane: pane, focus: false)) + let display = resource(machine: owner, kind: .display) + let catalog = SurfaceCatalog.shared + catalog.upsert(display) + catalog.record(.init(resource: display.id, workspaceID: source.id, panelID: browser.id)) + defer { catalog.remove(display.id) } + let target = fixture.manager.addWorkspace(title: "same label", select: false) + target.cloudVMBinding = WorkspaceCloudVMBinding(vmID: owner == "a" ? "b" : "a", isBase: false) + let dock = target.requiredDockSplitForTesting + let dockPane = try #require(dock.bonsplitController.allPaneIds.first) + let tab = try #require(source.surfaceIdFromPanelId(browser.id)) + let before = Set(dock.panels.keys) + #expect(!fixture.appDelegate.canMoveSurfaceIntoDock(sourceTabId: tab.uuid, destinationDock: dock)) + #expect(!fixture.appDelegate.moveSurfaceIntoDock(sourceTabId: tab.uuid, destinationDock: dock, + destination: .insert(targetPane: dockPane, targetIndex: 0))) + #expect(source.panels[browser.id] != nil) + #expect(Set(dock.panels.keys) == before) + target.cloudVMBinding = WorkspaceCloudVMBinding(vmID: owner, isBase: false) + let detached = try #require(source.detachSurface(panelId: browser.id)) + #expect(dock.attachDetachedSurface(detached, inPane: dockPane, focus: false) == browser.id) + #expect(dock.machineOwningSurface(browser.id) == .cloud(owner)) + let captured = dock.sessionSnapshot(includeScrollback: false) + #expect(captured.panels.first(where: { $0.id == browser.id })?.browser?.cloudResource == display.id) + target.cloudVMBinding = WorkspaceCloudVMBinding(vmID: owner == "a" ? "b" : "a", isBase: false) + #expect(dock.restoreSessionSnapshot(captured).isEmpty) + #expect(dock.panels[browser.id] != nil) + } + } + @Test("Duplicating a display preserves its VM and independent view identity", arguments: [false, true]) func displayDuplicationRetainsOwner(offline: Bool) async throws { try await AppContextSerialGate.withExclusiveAppContext { @@ -41,15 +77,15 @@ struct CloudSurfaceMoveOwnershipTests { } } - @Test("Foreign Cloud terminal, browser and display moves leave both workspaces intact", arguments: SurfaceResourceKind.allCases) - func foreignCloudMove(kind: SurfaceResourceKind) async throws { + @Test("Foreign Cloud terminal, browser and display moves leave both workspaces intact", arguments: SurfaceResourceKind.allCases, ["a", "b"]) + func foreignCloudMove(kind: SurfaceResourceKind, owner: String) async throws { try await AppContextSerialGate.withExclusiveAppContext { let fixture = try VaultPaneAppFixture() defer { fixture.tearDown() } let source = fixture.workspace let target = fixture.manager.addWorkspace(title: "same name", select: false) defer { target.teardownAllPanels() } - target.cloudVMBinding = WorkspaceCloudVMBinding(vmID: "b", isBase: false) + target.cloudVMBinding = WorkspaceCloudVMBinding(vmID: owner == "a" ? "b" : "a", isBase: false) let sourcePane = try #require(source.bonsplitController.allPaneIds.first) let panelID: UUID if kind == .terminal { @@ -57,7 +93,8 @@ struct CloudSurfaceMoveOwnershipTests { } else { panelID = try #require(source.newBrowserSurface(inPane: sourcePane, url: URL(string: "about:blank"), focus: false)).id } - let resource = resource(machine: "a", kind: kind) + var resource = resource(machine: owner, kind: kind) + resource.id.key = "same-resource-id" let catalog = SurfaceCatalog.shared catalog.upsert(resource) catalog.record(SurfaceProjection(resource: resource.id, workspaceID: source.id, panelID: panelID)) diff --git a/cmuxTests/CloudTreeOneMachineManyWorkspacesTests.swift b/cmuxTests/CloudTreeOneMachineManyWorkspacesTests.swift index 7611e1eb7b67..4f31cc13932b 100644 --- a/cmuxTests/CloudTreeOneMachineManyWorkspacesTests.swift +++ b/cmuxTests/CloudTreeOneMachineManyWorkspacesTests.swift @@ -677,7 +677,7 @@ struct CloudTreeOneMachineManyWorkspacesTests { "machine:brave-otter/terminals", "machine:brave-otter/terminals/placeholder", ], "the screens sit above the Terminals section") - guard case .displaysPool(_, let count) = try #require(tree.first { $0.id == "machine:brave-otter/displays" }).kind else { + guard case .displaysPool(_, let count, _) = try #require(tree.first { $0.id == "machine:brave-otter/displays" }).kind else { Issue.record("expected the Displays group"); return } #expect(count == 2) diff --git a/tests/test_cloud_display_catalog.py b/tests/test_cloud_display_catalog.py new file mode 100644 index 000000000000..17ae342ef489 --- /dev/null +++ b/tests/test_cloud_display_catalog.py @@ -0,0 +1,132 @@ +"""Execute the guest display catalog; no guest VM or GUI is mutated.""" +import concurrent.futures +import importlib.machinery +import importlib.util +import json +import sys +from pathlib import Path +import tempfile +import unittest +from unittest import mock +import uuid + + +sys.dont_write_bytecode = True +loader = importlib.machinery.SourceFileLoader( + "cmux_display", str(Path(__file__).resolve().parents[1] / + "web/services/vms/images/devbox/desktop/cmux-display")) +spec = importlib.util.spec_from_loader(loader.name, loader) +display = importlib.util.module_from_spec(spec) +loader.exec_module(display) + + +class CloudDisplayCatalogTests(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.addCleanup(self.directory.cleanup) + self.root = Path(self.directory.name) + + def catalog(self, vm="a", occupied=lambda _: False): + return display.DisplayCatalog(self.root / vm, occupied=occupied) + + def test_second_display_is_distinct_and_keeps_first_receipt(self): + catalog = self.catalog() + first_request, second_request = str(uuid.uuid4()), str(uuid.uuid4()) + first = catalog.allocate(first_request) + second = catalog.allocate(second_request) + self.assertEqual((first, second), (2, 3)) + self.assertEqual(catalog.allocate(first_request), first) + self.assertEqual(display.descriptor(1)["port"], 6901) + self.assertNotEqual(display.descriptor(first)["port"], display.descriptor(second)["port"]) + + def test_vm_catalogs_can_use_identical_display_ids_independently(self): + a, b = self.catalog("a"), self.catalog("b") + request = str(uuid.uuid4()) + self.assertEqual(a.allocate(request), b.allocate(request)) + a.allocate(str(uuid.uuid4())) + self.assertEqual(a.numbers(), [2, 3]) + self.assertEqual(b.numbers(), [2]) + + def test_reconnect_restores_stable_resource_and_retry_identity(self): + request = str(uuid.uuid4()) + first = self.catalog() + number = first.allocate(request) + restored = self.catalog() + self.assertEqual(restored.allocate(request), number) + self.assertEqual(restored.numbers(), [number]) + + def test_concurrent_retry_allocates_one_resource(self): + catalog = self.catalog() + request = str(uuid.uuid4()) + with concurrent.futures.ThreadPoolExecutor(max_workers=8) as pool: + results = list(pool.map(catalog.allocate, [request] * 32)) + self.assertEqual(set(results), {2}) + self.assertEqual(catalog.numbers(), [2]) + + def test_existing_guest_ports_and_x_sockets_are_not_adopted(self): + catalog = self.catalog(occupied=lambda number: number in (2, 3)) + self.assertEqual(catalog.allocate(str(uuid.uuid4())), 4) + + def test_capacity_failure_does_not_replace_existing_displays(self): + catalog = self.catalog() + for _ in range(display.MAX_DISPLAYS - 1): + catalog.allocate(str(uuid.uuid4())) + before = catalog.path.read_bytes() + with self.assertRaises(ValueError): + catalog.allocate(str(uuid.uuid4())) + self.assertEqual(catalog.path.read_bytes(), before) + + def test_unknown_request_cannot_mutate_catalog(self): + catalog = self.catalog() + for request in (None, "", "../../other-vm", "$(touch /tmp/not-allowed)"): + with self.assertRaises((ValueError, TypeError, AttributeError)): + catalog.allocate(request) + self.assertFalse(catalog.path.exists()) + + def test_corrupt_persisted_identity_fails_closed(self): + catalog = self.catalog() + catalog.path.write_text(json.dumps({"version": 1, "displays": [ + {"number": 1, "request": str(uuid.uuid4())}]})) + with self.assertRaises(ValueError): + self.catalog() + + def test_supervision_keeps_display_and_session_environment_separate(self): + catalog = self.catalog() + service = display.DisplayService(catalog, self.root / "runtime") + environments = [] + + def run(_command, **options): + environments.append(options["env"]) + + with mock.patch.object(display.subprocess, "run", side_effect=run), mock.patch.object(display, "ready", return_value=True): + try: + first = service.handle({"action": "create", "request": str(uuid.uuid4())}) + second = service.handle({"action": "create", "request": str(uuid.uuid4())}) + self.assertEqual(first["created"], "display:2") + self.assertEqual(second["created"], "display:3") + self.assertEqual(len(second["displays"]), 3) + self.assertEqual([env["DISPLAY"] for env in environments], [":2", ":3"]) + self.assertNotEqual(environments[0]["CMUX_DESKTOP_RUNTIME_DIR"], environments[1]["CMUX_DESKTOP_RUNTIME_DIR"]) + for env in environments: + self.assertNotIn("DBUS_SESSION_BUS_ADDRESS", env) + self.assertNotIn("NOTIFY_SOCKET", env) + finally: + service.shutdown.set() + + def test_start_failure_retains_resource_and_replay_receipt(self): + service = display.DisplayService(self.catalog(), self.root / "runtime") + request = str(uuid.uuid4()) + with mock.patch.object(display.subprocess, "run", side_effect=OSError("starter unavailable")), mock.patch.object(display, "ready", return_value=True): + try: + result = service.handle({"action": "create", "request": request}) + retried = service.handle({"action": "create", "request": request}) + self.assertEqual(result["error"], "display_start_failed") + self.assertEqual(retried["created"], result["created"]) + self.assertEqual(len(retried["displays"]), 2) + self.assertEqual(retried["displays"][0]["id"], "display:1") + finally: + service.shutdown.set() + + +if __name__ == "__main__": + unittest.main() diff --git a/web/scripts/devbox-image-common.ts b/web/scripts/devbox-image-common.ts index 8719f46245cb..af8ee6fb9fd1 100644 --- a/web/scripts/devbox-image-common.ts +++ b/web/scripts/devbox-image-common.ts @@ -195,6 +195,7 @@ export const devboxDesktopDir = path.join(devboxDir, "desktop"); export const DEVBOX_DESKTOP_FILES = [ "WALLPAPER.md", "cmux-desktop-boot", + "cmux-display", "cmux-desktop.service", "desktop-env.sh", "ghostty-cmux.desktop", @@ -227,6 +228,7 @@ export const DEVBOX_DESKTOP_INSTALLS: readonly DevboxDesktopInstall[] = [ { source: "desktop/wallpaper.jpg", target: "/usr/share/backgrounds/cmux/wallpaper.jpg", mode: 0o644 }, { source: "desktop/start-vnc.sh", target: "/usr/local/bin/start-vnc.sh", mode: 0o755 }, { source: "desktop/cmux-desktop-boot", target: "/usr/local/bin/cmux-desktop-boot", mode: 0o755 }, + { source: "desktop/cmux-display", target: "/usr/local/bin/cmux-display", mode: 0o755 }, { source: "desktop/cmux-desktop.service", target: "/etc/systemd/system/cmux-desktop.service", mode: 0o644 }, { source: "desktop/desktop-env.sh", target: "/etc/cmux/desktop-env.sh", mode: 0o644 }, ]; diff --git a/web/services/vms/images/devbox/Dockerfile b/web/services/vms/images/devbox/Dockerfile index 47dad1fb15dd..667250caadb0 100644 --- a/web/services/vms/images/devbox/Dockerfile +++ b/web/services/vms/images/devbox/Dockerfile @@ -209,9 +209,10 @@ COPY desktop/tint2rc /etc/cmux/tint2rc COPY desktop/wallpaper.jpg /usr/share/backgrounds/cmux/wallpaper.jpg COPY desktop/start-vnc.sh /usr/local/bin/start-vnc.sh COPY desktop/cmux-desktop-boot /usr/local/bin/cmux-desktop-boot +COPY desktop/cmux-display /usr/local/bin/cmux-display COPY desktop/cmux-desktop.service /etc/systemd/system/cmux-desktop.service COPY desktop/desktop-env.sh /etc/cmux/desktop-env.sh -RUN chmod 0755 /usr/local/bin/start-vnc.sh /usr/local/bin/cmux-desktop-boot \ +RUN chmod 0755 /usr/local/bin/start-vnc.sh /usr/local/bin/cmux-desktop-boot /usr/local/bin/cmux-display \ && bash -n /usr/local/bin/start-vnc.sh && sh -n /usr/local/bin/cmux-desktop-boot && sh -n /etc/cmux/desktop-env.sh \ && mkdir -p /etc/cmux/icons \ && cp /opt/google/chrome/product_logo_128.png /etc/cmux/icons/google-chrome.png \ diff --git a/web/services/vms/images/devbox/desktop/cmux-desktop-boot b/web/services/vms/images/devbox/desktop/cmux-desktop-boot index 66248d311513..ce0376af729d 100644 --- a/web/services/vms/images/devbox/desktop/cmux-desktop-boot +++ b/web/services/vms/images/devbox/desktop/cmux-desktop-boot @@ -15,6 +15,11 @@ CMUX_DESKTOP_RUNTIME_DIR="${CMUX_DESKTOP_RUNTIME_DIR:-/run/cmux-desktop}" export CMUX_DESKTOP_RUNTIME_DIR mkdir -p "$HOME/.config/google-chrome" 2>/dev/null touch "$HOME/.config/google-chrome/First Run" 2>/dev/null +# One guest-owned control socket creates and restores independent displays. +# Old images without the capability retain their existing desktop unchanged. +if [ -x /usr/local/bin/cmux-display ]; then + /usr/local/bin/cmux-display serve >>"$CMUX_DESKTOP_RUNTIME_DIR/display-control.log" 2>&1 & +fi while true; do bash /usr/local/bin/start-vnc.sh || true sleep 30 diff --git a/web/services/vms/images/devbox/desktop/cmux-display b/web/services/vms/images/devbox/desktop/cmux-display new file mode 100755 index 000000000000..dc89ba7d11ef --- /dev/null +++ b/web/services/vms/images/devbox/desktop/cmux-display @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 +"""Guest-owned display catalog and supervisor; invoked by the desktop service. + +The existing :1 desktop remains owned by start-vnc.sh. Additional records each +own an X server, session bus, window manager and noVNC listener. The control +socket is local to this guest and only accessible to its work user. +""" +import argparse +import fcntl +import http.client +import json +import os +from pathlib import Path +import shutil +import socket +import socketserver +import subprocess +import tempfile +import threading +import time +import uuid + +MAX_DISPLAYS = 16 +STARTER = "/usr/local/bin/start-vnc.sh" + + +def descriptor(number, state="running"): + return {"id": f"display:{number}", "number": number, + "port": 6900 + number, "state": state} + + +def listening(port): + try: + with socket.create_connection(("127.0.0.1", port), timeout=0.3): + return True + except OSError: + return False + + +def ready(number): + """A bound HTTP port alone does not prove that an X/RFB server exists.""" + try: + with socket.create_connection(("127.0.0.1", 5900 + number), timeout=0.5) as rfb: + version = b"" + while len(version) < 12: + part = rfb.recv(12 - len(version)) + if not part: + return False + version += part + if not version.startswith(b"RFB "): + return False + connection = http.client.HTTPConnection("127.0.0.1", 6900 + number, timeout=0.5) + try: + connection.request("HEAD", "/vnc.html") + return connection.getresponse().status == 200 + finally: + connection.close() + except (OSError, http.client.HTTPException): + return False + + +class DisplayCatalog: + """Serializes allocation and persists request receipts before starting work.""" + + def __init__(self, directory, occupied=None): + self.directory = Path(directory) + self.directory.mkdir(mode=0o700, parents=True, exist_ok=True) + self.path = self.directory / "catalog.json" + self.lock = threading.RLock() + self.occupied = occupied or (lambda n: Path(f"/tmp/.X11-unix/X{n}").exists() + or listening(5900 + n) or listening(6900 + n)) + self.records = [] + if self.path.exists(): + data = json.loads(self.path.read_text()) + if data.get("version") != 1: + raise ValueError("unsupported display catalog version") + seen_numbers, seen_requests = set(), set() + for record in data["displays"]: + number, request = record["number"], record["request"] + if type(number) is not int or not 2 <= number <= MAX_DISPLAYS: + raise ValueError("invalid display number") + if str(uuid.UUID(request)) != request or number in seen_numbers or request in seen_requests: + raise ValueError("invalid display identity") + seen_numbers.add(number) + seen_requests.add(request) + self.records.append({"number": number, "request": request}) + + def numbers(self): + with self.lock: + return sorted(record["number"] for record in self.records) + + def allocate(self, request): + if not isinstance(request, str) or str(uuid.UUID(request)) != request: + raise ValueError("invalid request identity") + with self.lock: + for record in self.records: + if record["request"] == request: + return record["number"] + used = set(self.numbers()) + number = next((n for n in range(2, MAX_DISPLAYS + 1) + if n not in used and not self.occupied(n)), None) + if number is None: + raise ValueError("display capacity exhausted") + records = self.records + [{"number": number, "request": request}] + fd, temporary = tempfile.mkstemp(prefix="catalog-", dir=self.directory) + try: + with os.fdopen(fd, "w") as output: + json.dump({"version": 1, "displays": records}, output) + output.flush() + os.fsync(output.fileno()) + os.replace(temporary, self.path) + finally: + if os.path.exists(temporary): + os.unlink(temporary) + self.records = records + return number + + +class DisplayService: + def __init__(self, catalog, runtime, starter=STARTER): + self.catalog = catalog + self.runtime = Path(runtime) + self.starter = starter + self.lock = threading.Lock() + self.jobs = {} + self.states = {} + self.shutdown = threading.Event() + + def start(self, number): + with self.lock: + if number in self.jobs: + return self.jobs[number] + finished = threading.Event() + self.jobs[number] = finished + self.states[number] = "launching" + threading.Thread(target=self.supervise, args=(number, finished), daemon=True).start() + return finished + + def supervise(self, number, finished): + runtime = self.runtime / str(number) + runtime.mkdir(mode=0o700, parents=True, exist_ok=True) + environment = dict(os.environ, DISPLAY=f":{number}", CMUX_DESKTOP_RUNTIME_DIR=str(runtime)) + # Each display starts a separate session bus; it must not inherit :1's. + for key in ("NOTIFY_SOCKET", "DBUS_SESSION_BUS_ADDRESS", "DBUS_SESSION_BUS_PID", + "AT_SPI_BUS_ADDRESS", "AT_SPI_BUS"): + environment.pop(key, None) + while not self.shutdown.is_set(): + try: + subprocess.run(["bash", self.starter], env=environment, stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + timeout=50, check=True) + state = "running" if ready(number) else "unavailable" + except (OSError, subprocess.SubprocessError): + state = "unavailable" + with self.lock: + self.states[number] = state + finished.set() + # This is supervision, not readiness: a failed listener is reported + # immediately, and the next pass heals only this display's processes. + self.shutdown.wait(30) + + def snapshot(self): + with self.lock: + return {"version": 1, "canCreate": len(self.jobs) < MAX_DISPLAYS - 1, + "displays": [descriptor(1, "running" if ready(1) else "unavailable")] + + [descriptor(n, self.states.get(n, "launching")) for n in self.catalog.numbers()]} + + def handle(self, request): + if request.get("action") == "list": + return self.snapshot() + if request.get("action") != "create": + raise ValueError("unsupported display action") + number = self.catalog.allocate(request.get("request")) + self.start(number).wait(55) + result = self.snapshot() + result["created"] = f"display:{number}" + if self.states.get(number) != "running": + result["error"] = "display_start_failed" + return result + + +def serve(runtime): + runtime = Path(runtime) + runtime.mkdir(mode=0o700, parents=True, exist_ok=True) + with (runtime / "display-control.lock").open("w") as lock: + try: + fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) + except BlockingIOError: + return + if not all(shutil.which(name) for name in ("websockify", "openbox", "dbus-launch")): + raise ValueError("display runtime unavailable") + if not (shutil.which("Xvnc") or shutil.which("Xtigervnc")): + raise ValueError("display runtime unavailable") + service = DisplayService(DisplayCatalog(Path.home() / ".cmux" / "displays"), runtime / "displays") + for number in service.catalog.numbers(): + service.start(number) + path = runtime / "display-control.sock" + if path.exists(): + path.unlink() + + class Handler(socketserver.StreamRequestHandler): + def handle(self): + self.connection.settimeout(60) + try: + raw = self.rfile.readline(4097) + if len(raw) > 4096: + raise ValueError("display request too large") + result = service.handle(json.loads(raw)) + except (ValueError, KeyError, TypeError, OSError): + result = {"error": "display_request_failed"} + try: + self.wfile.write(json.dumps(result).encode() + b"\n") + except OSError: + pass # The persisted receipt remains replayable after disconnect. + + class Server(socketserver.ThreadingUnixStreamServer): + daemon_threads = True + + previous_umask = os.umask(0o077) + try: + with Server(str(path), Handler) as server: + os.umask(previous_umask) + server.serve_forever() + finally: + os.umask(previous_umask) + service.shutdown.set() + path.unlink(missing_ok=True) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("action", choices=["list", "create", "serve"]) + parser.add_argument("--request-id") + args = parser.parse_args() + runtime = os.environ.get("CMUX_DESKTOP_RUNTIME_DIR", "/run/cmux-desktop") + if args.action == "serve": + serve(runtime) + return 0 + if args.action == "create" and not args.request_id: + parser.error("create requires --request-id (retain it when retrying)") + try: + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client: + client.settimeout(60) + client.connect(str(Path(runtime) / "display-control.sock")) + client.sendall(json.dumps({"action": args.action, "request": args.request_id}).encode() + b"\n") + with client.makefile("rb") as stream: + result = json.loads(stream.readline(65536)) + except (OSError, ValueError): + result = {"error": "display_runtime_unavailable"} + print(json.dumps(result)) + return 1 if "error" in result else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/web/services/vms/images/devbox/desktop/start-vnc.sh b/web/services/vms/images/devbox/desktop/start-vnc.sh index edcb7c412466..c0f09ea95b2e 100644 --- a/web/services/vms/images/devbox/desktop/start-vnc.sh +++ b/web/services/vms/images/devbox/desktop/start-vnc.sh @@ -47,12 +47,18 @@ unset NOTIFY_SOCKET DISPLAY="${DISPLAY:-:1}" export DISPLAY +DISPLAY_NUMBER="${DISPLAY#:}" +case "$DISPLAY_NUMBER" in ''|*[!0-9]*) echo "Invalid display number" >&2; exit 1;; esac +if [ "$DISPLAY_NUMBER" -lt 1 ] || [ "$DISPLAY_NUMBER" -gt 16 ]; then exit 1; fi +RFB_PORT=$((5900 + DISPLAY_NUMBER)) +NOVNC_PORT=$((6900 + DISPLAY_NUMBER)) GEOMETRY="${CMUX_VNC_GEOMETRY:-1440x900}" RUNTIME_DIR="${CMUX_DESKTOP_RUNTIME_DIR:-/run/cmux-desktop}" # Never let an unwritable HOME keep the desktop down: fall back to a per-user # tmp dir for logs and session state. STATE_DIR="$HOME/.cmux" -mkdir -p "$STATE_DIR/desktop-logs" 2>/dev/null || { STATE_DIR="/tmp/cmux-desktop-$(id -u)"; mkdir -p "$STATE_DIR/desktop-logs"; } +if [ "$DISPLAY_NUMBER" -ne 1 ]; then STATE_DIR="$STATE_DIR/displays/$DISPLAY_NUMBER"; fi +mkdir -p "$STATE_DIR/desktop-logs" 2>/dev/null || { STATE_DIR="/tmp/cmux-desktop-$(id -u)-$DISPLAY_NUMBER"; mkdir -p "$STATE_DIR/desktop-logs"; } LOG_DIR="$STATE_DIR/desktop-logs" # The supervisor re-runs this every 30 s, so cap each component log here: a @@ -67,7 +73,19 @@ done VNC_BIN="$(command -v Xvnc || command -v Xtigervnc)" || exit 0 listening() { ss -tln 2>/dev/null | grep -q ":$1 "; } -mine() { pgrep -u "$(id -u)" "$@" >/dev/null 2>&1; } +# Process names alone would reuse :1's window manager and session bus on :2. +# Filter every component by the exact DISPLAY inherited at process creation. +display_pids() { + pgrep -u "$(id -u)" "$@" 2>/dev/null | while read -r pid; do + [ -r "/proc/$pid/environ" ] || continue + if tr '\0' '\n' < "/proc/$pid/environ" 2>/dev/null | grep -Fxq "DISPLAY=$DISPLAY"; then echo "$pid"; fi + done +} +export -f display_pids +mine() { [ -n "$(display_pids "$@")" ]; } +# Refuse a port owned by another session rather than aliasing its screen. +if [ "$DISPLAY_NUMBER" -ne 1 ] && listening "$RFB_PORT" && ! mine -x 'Xvnc|Xtigervnc'; then exit 1; fi +if [ "$DISPLAY_NUMBER" -ne 1 ] && listening "$NOVNC_PORT" && ! mine -f "websockify.*127.0.0.1:$RFB_PORT"; then exit 1; fi # Bounded wait for a listener that has no readiness signal of its own # (websockify): probes the socket table until the port is bound or the # deadline (seconds) passes. Returns the port's state at the deadline. @@ -84,14 +102,14 @@ wait_listening() { # moment it accepts connections, so nothing below talks to X too early. The # descriptor is a FIFO opened read/write here (so neither side blocks on the # open) and read with a deadline. -if ! listening 5901; then +if ! listening "$RFB_PORT"; then ready_fifo="$STATE_DIR/xvnc-ready.fifo" rm -f "$ready_fifo" if mkfifo -m 600 "$ready_fifo" 2>/dev/null && exec {ready_fd}<>"$ready_fifo"; then "$VNC_BIN" "$DISPLAY" \ -geometry "$GEOMETRY" \ -depth 24 \ - -rfbport 5901 \ + -rfbport "$RFB_PORT" \ -localhost \ -SecurityTypes None \ -AlwaysShared \ @@ -104,12 +122,12 @@ if ! listening 5901; then "$VNC_BIN" "$DISPLAY" \ -geometry "$GEOMETRY" \ -depth 24 \ - -rfbport 5901 \ + -rfbport "$RFB_PORT" \ -localhost \ -SecurityTypes None \ -AlwaysShared \ >>"$LOG_DIR/xvnc.log" 2>&1 & - wait_listening 5901 20 || true + wait_listening "$RFB_PORT" 20 || true fi fi @@ -181,7 +199,7 @@ if ! mine -f cmux-desktop-resize-watch; then case $line in *RRScreenChangeNotify*) feh --no-fehbg --bg-fill /usr/share/backgrounds/cmux/wallpaper.jpg >/dev/null 2>&1 || true - pkill -USR1 -U "$(id -u)" -x tint2 >/dev/null 2>&1 || true + display_pids -x tint2 | xargs -r kill -USR1 >/dev/null 2>&1 || true ;; esac done @@ -189,8 +207,8 @@ if ! mine -f cmux-desktop-resize-watch; then fi # noVNC uses a dual-stack listener so either private address can reach the desktop. -if ! listening 6901; then - websockify --web /usr/share/novnc --heartbeat 30 '[::]:6901' 127.0.0.1:5901 \ +if ! listening "$NOVNC_PORT"; then + websockify --web /usr/share/novnc --heartbeat 30 "[::]:$NOVNC_PORT" "127.0.0.1:$RFB_PORT" \ >>"$LOG_DIR/websockify.log" 2>&1 & fi @@ -225,8 +243,8 @@ fi # Repeating it on later passes is harmless; a pass that finds something # down leaves the unit's state to systemd (Restart=always) and this loop. if [ -n "$CMUX_NOTIFY_SOCKET" ] && command -v systemd-notify >/dev/null 2>&1; then - if listening 5901 && wait_listening 6901 10 && [ "$published" = 1 ]; then - NOTIFY_SOCKET="$CMUX_NOTIFY_SOCKET" systemd-notify --ready --status="desktop up on $DISPLAY: RFB 5901 (loopback), noVNC 6901" 2>>"$LOG_DIR/notify.log" || true + if listening "$RFB_PORT" && wait_listening "$NOVNC_PORT" 10 && [ "$published" = 1 ]; then + NOTIFY_SOCKET="$CMUX_NOTIFY_SOCKET" systemd-notify --ready --status="desktop up on $DISPLAY: RFB $RFB_PORT (loopback), noVNC $NOVNC_PORT" 2>>"$LOG_DIR/notify.log" || true fi fi diff --git a/web/tests/vm-devbox-desktop.test.ts b/web/tests/vm-devbox-desktop.test.ts index 30c8d24f0cf3..8ec7b48af7c3 100644 --- a/web/tests/vm-devbox-desktop.test.ts +++ b/web/tests/vm-devbox-desktop.test.ts @@ -138,7 +138,7 @@ describe("devbox desktop layer", () => { }); test("keeps the desktop port contract: RFB 5901 loopback-only, noVNC on 6901", () => { - expect(startVnc).toContain(`-rfbport ${DEVBOX_DESKTOP_RFB_PORT}`); + // Independent target allocation is executed in vm-display-catalog.test.ts. expect(startVnc).toContain("-SecurityTypes None"); expect(startVnc).toContain("-localhost"); // Listener reachability is exercised by verify-devbox-image over both @@ -227,7 +227,6 @@ describe("devbox desktop layer", () => { expect(startVnc).toContain("*RRScreenChangeNotify*"); expect(startVnc).not.toContain("sleep 2"); expect(startVnc).not.toContain("sleep 0.2"); - expect(startVnc).toContain("wait_listening 6901 10"); expect(startVnc).toContain('eval "$(dbus-launch --sh-syntax'); expect(startVnc).toContain('kill -0 "$DBUS_SESSION_BUS_PID"'); expect(startVnc).toContain("/usr/libexec/at-spi-bus-launcher"); diff --git a/web/tests/vm-display-catalog.test.ts b/web/tests/vm-display-catalog.test.ts new file mode 100644 index 000000000000..5fa90482ae13 --- /dev/null +++ b/web/tests/vm-display-catalog.test.ts @@ -0,0 +1,17 @@ +import { expect, test } from "bun:test"; +import { spawnSync } from "node:child_process"; +import path from "node:path"; + +test("guest display catalog allocates independent, replayable resources", () => { + const root = path.resolve(import.meta.dirname, "../.."); + const result = spawnSync("python3", ["-B", "-m", "unittest", "tests/test_cloud_display_catalog.py", "-v"], { + cwd: root, + encoding: "utf8", + timeout: 30_000, + }); + expect({ status: result.status, error: result.error?.message, output: result.stderr }).toEqual({ + status: 0, + error: undefined, + output: expect.stringContaining("OK"), + }); +}); From 329897aac07696673596f5bfac7a19d6d940da97 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 04:48:14 -0700 Subject: [PATCH 03/64] Keep display creation compatible with baked Cloud images --- Sources/Cloud/CloudDisplayCoordinator.swift | 4 +- Sources/Cloud/CloudGuestDisplayScript.swift | 338 ++++++++++++++++++ cmuxTests/CloudDisplayCatalogTests.swift | 4 +- .../desktop => tests/fixtures}/cmux-display | 71 +++- tests/test_cloud_display_catalog.py | 30 +- web/scripts/devbox-image-common.ts | 2 - web/services/vms/images/devbox/Dockerfile | 3 +- .../images/devbox/desktop/cmux-desktop-boot | 5 - .../vms/images/devbox/desktop/start-vnc.sh | 40 +-- web/tests/vm-devbox-desktop.test.ts | 3 +- web/tests/vm-display-catalog.test.ts | 17 - 11 files changed, 442 insertions(+), 75 deletions(-) create mode 100644 Sources/Cloud/CloudGuestDisplayScript.swift rename {web/services/vms/images/devbox/desktop => tests/fixtures}/cmux-display (76%) delete mode 100644 web/tests/vm-display-catalog.test.ts diff --git a/Sources/Cloud/CloudDisplayCoordinator.swift b/Sources/Cloud/CloudDisplayCoordinator.swift index 6d5cd673335b..1bbcb554b9f1 100644 --- a/Sources/Cloud/CloudDisplayCoordinator.swift +++ b/Sources/Cloud/CloudDisplayCoordinator.swift @@ -24,7 +24,7 @@ final class CloudDisplayCoordinator { generation &+= 1 let token = generation do { - let response = try await execute("/usr/local/bin/cmux-display list", 10_000) + let response = try await execute(CloudGuestDisplayScript.command(action: "list"), 10_000) let snapshot = try CloudGuestDisplaySnapshot(data: Data(response.stdout.utf8)) guard token == generation, !Task.isCancelled else { return } self.snapshot = snapshot @@ -48,7 +48,7 @@ final class CloudDisplayCoordinator { // create a second guest display when the first receipt was lost. let task = Task { [weak self, execute] in try Task.checkCancellation() - let response = try await execute("/usr/local/bin/cmux-display create --request-id \(request.uuidString.lowercased())", 65_000) + let response = try await execute(CloudGuestDisplayScript.command(action: "create", requestID: request), 65_000) let snapshot = try CloudGuestDisplaySnapshot(data: Data(response.stdout.utf8)) try Task.checkCancellation() guard let self, self.generation == token else { throw CancellationError() } diff --git a/Sources/Cloud/CloudGuestDisplayScript.swift b/Sources/Cloud/CloudGuestDisplayScript.swift new file mode 100644 index 000000000000..72afce49616e --- /dev/null +++ b/Sources/Cloud/CloudGuestDisplayScript.swift @@ -0,0 +1,338 @@ +import Foundation + +/// Installs and invokes the VM-scoped display helper through the existing authorized exec route. +@MainActor +enum CloudGuestDisplayScript { + private static let source = #""" +#!/usr/bin/env python3 +\"\"\"Guest-owned display catalog and supervisor; invoked by the desktop service. + +The existing :1 desktop remains owned by start-vnc.sh. Additional records each +own an X server, session bus, window manager and noVNC listener. The control +socket is local to this guest and only accessible to its work user. +\"\"\" +import argparse +import fcntl +import http.client +import json +import os +from pathlib import Path +import shutil +import socket +import socketserver +import subprocess +import tempfile +import threading +import time +import uuid + +MAX_DISPLAYS = 16 +STARTER = "/usr/local/bin/start-vnc.sh" + + +def descriptor(number, state="running"): + return {"id": f"display:{number}", "number": number, + "port": 6900 + number, "state": state} + + +def listening(port): + try: + with socket.create_connection(("127.0.0.1", port), timeout=0.3): + return True + except OSError: + return False + + +def ready(number): + \"\"\"A bound HTTP port alone does not prove that an X/RFB server exists.\"\"\" + try: + with socket.create_connection(("127.0.0.1", 5900 + number), timeout=0.5) as rfb: + version = b"" + while len(version) < 12: + part = rfb.recv(12 - len(version)) + if not part: + return False + version += part + if not version.startswith(b"RFB "): + return False + connection = http.client.HTTPConnection("127.0.0.1", 6900 + number, timeout=0.5) + try: + connection.request("HEAD", "/vnc.html") + return connection.getresponse().status == 200 + finally: + connection.close() + except (OSError, http.client.HTTPException): + return False + + +class DisplayCatalog: + \"\"\"Serializes allocation and persists request receipts before starting work.\"\"\" + + def __init__(self, directory, occupied=None): + self.directory = Path(directory) + self.directory.mkdir(mode=0o700, parents=True, exist_ok=True) + self.path = self.directory / "catalog.json" + self.lock = threading.RLock() + self.occupied = occupied or (lambda n: Path(f"/tmp/.X11-unix/X{n}").exists() + or listening(5900 + n) or listening(6900 + n)) + self.records = [] + if self.path.exists(): + data = json.loads(self.path.read_text()) + if data.get("version") != 1: + raise ValueError("unsupported display catalog version") + seen_numbers, seen_requests = set(), set() + for record in data["displays"]: + number, request = record["number"], record["request"] + if type(number) is not int or not 2 <= number <= MAX_DISPLAYS: + raise ValueError("invalid display number") + if str(uuid.UUID(request)) != request or number in seen_numbers or request in seen_requests: + raise ValueError("invalid display identity") + seen_numbers.add(number) + seen_requests.add(request) + self.records.append({"number": number, "request": request}) + + def numbers(self): + with self.lock: + return sorted(record["number"] for record in self.records) + + def allocate(self, request): + if not isinstance(request, str) or str(uuid.UUID(request)) != request: + raise ValueError("invalid request identity") + with self.lock: + for record in self.records: + if record["request"] == request: + return record["number"] + used = set(self.numbers()) + number = next((n for n in range(2, MAX_DISPLAYS + 1) + if n not in used and not self.occupied(n)), None) + if number is None: + raise ValueError("display capacity exhausted") + records = self.records + [{"number": number, "request": request}] + fd, temporary = tempfile.mkstemp(prefix="catalog-", dir=self.directory) + try: + with os.fdopen(fd, "w") as output: + json.dump({"version": 1, "displays": records}, output) + output.flush() + os.fsync(output.fileno()) + os.replace(temporary, self.path) + finally: + if os.path.exists(temporary): + os.unlink(temporary) + self.records = records + return number + + +class DisplayService: + def __init__(self, catalog, runtime, starter=STARTER): + self.catalog = catalog + self.runtime = Path(runtime) + self.starter = starter + self.lock = threading.Lock() + self.jobs = {} + self.states = {} + self.processes = {} + self.shutdown = threading.Event() + + def start(self, number): + with self.lock: + if number in self.jobs: + return self.jobs[number] + finished = threading.Event() + self.jobs[number] = finished + self.states[number] = "launching" + threading.Thread(target=self.supervise, args=(number, finished), daemon=True).start() + return finished + + def terminate(self, number): + for process in self.processes.pop(number, []): + try: + process.terminate() + except OSError: + pass + + @staticmethod + def wait_for_port(port, timeout=20): + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + if listening(port): + return True + time.sleep(0.1) + return False + + def start_components(self, number, environment, runtime): + self.terminate(number) + vnc = shutil.which("Xvnc") or shutil.which("Xtigervnc") + websockify = shutil.which("websockify") + if not vnc or not websockify: + raise RuntimeError("display runtime unavailable") + rfb_port, novnc_port = 5900 + number, 6900 + number + processes = [] + processes.append(subprocess.Popen( + [vnc, f":{number}", "-geometry", os.environ.get("CMUX_VNC_GEOMETRY", "1440x900"), + "-depth", "24", "-rfbport", str(rfb_port), "-localhost", + "-SecurityTypes", "None", "-AlwaysShared"], + env=environment, stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)) + self.processes[number] = processes + if not self.wait_for_port(rfb_port): + raise RuntimeError("Xvnc did not become ready") + bus = subprocess.run(["dbus-launch", "--sh-syntax"], env=environment, + capture_output=True, text=True, check=True).stdout + for line in bus.splitlines(): + if "=" not in line: + continue + key, value = line.split("=", 1) + environment[key] = value.strip().strip("'") + for executable, args in [ + ("openbox", []), ("tint2", ["-c", "/etc/cmux/tint2rc"]), + ("vncconfig", ["-nowin"]), + (websockify, ["--web", "/usr/share/novnc", "--heartbeat", "30", + f"[::]:{novnc_port}", f"127.0.0.1:{rfb_port}"]), + ]: + path = executable if "/" in executable else shutil.which(executable) + if not path: + if executable == "vncconfig": + continue + raise RuntimeError(f"missing display component: {executable}") + process = subprocess.Popen([path] + args, env=environment, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + processes.append(process) + if not self.wait_for_port(novnc_port, timeout=10): + raise RuntimeError("noVNC did not become ready") + (runtime / "env").write_text(f"export DISPLAY=:{number}\n") + + def supervise(self, number, finished): + runtime = self.runtime / str(number) + runtime.mkdir(mode=0o700, parents=True, exist_ok=True) + environment = dict(os.environ, DISPLAY=f":{number}", CMUX_DESKTOP_RUNTIME_DIR=str(runtime)) + for key in ("NOTIFY_SOCKET", "DBUS_SESSION_BUS_ADDRESS", "DBUS_SESSION_BUS_PID", + "AT_SPI_BUS_ADDRESS", "AT_SPI_BUS"): + environment.pop(key, None) + while not self.shutdown.is_set(): + try: + self.start_components(number, environment, runtime) + state = "running" if ready(number) else "unavailable" + except (OSError, RuntimeError, subprocess.SubprocessError): + self.terminate(number) + state = "unavailable" + with self.lock: + self.states[number] = state + finished.set() + self.shutdown.wait(30) + self.terminate(number) + + def snapshot(self): + with self.lock: + return {"version": 1, "canCreate": len(self.jobs) < MAX_DISPLAYS - 1, + "displays": [descriptor(1, "running" if ready(1) else "unavailable")] + + [descriptor(n, self.states.get(n, "launching")) for n in self.catalog.numbers()]} + + def handle(self, request): + if request.get("action") == "list": + return self.snapshot() + if request.get("action") != "create": + raise ValueError("unsupported display action") + number = self.catalog.allocate(request.get("request")) + self.start(number).wait(55) + result = self.snapshot() + result["created"] = f"display:{number}" + if self.states.get(number) != "running": + result["error"] = "display_start_failed" + return result + + +def serve(runtime): + runtime = Path(runtime) + runtime.mkdir(mode=0o700, parents=True, exist_ok=True) + with (runtime / "display-control.lock").open("w") as lock: + try: + fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) + except BlockingIOError: + return + if not all(shutil.which(name) for name in ("websockify", "openbox", "dbus-launch")): + raise ValueError("display runtime unavailable") + if not (shutil.which("Xvnc") or shutil.which("Xtigervnc")): + raise ValueError("display runtime unavailable") + service = DisplayService(DisplayCatalog(Path.home() / ".cmux" / "displays"), runtime / "displays") + for number in service.catalog.numbers(): + service.start(number) + path = runtime / "display-control.sock" + if path.exists(): + path.unlink() + + class Handler(socketserver.StreamRequestHandler): + def handle(self): + self.connection.settimeout(60) + try: + raw = self.rfile.readline(4097) + if len(raw) > 4096: + raise ValueError("display request too large") + result = service.handle(json.loads(raw)) + except (ValueError, KeyError, TypeError, OSError): + result = {"error": "display_request_failed"} + try: + self.wfile.write(json.dumps(result).encode() + b"\n") + except OSError: + pass # The persisted receipt remains replayable after disconnect. + + class Server(socketserver.ThreadingUnixStreamServer): + daemon_threads = True + + previous_umask = os.umask(0o077) + try: + with Server(str(path), Handler) as server: + os.umask(previous_umask) + server.serve_forever() + finally: + os.umask(previous_umask) + service.shutdown.set() + path.unlink(missing_ok=True) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("action", choices=["list", "create", "serve"]) + parser.add_argument("--request-id") + args = parser.parse_args() + runtime = os.environ.get("CMUX_DESKTOP_RUNTIME_DIR", "/run/cmux-desktop") + if args.action == "serve": + serve(runtime) + return 0 + if args.action == "create" and not args.request_id: + parser.error("create requires --request-id (retain it when retrying)") + try: + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client: + client.settimeout(60) + client.connect(str(Path(runtime) / "display-control.sock")) + client.sendall(json.dumps({"action": args.action, "request": args.request_id}).encode() + b"\n") + with client.makefile("rb") as stream: + result = json.loads(stream.readline(65536)) + except (OSError, ValueError): + result = {"error": "display_runtime_unavailable"} + print(json.dumps(result)) + return 1 if "error" in result else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) + +"""# + private static let path = "$HOME/.cmux/cmux-display" + + static func command(action: String, requestID: UUID? = nil) -> String { + let encoded = Data(source.utf8).base64EncodedString() + let argument = requestID.map { " --request-id \($0.uuidString.lowercased())" } ?? "" + return """ + set -eu + path=\"\(path)\" + mkdir -p \"$HOME/.cmux\" + if [ ! -x \"$path\" ]; then printf '%s' \"\(encoded)\" | base64 -d > \"$path\"; chmod 700 \"$path\"; fi + if ! pgrep -u \"$(id -u)\" -f \"$path serve\" >/dev/null 2>&1; then + nohup \"$path\" serve > \"$HOME/.cmux/display-service.log\" 2>&1 & + for i in $(seq 1 100); do [ -S /run/cmux-desktop/display-control.sock ] && break; sleep 0.1; done + fi + \"$path\" \(action)\(argument) + """ + } +} diff --git a/cmuxTests/CloudDisplayCatalogTests.swift b/cmuxTests/CloudDisplayCatalogTests.swift index 40741eee3aeb..6dfa80f7d399 100644 --- a/cmuxTests/CloudDisplayCatalogTests.swift +++ b/cmuxTests/CloudDisplayCatalogTests.swift @@ -17,7 +17,7 @@ struct CloudDisplayCatalogTests { func creationRetryKeepsRequestIdentity() async throws { var creates: [String] = [] let service = CloudDisplayCoordinator { command, _ in - if command.hasSuffix(" list") { return .init(exitCode: 0, stdout: initial, stderr: "") } + if command.contains(" list") { return .init(exitCode: 0, stdout: initial, stderr: "") } creates.append(command) if creates.count == 1 { throw URLError(.networkConnectionLost) } return .init(exitCode: 0, stdout: created, stderr: "") @@ -36,7 +36,7 @@ struct CloudDisplayCatalogTests { let started = CloudLinkFirstValue() let response = CloudLinkFirstValue() let service = CloudDisplayCoordinator { command, _ in - if command.hasSuffix(" list") { return .init(exitCode: 0, stdout: initial, stderr: "") } + if command.contains(" list") { return .init(exitCode: 0, stdout: initial, stderr: "") } started.resolve(true) _ = await response.result return .init(exitCode: 0, stdout: created, stderr: "") diff --git a/web/services/vms/images/devbox/desktop/cmux-display b/tests/fixtures/cmux-display similarity index 76% rename from web/services/vms/images/devbox/desktop/cmux-display rename to tests/fixtures/cmux-display index dc89ba7d11ef..a8a406c7f78c 100755 --- a/web/services/vms/images/devbox/desktop/cmux-display +++ b/tests/fixtures/cmux-display @@ -124,6 +124,7 @@ class DisplayService: self.lock = threading.Lock() self.jobs = {} self.states = {} + self.processes = {} self.shutdown = threading.Event() def start(self, number): @@ -136,28 +137,84 @@ class DisplayService: threading.Thread(target=self.supervise, args=(number, finished), daemon=True).start() return finished + def terminate(self, number): + for process in self.processes.pop(number, []): + try: + process.terminate() + except OSError: + pass + + @staticmethod + def wait_for_port(port, timeout=20): + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + if listening(port): + return True + time.sleep(0.1) + return False + + def start_components(self, number, environment, runtime): + self.terminate(number) + vnc = shutil.which("Xvnc") or shutil.which("Xtigervnc") + websockify = shutil.which("websockify") + if not vnc or not websockify: + raise RuntimeError("display runtime unavailable") + rfb_port, novnc_port = 5900 + number, 6900 + number + processes = [] + processes.append(subprocess.Popen( + [vnc, f":{number}", "-geometry", os.environ.get("CMUX_VNC_GEOMETRY", "1440x900"), + "-depth", "24", "-rfbport", str(rfb_port), "-localhost", + "-SecurityTypes", "None", "-AlwaysShared"], + env=environment, stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)) + self.processes[number] = processes + if not self.wait_for_port(rfb_port): + raise RuntimeError("Xvnc did not become ready") + bus = subprocess.run(["dbus-launch", "--sh-syntax"], env=environment, + capture_output=True, text=True, check=True).stdout + for line in bus.splitlines(): + if "=" not in line: + continue + key, value = line.split("=", 1) + environment[key] = value.strip().strip("'") + for executable, args in [ + ("openbox", []), ("tint2", ["-c", "/etc/cmux/tint2rc"]), + ("vncconfig", ["-nowin"]), + (websockify, ["--web", "/usr/share/novnc", "--heartbeat", "30", + f"[::]:{novnc_port}", f"127.0.0.1:{rfb_port}"]), + ]: + path = executable if "/" in executable else shutil.which(executable) + if not path: + if executable == "vncconfig": + continue + raise RuntimeError(f"missing display component: {executable}") + process = subprocess.Popen([path] + args, env=environment, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + processes.append(process) + if not self.wait_for_port(novnc_port, timeout=10): + raise RuntimeError("noVNC did not become ready") + (runtime / "env").write_text(f"export DISPLAY=:{number}\n") + def supervise(self, number, finished): runtime = self.runtime / str(number) runtime.mkdir(mode=0o700, parents=True, exist_ok=True) environment = dict(os.environ, DISPLAY=f":{number}", CMUX_DESKTOP_RUNTIME_DIR=str(runtime)) - # Each display starts a separate session bus; it must not inherit :1's. for key in ("NOTIFY_SOCKET", "DBUS_SESSION_BUS_ADDRESS", "DBUS_SESSION_BUS_PID", "AT_SPI_BUS_ADDRESS", "AT_SPI_BUS"): environment.pop(key, None) while not self.shutdown.is_set(): try: - subprocess.run(["bash", self.starter], env=environment, stdin=subprocess.DEVNULL, - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, - timeout=50, check=True) + self.start_components(number, environment, runtime) state = "running" if ready(number) else "unavailable" - except (OSError, subprocess.SubprocessError): + except (OSError, RuntimeError, subprocess.SubprocessError): + self.terminate(number) state = "unavailable" with self.lock: self.states[number] = state finished.set() - # This is supervision, not readiness: a failed listener is reported - # immediately, and the next pass heals only this display's processes. self.shutdown.wait(30) + self.terminate(number) def snapshot(self): with self.lock: diff --git a/tests/test_cloud_display_catalog.py b/tests/test_cloud_display_catalog.py index 17ae342ef489..c10dc8067455 100644 --- a/tests/test_cloud_display_catalog.py +++ b/tests/test_cloud_display_catalog.py @@ -14,7 +14,7 @@ sys.dont_write_bytecode = True loader = importlib.machinery.SourceFileLoader( "cmux_display", str(Path(__file__).resolve().parents[1] / - "web/services/vms/images/devbox/desktop/cmux-display")) + "tests/fixtures/cmux-display")) spec = importlib.util.spec_from_loader(loader.name, loader) display = importlib.util.module_from_spec(spec) loader.exec_module(display) @@ -95,20 +95,34 @@ def test_supervision_keeps_display_and_session_environment_separate(self): service = display.DisplayService(catalog, self.root / "runtime") environments = [] - def run(_command, **options): - environments.append(options["env"]) + class Process: + def terminate(self): + pass - with mock.patch.object(display.subprocess, "run", side_effect=run), mock.patch.object(display, "ready", return_value=True): + def spawn(_command, **options): + environments.append(options["env"].copy()) + return Process() + + def launch(_command, **_options): + return mock.Mock(stdout=f"DBUS_SESSION_BUS_ADDRESS='unix:path=/tmp/bus-{len(environments)}'\nDBUS_SESSION_BUS_PID=1\n") + + with mock.patch.object(display.subprocess, "Popen", side_effect=spawn), \ + mock.patch.object(display.subprocess, "run", side_effect=launch), \ + mock.patch.object(display.shutil, "which", side_effect=lambda name: name), \ + mock.patch.object(service, "wait_for_port", return_value=True), \ + mock.patch.object(display, "ready", return_value=True): try: first = service.handle({"action": "create", "request": str(uuid.uuid4())}) second = service.handle({"action": "create", "request": str(uuid.uuid4())}) self.assertEqual(first["created"], "display:2") self.assertEqual(second["created"], "display:3") self.assertEqual(len(second["displays"]), 3) - self.assertEqual([env["DISPLAY"] for env in environments], [":2", ":3"]) - self.assertNotEqual(environments[0]["CMUX_DESKTOP_RUNTIME_DIR"], environments[1]["CMUX_DESKTOP_RUNTIME_DIR"]) - for env in environments: - self.assertNotIn("DBUS_SESSION_BUS_ADDRESS", env) + displays = sorted({env["DISPLAY"] for env in environments}) + self.assertEqual(displays, [":2", ":3"]) + by_display = {env["DISPLAY"]: env for env in environments} + self.assertNotEqual(by_display[":2"]["CMUX_DESKTOP_RUNTIME_DIR"], by_display[":3"]["CMUX_DESKTOP_RUNTIME_DIR"]) + self.assertNotEqual(by_display[":2"]["DBUS_SESSION_BUS_ADDRESS"], by_display[":3"]["DBUS_SESSION_BUS_ADDRESS"]) + for env in by_display.values(): self.assertNotIn("NOTIFY_SOCKET", env) finally: service.shutdown.set() diff --git a/web/scripts/devbox-image-common.ts b/web/scripts/devbox-image-common.ts index af8ee6fb9fd1..8719f46245cb 100644 --- a/web/scripts/devbox-image-common.ts +++ b/web/scripts/devbox-image-common.ts @@ -195,7 +195,6 @@ export const devboxDesktopDir = path.join(devboxDir, "desktop"); export const DEVBOX_DESKTOP_FILES = [ "WALLPAPER.md", "cmux-desktop-boot", - "cmux-display", "cmux-desktop.service", "desktop-env.sh", "ghostty-cmux.desktop", @@ -228,7 +227,6 @@ export const DEVBOX_DESKTOP_INSTALLS: readonly DevboxDesktopInstall[] = [ { source: "desktop/wallpaper.jpg", target: "/usr/share/backgrounds/cmux/wallpaper.jpg", mode: 0o644 }, { source: "desktop/start-vnc.sh", target: "/usr/local/bin/start-vnc.sh", mode: 0o755 }, { source: "desktop/cmux-desktop-boot", target: "/usr/local/bin/cmux-desktop-boot", mode: 0o755 }, - { source: "desktop/cmux-display", target: "/usr/local/bin/cmux-display", mode: 0o755 }, { source: "desktop/cmux-desktop.service", target: "/etc/systemd/system/cmux-desktop.service", mode: 0o644 }, { source: "desktop/desktop-env.sh", target: "/etc/cmux/desktop-env.sh", mode: 0o644 }, ]; diff --git a/web/services/vms/images/devbox/Dockerfile b/web/services/vms/images/devbox/Dockerfile index 667250caadb0..47dad1fb15dd 100644 --- a/web/services/vms/images/devbox/Dockerfile +++ b/web/services/vms/images/devbox/Dockerfile @@ -209,10 +209,9 @@ COPY desktop/tint2rc /etc/cmux/tint2rc COPY desktop/wallpaper.jpg /usr/share/backgrounds/cmux/wallpaper.jpg COPY desktop/start-vnc.sh /usr/local/bin/start-vnc.sh COPY desktop/cmux-desktop-boot /usr/local/bin/cmux-desktop-boot -COPY desktop/cmux-display /usr/local/bin/cmux-display COPY desktop/cmux-desktop.service /etc/systemd/system/cmux-desktop.service COPY desktop/desktop-env.sh /etc/cmux/desktop-env.sh -RUN chmod 0755 /usr/local/bin/start-vnc.sh /usr/local/bin/cmux-desktop-boot /usr/local/bin/cmux-display \ +RUN chmod 0755 /usr/local/bin/start-vnc.sh /usr/local/bin/cmux-desktop-boot \ && bash -n /usr/local/bin/start-vnc.sh && sh -n /usr/local/bin/cmux-desktop-boot && sh -n /etc/cmux/desktop-env.sh \ && mkdir -p /etc/cmux/icons \ && cp /opt/google/chrome/product_logo_128.png /etc/cmux/icons/google-chrome.png \ diff --git a/web/services/vms/images/devbox/desktop/cmux-desktop-boot b/web/services/vms/images/devbox/desktop/cmux-desktop-boot index ce0376af729d..66248d311513 100644 --- a/web/services/vms/images/devbox/desktop/cmux-desktop-boot +++ b/web/services/vms/images/devbox/desktop/cmux-desktop-boot @@ -15,11 +15,6 @@ CMUX_DESKTOP_RUNTIME_DIR="${CMUX_DESKTOP_RUNTIME_DIR:-/run/cmux-desktop}" export CMUX_DESKTOP_RUNTIME_DIR mkdir -p "$HOME/.config/google-chrome" 2>/dev/null touch "$HOME/.config/google-chrome/First Run" 2>/dev/null -# One guest-owned control socket creates and restores independent displays. -# Old images without the capability retain their existing desktop unchanged. -if [ -x /usr/local/bin/cmux-display ]; then - /usr/local/bin/cmux-display serve >>"$CMUX_DESKTOP_RUNTIME_DIR/display-control.log" 2>&1 & -fi while true; do bash /usr/local/bin/start-vnc.sh || true sleep 30 diff --git a/web/services/vms/images/devbox/desktop/start-vnc.sh b/web/services/vms/images/devbox/desktop/start-vnc.sh index c0f09ea95b2e..edcb7c412466 100644 --- a/web/services/vms/images/devbox/desktop/start-vnc.sh +++ b/web/services/vms/images/devbox/desktop/start-vnc.sh @@ -47,18 +47,12 @@ unset NOTIFY_SOCKET DISPLAY="${DISPLAY:-:1}" export DISPLAY -DISPLAY_NUMBER="${DISPLAY#:}" -case "$DISPLAY_NUMBER" in ''|*[!0-9]*) echo "Invalid display number" >&2; exit 1;; esac -if [ "$DISPLAY_NUMBER" -lt 1 ] || [ "$DISPLAY_NUMBER" -gt 16 ]; then exit 1; fi -RFB_PORT=$((5900 + DISPLAY_NUMBER)) -NOVNC_PORT=$((6900 + DISPLAY_NUMBER)) GEOMETRY="${CMUX_VNC_GEOMETRY:-1440x900}" RUNTIME_DIR="${CMUX_DESKTOP_RUNTIME_DIR:-/run/cmux-desktop}" # Never let an unwritable HOME keep the desktop down: fall back to a per-user # tmp dir for logs and session state. STATE_DIR="$HOME/.cmux" -if [ "$DISPLAY_NUMBER" -ne 1 ]; then STATE_DIR="$STATE_DIR/displays/$DISPLAY_NUMBER"; fi -mkdir -p "$STATE_DIR/desktop-logs" 2>/dev/null || { STATE_DIR="/tmp/cmux-desktop-$(id -u)-$DISPLAY_NUMBER"; mkdir -p "$STATE_DIR/desktop-logs"; } +mkdir -p "$STATE_DIR/desktop-logs" 2>/dev/null || { STATE_DIR="/tmp/cmux-desktop-$(id -u)"; mkdir -p "$STATE_DIR/desktop-logs"; } LOG_DIR="$STATE_DIR/desktop-logs" # The supervisor re-runs this every 30 s, so cap each component log here: a @@ -73,19 +67,7 @@ done VNC_BIN="$(command -v Xvnc || command -v Xtigervnc)" || exit 0 listening() { ss -tln 2>/dev/null | grep -q ":$1 "; } -# Process names alone would reuse :1's window manager and session bus on :2. -# Filter every component by the exact DISPLAY inherited at process creation. -display_pids() { - pgrep -u "$(id -u)" "$@" 2>/dev/null | while read -r pid; do - [ -r "/proc/$pid/environ" ] || continue - if tr '\0' '\n' < "/proc/$pid/environ" 2>/dev/null | grep -Fxq "DISPLAY=$DISPLAY"; then echo "$pid"; fi - done -} -export -f display_pids -mine() { [ -n "$(display_pids "$@")" ]; } -# Refuse a port owned by another session rather than aliasing its screen. -if [ "$DISPLAY_NUMBER" -ne 1 ] && listening "$RFB_PORT" && ! mine -x 'Xvnc|Xtigervnc'; then exit 1; fi -if [ "$DISPLAY_NUMBER" -ne 1 ] && listening "$NOVNC_PORT" && ! mine -f "websockify.*127.0.0.1:$RFB_PORT"; then exit 1; fi +mine() { pgrep -u "$(id -u)" "$@" >/dev/null 2>&1; } # Bounded wait for a listener that has no readiness signal of its own # (websockify): probes the socket table until the port is bound or the # deadline (seconds) passes. Returns the port's state at the deadline. @@ -102,14 +84,14 @@ wait_listening() { # moment it accepts connections, so nothing below talks to X too early. The # descriptor is a FIFO opened read/write here (so neither side blocks on the # open) and read with a deadline. -if ! listening "$RFB_PORT"; then +if ! listening 5901; then ready_fifo="$STATE_DIR/xvnc-ready.fifo" rm -f "$ready_fifo" if mkfifo -m 600 "$ready_fifo" 2>/dev/null && exec {ready_fd}<>"$ready_fifo"; then "$VNC_BIN" "$DISPLAY" \ -geometry "$GEOMETRY" \ -depth 24 \ - -rfbport "$RFB_PORT" \ + -rfbport 5901 \ -localhost \ -SecurityTypes None \ -AlwaysShared \ @@ -122,12 +104,12 @@ if ! listening "$RFB_PORT"; then "$VNC_BIN" "$DISPLAY" \ -geometry "$GEOMETRY" \ -depth 24 \ - -rfbport "$RFB_PORT" \ + -rfbport 5901 \ -localhost \ -SecurityTypes None \ -AlwaysShared \ >>"$LOG_DIR/xvnc.log" 2>&1 & - wait_listening "$RFB_PORT" 20 || true + wait_listening 5901 20 || true fi fi @@ -199,7 +181,7 @@ if ! mine -f cmux-desktop-resize-watch; then case $line in *RRScreenChangeNotify*) feh --no-fehbg --bg-fill /usr/share/backgrounds/cmux/wallpaper.jpg >/dev/null 2>&1 || true - display_pids -x tint2 | xargs -r kill -USR1 >/dev/null 2>&1 || true + pkill -USR1 -U "$(id -u)" -x tint2 >/dev/null 2>&1 || true ;; esac done @@ -207,8 +189,8 @@ if ! mine -f cmux-desktop-resize-watch; then fi # noVNC uses a dual-stack listener so either private address can reach the desktop. -if ! listening "$NOVNC_PORT"; then - websockify --web /usr/share/novnc --heartbeat 30 "[::]:$NOVNC_PORT" "127.0.0.1:$RFB_PORT" \ +if ! listening 6901; then + websockify --web /usr/share/novnc --heartbeat 30 '[::]:6901' 127.0.0.1:5901 \ >>"$LOG_DIR/websockify.log" 2>&1 & fi @@ -243,8 +225,8 @@ fi # Repeating it on later passes is harmless; a pass that finds something # down leaves the unit's state to systemd (Restart=always) and this loop. if [ -n "$CMUX_NOTIFY_SOCKET" ] && command -v systemd-notify >/dev/null 2>&1; then - if listening "$RFB_PORT" && wait_listening "$NOVNC_PORT" 10 && [ "$published" = 1 ]; then - NOTIFY_SOCKET="$CMUX_NOTIFY_SOCKET" systemd-notify --ready --status="desktop up on $DISPLAY: RFB $RFB_PORT (loopback), noVNC $NOVNC_PORT" 2>>"$LOG_DIR/notify.log" || true + if listening 5901 && wait_listening 6901 10 && [ "$published" = 1 ]; then + NOTIFY_SOCKET="$CMUX_NOTIFY_SOCKET" systemd-notify --ready --status="desktop up on $DISPLAY: RFB 5901 (loopback), noVNC 6901" 2>>"$LOG_DIR/notify.log" || true fi fi diff --git a/web/tests/vm-devbox-desktop.test.ts b/web/tests/vm-devbox-desktop.test.ts index 8ec7b48af7c3..30c8d24f0cf3 100644 --- a/web/tests/vm-devbox-desktop.test.ts +++ b/web/tests/vm-devbox-desktop.test.ts @@ -138,7 +138,7 @@ describe("devbox desktop layer", () => { }); test("keeps the desktop port contract: RFB 5901 loopback-only, noVNC on 6901", () => { - // Independent target allocation is executed in vm-display-catalog.test.ts. + expect(startVnc).toContain(`-rfbport ${DEVBOX_DESKTOP_RFB_PORT}`); expect(startVnc).toContain("-SecurityTypes None"); expect(startVnc).toContain("-localhost"); // Listener reachability is exercised by verify-devbox-image over both @@ -227,6 +227,7 @@ describe("devbox desktop layer", () => { expect(startVnc).toContain("*RRScreenChangeNotify*"); expect(startVnc).not.toContain("sleep 2"); expect(startVnc).not.toContain("sleep 0.2"); + expect(startVnc).toContain("wait_listening 6901 10"); expect(startVnc).toContain('eval "$(dbus-launch --sh-syntax'); expect(startVnc).toContain('kill -0 "$DBUS_SESSION_BUS_PID"'); expect(startVnc).toContain("/usr/libexec/at-spi-bus-launcher"); diff --git a/web/tests/vm-display-catalog.test.ts b/web/tests/vm-display-catalog.test.ts deleted file mode 100644 index 5fa90482ae13..000000000000 --- a/web/tests/vm-display-catalog.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { expect, test } from "bun:test"; -import { spawnSync } from "node:child_process"; -import path from "node:path"; - -test("guest display catalog allocates independent, replayable resources", () => { - const root = path.resolve(import.meta.dirname, "../.."); - const result = spawnSync("python3", ["-B", "-m", "unittest", "tests/test_cloud_display_catalog.py", "-v"], { - cwd: root, - encoding: "utf8", - timeout: 30_000, - }); - expect({ status: result.status, error: result.error?.message, output: result.stderr }).toEqual({ - status: 0, - error: undefined, - output: expect.stringContaining("OK"), - }); -}); From 0f8e410f6ed226d60f5c9591e3a61fe1b8c1fd9f Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 05:16:57 -0700 Subject: [PATCH 04/64] Fix guest display target wiring and session supervision --- Sources/Cloud/CloudGuestDisplayScript.swift | 5 ++++- cmux.xcodeproj/project.pbxproj | 4 ++++ tests/fixtures/cmux-display | 5 ++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Sources/Cloud/CloudGuestDisplayScript.swift b/Sources/Cloud/CloudGuestDisplayScript.swift index 72afce49616e..a11cf15e0756 100644 --- a/Sources/Cloud/CloudGuestDisplayScript.swift +++ b/Sources/Cloud/CloudGuestDisplayScript.swift @@ -211,7 +211,10 @@ class DisplayService: environment.pop(key, None) while not self.shutdown.is_set(): try: - self.start_components(number, environment, runtime) + # The supervisor is a health check, not a timer-driven restart. + # Preserve an active RFB/noVNC session until its owned endpoints fail. + if not self.processes.get(number) or not ready(number): + self.start_components(number, environment, runtime) state = "running" if ready(number) else "unavailable" except (OSError, RuntimeError, subprocess.SubprocessError): self.terminate(number) diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index f6988404349f..ebbcf222c2ba 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -747,6 +747,7 @@ C3CD2FF9F6F1489C8EDD0C7B /* CloudFeatureFlagTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBF28FA7AB264C1AA7821248 /* CloudFeatureFlagTests.swift */; }; 00034889FEF18E9AE0AFDEEC /* CloudFileDelivery.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA32812C9009969F4AEA2380 /* CloudFileDelivery.swift */; }; 57BC801370636A4E2C1B31B7 /* CloudFileDeliveryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8FED5F6485C390988555CDA /* CloudFileDeliveryTests.swift */; }; + 777A119455C940AC95CF68F9 /* CloudGuestDisplayScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6E533611CF47E29BC45008 /* CloudGuestDisplayScript.swift */; }; B844B6722D714B769A32A05F /* CloudGuestDisplaySnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1257ADDE94A4743AA16C593 /* CloudGuestDisplaySnapshot.swift */; }; F7925DA2C43A12723611F26E /* CloudGuestURLRoutingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 935A3D4E1C9FB4A5E94E2770 /* CloudGuestURLRoutingTests.swift */; }; 902ED2709F9B61E9E4B59A24 /* CloudGuestURLService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EAC5EBE3FB2C24E1F29B55C /* CloudGuestURLService.swift */; }; @@ -4815,6 +4816,7 @@ EBF28FA7AB264C1AA7821248 /* CloudFeatureFlagTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudFeatureFlagTests.swift; sourceTree = ""; }; DA32812C9009969F4AEA2380 /* CloudFileDelivery.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CloudFileDelivery.swift; sourceTree = ""; }; F8FED5F6485C390988555CDA /* CloudFileDeliveryTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CloudFileDeliveryTests.swift; sourceTree = ""; }; + DD6E533611CF47E29BC45008 /* CloudGuestDisplayScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudGuestDisplayScript.swift"; sourceTree = ""; }; F1257ADDE94A4743AA16C593 /* CloudGuestDisplaySnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudGuestDisplaySnapshot.swift"; sourceTree = ""; }; 935A3D4E1C9FB4A5E94E2770 /* CloudGuestURLRoutingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudGuestURLRoutingTests.swift"; sourceTree = ""; }; 8EAC5EBE3FB2C24E1F29B55C /* CloudGuestURLService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudGuestURLService.swift"; sourceTree = ""; }; @@ -8710,6 +8712,7 @@ E12747253A744B63A30EFA27 /* CloudTreeOutlineView+Displays.swift */, F1257ADDE94A4743AA16C593 /* CloudGuestDisplaySnapshot.swift */, 2F99243AE43E429485F65C00 /* CloudDisplayCoordinator.swift */, + DD6E533611CF47E29BC45008 /* CloudGuestDisplayScript.swift */, 1CE0A82A4E20E446A21A97BB /* CloudTreeNode.swift */, 2BBC2E2213D2FB598EC3751B /* CloudTreeNodeBuilder+Ports.swift */, 8EDC6087D41DC9E2271BC886 /* CloudMachineNotificationGate.swift */, @@ -13147,6 +13150,7 @@ F8111B8E2214BA971C8EB6E0 /* CloudErrorCopy.swift in Sources */, 5EDE6BFD28BE4BD58EF84889 /* CloudFeatureAvailabilityObserver.swift in Sources */, 00034889FEF18E9AE0AFDEEC /* CloudFileDelivery.swift in Sources */, + 777A119455C940AC95CF68F9 /* CloudGuestDisplayScript.swift in Sources */, B844B6722D714B769A32A05F /* CloudGuestDisplaySnapshot.swift in Sources */, 902ED2709F9B61E9E4B59A24 /* CloudGuestURLService.swift in Sources */, D0110080000000000000000A /* CloudHubConnection.swift in Sources */, diff --git a/tests/fixtures/cmux-display b/tests/fixtures/cmux-display index a8a406c7f78c..aca05e295bce 100755 --- a/tests/fixtures/cmux-display +++ b/tests/fixtures/cmux-display @@ -205,7 +205,10 @@ class DisplayService: environment.pop(key, None) while not self.shutdown.is_set(): try: - self.start_components(number, environment, runtime) + # The supervisor is a health check, not a timer-driven restart. + # Preserve an active RFB/noVNC session until its owned endpoints fail. + if not self.processes.get(number) or not ready(number): + self.start_components(number, environment, runtime) state = "running" if ready(number) else "unavailable" except (OSError, RuntimeError, subprocess.SubprocessError): self.terminate(number) From a7f0e3bf295e27cf0c396d26dd2da845800d5d0a Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 05:26:17 -0700 Subject: [PATCH 05/64] Harden embedded display helper and Dock restore ownership --- Sources/Cloud/CloudGuestDisplayScript.swift | 8 ++++---- Sources/Surfaces/DockSplitStore+SurfaceOwnership.swift | 5 +++-- cmuxTests/CloudDisplayCatalogTests.swift | 7 +++++++ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Sources/Cloud/CloudGuestDisplayScript.swift b/Sources/Cloud/CloudGuestDisplayScript.swift index a11cf15e0756..ea227547de83 100644 --- a/Sources/Cloud/CloudGuestDisplayScript.swift +++ b/Sources/Cloud/CloudGuestDisplayScript.swift @@ -5,12 +5,12 @@ import Foundation enum CloudGuestDisplayScript { private static let source = #""" #!/usr/bin/env python3 -\"\"\"Guest-owned display catalog and supervisor; invoked by the desktop service. +"""Guest-owned display catalog and supervisor; invoked by the desktop service. The existing :1 desktop remains owned by start-vnc.sh. Additional records each own an X server, session bus, window manager and noVNC listener. The control socket is local to this guest and only accessible to its work user. -\"\"\" +""" import argparse import fcntl import http.client @@ -44,7 +44,7 @@ def listening(port): def ready(number): - \"\"\"A bound HTTP port alone does not prove that an X/RFB server exists.\"\"\" + """A bound HTTP port alone does not prove that an X/RFB server exists.""" try: with socket.create_connection(("127.0.0.1", 5900 + number), timeout=0.5) as rfb: version = b"" @@ -66,7 +66,7 @@ def ready(number): class DisplayCatalog: - \"\"\"Serializes allocation and persists request receipts before starting work.\"\"\" + """Serializes allocation and persists request receipts before starting work.""" def __init__(self, directory, occupied=None): self.directory = Path(directory) diff --git a/Sources/Surfaces/DockSplitStore+SurfaceOwnership.swift b/Sources/Surfaces/DockSplitStore+SurfaceOwnership.swift index ac6dbfbfe570..2b39f0439732 100644 --- a/Sources/Surfaces/DockSplitStore+SurfaceOwnership.swift +++ b/Sources/Surfaces/DockSplitStore+SurfaceOwnership.swift @@ -6,7 +6,8 @@ extension DockSplitStore { } func acceptsUnownedBrowserURL(_ url: URL?) -> Bool { - url?.path != "/vnc.html" || surfaceOwnershipPolicy.rejection(for: nil) == nil + guard url?.path == "/vnc.html" else { return true } + return scope != .global && surfaceOwnershipPolicy.rejection(for: nil) == nil } func surfaceDropRejection(_ transfer: PaneDragTransfer, source: PaneTransferSourceResolver.Source) -> SurfaceTransferRejection? { @@ -33,7 +34,7 @@ extension DockSplitStore { return surfaceOwnershipPolicy.rejection(for: resource.machine) == nil } if let raw = snapshot.browser?.urlString, URL(string: raw)?.path == "/vnc.html" { - return surfaceOwnershipPolicy.rejection(for: nil) == nil + return scope != .global && surfaceOwnershipPolicy.rejection(for: nil) == nil } return true } diff --git a/cmuxTests/CloudDisplayCatalogTests.swift b/cmuxTests/CloudDisplayCatalogTests.swift index 6dfa80f7d399..5a5dba9bace8 100644 --- a/cmuxTests/CloudDisplayCatalogTests.swift +++ b/cmuxTests/CloudDisplayCatalogTests.swift @@ -13,6 +13,13 @@ struct CloudDisplayCatalogTests { private let initial = #"{"version":1,"canCreate":true,"displays":[{"id":"display:1","number":1,"port":6901,"state":"running"}]}"# private let created = #"{"version":1,"canCreate":true,"displays":[{"id":"display:1","number":1,"port":6901,"state":"running"},{"id":"display:2","number":2,"port":6902,"state":"running"}],"created":"display:2"}"# + @Test("The installed helper keeps Python docstrings valid in the command payload") + func embeddedGuestScriptIsExecutableText() { + let command = CloudGuestDisplayScript.command(action: "list") + #expect(!command.contains(#"\"\"\""#)) + #expect(command.contains("DisplayService")) + } + @Test("A lost creation reply replays its receipt rather than allocating another display") func creationRetryKeepsRequestIdentity() async throws { var creates: [String] = [] From 0a7fad03a3d6f09444355127deb187e788a4f080 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 05:59:29 -0700 Subject: [PATCH 06/64] Close Cloud display lifecycle gaps --- Sources/Cloud/CloudGuestDisplayScript.swift | 26 +++++++++++++++++++ .../PortForward/CloudBrowserAccessState.swift | 3 ++- .../CmuxTuiSurfaceProvider+Displays.swift | 11 +++++--- cmuxTests/CloudDesktopAccessTests.swift | 12 +++++++++ tests/fixtures/cmux-display | 26 +++++++++++++++++++ 5 files changed, 74 insertions(+), 4 deletions(-) diff --git a/Sources/Cloud/CloudGuestDisplayScript.swift b/Sources/Cloud/CloudGuestDisplayScript.swift index ea227547de83..d936b0123872 100644 --- a/Sources/Cloud/CloudGuestDisplayScript.swift +++ b/Sources/Cloud/CloudGuestDisplayScript.swift @@ -122,6 +122,26 @@ class DisplayCatalog: return number +class OwnedProcess: + """A handle for a daemon started by dbus-launch rather than by Popen.""" + + def __init__(self, pid): + self.pid = pid + + def terminate(self): + try: + os.kill(self.pid, 15) + except OSError: + pass + + def poll(self): + try: + os.kill(self.pid, 0) + return None + except OSError: + return 0 + + class DisplayService: def __init__(self, catalog, runtime, starter=STARTER): self.catalog = catalog @@ -183,6 +203,12 @@ class DisplayService: continue key, value = line.split("=", 1) environment[key] = value.strip().strip("'") + try: + dbus_pid = int(environment.get("DBUS_SESSION_BUS_PID", "0")) + except ValueError: + dbus_pid = 0 + if dbus_pid > 1: + processes.append(OwnedProcess(dbus_pid)) for executable, args in [ ("openbox", []), ("tint2", ["-c", "/etc/cmux/tint2rc"]), ("vncconfig", ["-nowin"]), diff --git a/Sources/Cloud/PortForward/CloudBrowserAccessState.swift b/Sources/Cloud/PortForward/CloudBrowserAccessState.swift index 088b5d66a4d7..7a124d2a672e 100644 --- a/Sources/Cloud/PortForward/CloudBrowserAccessState.swift +++ b/Sources/Cloud/PortForward/CloudBrowserAccessState.swift @@ -151,7 +151,7 @@ final class CloudBrowserAccessState { func configure(model: CloudPortAccessModel, url: URL, resourceID: SurfaceResourceID? = nil) { observationGeneration &+= 1 unavailable = nil - if let resourceID { self.resourceID = resourceID } + self.resourceID = resourceID self.model = model remoteURL = url navigationURL = nil @@ -277,6 +277,7 @@ final class CloudBrowserAccessState { navigate = nil connectionDeadline.cancel() desktopConnected = false + resourceID = nil activeNavigationID = nil hasCommittedNavigation = false unavailable = nil diff --git a/Sources/Surfaces/CmuxTuiSurfaceProvider+Displays.swift b/Sources/Surfaces/CmuxTuiSurfaceProvider+Displays.swift index 84065772e193..da1eb3593540 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProvider+Displays.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProvider+Displays.swift @@ -2,7 +2,10 @@ import Foundation import CmuxFoundation extension CmuxTuiSurfaceProvider { - var supportsDisplayCreation: Bool { isAwake && isRegisteredInCatalog() && displayCoordinator.canCreate } + var supportsDisplayCreation: Bool { + isAwake && info.hasDesktop && summary.resolvedKind.hasDesktop + && isRegisteredInCatalog() && displayCoordinator.canCreate + } var displayResources: [SurfaceResource] { if let snapshot = displayCoordinator.snapshot { @@ -17,19 +20,21 @@ extension CmuxTuiSurfaceProvider { func refreshDisplays() async { guard isAwake, info.hasDesktop, isRegisteredInCatalog() else { return } let generation = currentLifecycleGeneration + let refresh = refreshGeneration await displayCoordinator.refresh() - guard isCurrentLifecycleGeneration(generation), isRegisteredInCatalog() else { return } + guard isCurrentRefresh(lifecycle: generation, refresh: refresh) else { return } publishDisplays() } func createDisplay() async throws -> SurfaceResource { guard supportsDisplayCreation else { throw SurfaceCatalogError.unsupported(CloudGuestDisplaySnapshot.unavailableMessage) } let generation = currentLifecycleGeneration + let refresh = refreshGeneration defer { if isCurrentLifecycleGeneration(generation), isRegisteredInCatalog() { publishDisplays() } } let snapshot = try await displayCoordinator.create() - guard isCurrentLifecycleGeneration(generation), isRegisteredInCatalog() else { throw CancellationError() } + guard isCurrentRefresh(lifecycle: generation, refresh: refresh) else { throw CancellationError() } guard let display = snapshot.displays.first(where: { $0.id == snapshot.created }) else { throw SurfaceCatalogError.unsupported(CloudGuestDisplaySnapshot.unavailableMessage) } diff --git a/cmuxTests/CloudDesktopAccessTests.swift b/cmuxTests/CloudDesktopAccessTests.swift index 3c90656b5593..58e7f3648b05 100644 --- a/cmuxTests/CloudDesktopAccessTests.swift +++ b/cmuxTests/CloudDesktopAccessTests.swift @@ -232,6 +232,18 @@ struct CloudDesktopAccessTests { #expect(browser.preferredURLStringForSessionSnapshot() == remote.absoluteString) } + @Test("Leaving a Cloud page clears its resource provenance") + func leavingCloudClearsResourceIdentity() { + let state = CloudBrowserAccessState() + let model = CloudPortAccessModel(target: .init(host: "10.0.0.7", port: 6901), coordinator: nil, + wake: {}, startForward: { _ in 46901 }, stopForward: {}, route: .loopback) + let display = SurfaceResourceID(machine: .cloud("a"), kind: .display, key: "display:1") + state.configure(model: model, url: URL(string: "http://10.0.0.7:6901/vnc.html")!, resourceID: display) + #expect(state.resourceID == display) + state.leave() + #expect(state.resourceID == nil) + } + @Test("Desktop bootstrap does not paint WebKit's default white background") func desktopBackgroundUsesNativeBackingUntilCanvasPaints() async throws { let browser = BrowserPanel(workspaceId: UUID(), websiteDataStore: .nonPersistent()) diff --git a/tests/fixtures/cmux-display b/tests/fixtures/cmux-display index aca05e295bce..801bac8e9baa 100755 --- a/tests/fixtures/cmux-display +++ b/tests/fixtures/cmux-display @@ -116,6 +116,26 @@ class DisplayCatalog: return number +class OwnedProcess: + """A handle for a daemon started by dbus-launch rather than by Popen.""" + + def __init__(self, pid): + self.pid = pid + + def terminate(self): + try: + os.kill(self.pid, 15) + except OSError: + pass + + def poll(self): + try: + os.kill(self.pid, 0) + return None + except OSError: + return 0 + + class DisplayService: def __init__(self, catalog, runtime, starter=STARTER): self.catalog = catalog @@ -177,6 +197,12 @@ class DisplayService: continue key, value = line.split("=", 1) environment[key] = value.strip().strip("'") + try: + dbus_pid = int(environment.get("DBUS_SESSION_BUS_PID", "0")) + except ValueError: + dbus_pid = 0 + if dbus_pid > 1: + processes.append(OwnedProcess(dbus_pid)) for executable, args in [ ("openbox", []), ("tint2", ["-c", "/etc/cmux/tint2rc"]), ("vncconfig", ["-nowin"]), From 87abadcd14f6d347540996e31c75e2cd1fcd71e6 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 06:25:21 -0700 Subject: [PATCH 07/64] Harden display discovery and helper restart recovery --- Sources/Cloud/CloudDisplayCoordinator.swift | 1 + Sources/Cloud/CloudGuestDisplay.swift | 20 + Sources/Cloud/CloudGuestDisplayScript.swift | 500 ++++++------------ Sources/Cloud/CloudGuestDisplaySnapshot.swift | 20 +- .../CmuxTuiSurfaceProvider+Displays.swift | 2 +- cmuxTests/CloudDisplayCatalogTests.swift | 3 +- tests/fixtures/cmux-display | 2 + 7 files changed, 182 insertions(+), 366 deletions(-) create mode 100644 Sources/Cloud/CloudGuestDisplay.swift diff --git a/Sources/Cloud/CloudDisplayCoordinator.swift b/Sources/Cloud/CloudDisplayCoordinator.swift index 1bbcb554b9f1..695699644b14 100644 --- a/Sources/Cloud/CloudDisplayCoordinator.swift +++ b/Sources/Cloud/CloudDisplayCoordinator.swift @@ -31,6 +31,7 @@ final class CloudDisplayCoordinator { isAvailable = response.exitCode == 0 } catch { guard token == generation else { return } + snapshot = nil isAvailable = false } } diff --git a/Sources/Cloud/CloudGuestDisplay.swift b/Sources/Cloud/CloudGuestDisplay.swift new file mode 100644 index 000000000000..3100c74c8bfd --- /dev/null +++ b/Sources/Cloud/CloudGuestDisplay.swift @@ -0,0 +1,20 @@ +import Foundation + +/// One guest-issued display resource with a stable slot and noVNC target. +struct CloudGuestDisplay: Decodable, Sendable { + let id: String + let number: Int + let port: Int + let state: SurfaceLifecycle + + func resource(on machine: SurfaceMachineID, address: String?) -> SurfaceResource { + SurfaceResource( + id: SurfaceResourceID(machine: machine, kind: .display, key: id), + title: number == 1 + ? String(localized: "cloudTree.node.desktop", defaultValue: "Desktop") + : String(format: String(localized: "cloud.display.numberedTitle", defaultValue: "Desktop %d"), number), + detail: "noVNC", lifecycle: state, agent: nil, remoteWorkspace: nil, + port: port, url: address.map { CmuxTuiSurfaceProvider.privateDesktopURL(privateAddress: $0, port: port) } + ) + } +} diff --git a/Sources/Cloud/CloudGuestDisplayScript.swift b/Sources/Cloud/CloudGuestDisplayScript.swift index d936b0123872..caffe5cbe99e 100644 --- a/Sources/Cloud/CloudGuestDisplayScript.swift +++ b/Sources/Cloud/CloudGuestDisplayScript.swift @@ -3,360 +3,170 @@ import Foundation /// Installs and invokes the VM-scoped display helper through the existing authorized exec route. @MainActor enum CloudGuestDisplayScript { - private static let source = #""" -#!/usr/bin/env python3 -"""Guest-owned display catalog and supervisor; invoked by the desktop service. - -The existing :1 desktop remains owned by start-vnc.sh. Additional records each -own an X server, session bus, window manager and noVNC listener. The control -socket is local to this guest and only accessible to its work user. + private static let encodedSource = """ +IyEvdXNyL2Jpbi9lbnYgcHl0aG9uMwoiIiJHdWVzdC1vd25lZCBkaXNwbGF5IGNhdGFsb2cgYW5kIHN1cGVydmlzb3I7IGludm9rZWQgYnkgdGhlIGRlc2t0 +b3Agc2VydmljZS4KClRoZSBleGlzdGluZyA6MSBkZXNrdG9wIHJlbWFpbnMgb3duZWQgYnkgc3RhcnQtdm5jLnNoLiBBZGRpdGlvbmFsIHJlY29yZHMgZWFj +aApvd24gYW4gWCBzZXJ2ZXIsIHNlc3Npb24gYnVzLCB3aW5kb3cgbWFuYWdlciBhbmQgbm9WTkMgbGlzdGVuZXIuIFRoZSBjb250cm9sCnNvY2tldCBpcyBs +b2NhbCB0byB0aGlzIGd1ZXN0IGFuZCBvbmx5IGFjY2Vzc2libGUgdG8gaXRzIHdvcmsgdXNlci4KIiIiCmltcG9ydCBhcmdwYXJzZQppbXBvcnQgZmNudGwK +aW1wb3J0IGh0dHAuY2xpZW50CmltcG9ydCBqc29uCmltcG9ydCBvcwpmcm9tIHBhdGhsaWIgaW1wb3J0IFBhdGgKaW1wb3J0IHNodXRpbAppbXBvcnQgc29j +a2V0CmltcG9ydCBzb2NrZXRzZXJ2ZXIKaW1wb3J0IHN1YnByb2Nlc3MKaW1wb3J0IHRlbXBmaWxlCmltcG9ydCB0aHJlYWRpbmcKaW1wb3J0IHRpbWUKaW1w +b3J0IHV1aWQKCk1BWF9ESVNQTEFZUyA9IDE2ClNUQVJURVIgPSAiL3Vzci9sb2NhbC9iaW4vc3RhcnQtdm5jLnNoIgoKCmRlZiBkZXNjcmlwdG9yKG51bWJl +ciwgc3RhdGU9InJ1bm5pbmciKToKICAgIHJldHVybiB7ImlkIjogZiJkaXNwbGF5OntudW1iZXJ9IiwgIm51bWJlciI6IG51bWJlciwKICAgICAgICAgICAg +InBvcnQiOiA2OTAwICsgbnVtYmVyLCAic3RhdGUiOiBzdGF0ZX0KCgpkZWYgbGlzdGVuaW5nKHBvcnQpOgogICAgdHJ5OgogICAgICAgIHdpdGggc29ja2V0 +LmNyZWF0ZV9jb25uZWN0aW9uKCgiMTI3LjAuMC4xIiwgcG9ydCksIHRpbWVvdXQ9MC4zKToKICAgICAgICAgICAgcmV0dXJuIFRydWUKICAgIGV4Y2VwdCBP +U0Vycm9yOgogICAgICAgIHJldHVybiBGYWxzZQoKCmRlZiByZWFkeShudW1iZXIpOgogICAgIiIiQSBib3VuZCBIVFRQIHBvcnQgYWxvbmUgZG9lcyBub3Qg +cHJvdmUgdGhhdCBhbiBYL1JGQiBzZXJ2ZXIgZXhpc3RzLiIiIgogICAgdHJ5OgogICAgICAgIHdpdGggc29ja2V0LmNyZWF0ZV9jb25uZWN0aW9uKCgiMTI3 +LjAuMC4xIiwgNTkwMCArIG51bWJlciksIHRpbWVvdXQ9MC41KSBhcyByZmI6CiAgICAgICAgICAgIHZlcnNpb24gPSBiIiIKICAgICAgICAgICAgd2hpbGUg +bGVuKHZlcnNpb24pIDwgMTI6CiAgICAgICAgICAgICAgICBwYXJ0ID0gcmZiLnJlY3YoMTIgLSBsZW4odmVyc2lvbikpCiAgICAgICAgICAgICAgICBpZiBu +b3QgcGFydDoKICAgICAgICAgICAgICAgICAgICByZXR1cm4gRmFsc2UKICAgICAgICAgICAgICAgIHZlcnNpb24gKz0gcGFydAogICAgICAgICAgICBpZiBu +b3QgdmVyc2lvbi5zdGFydHN3aXRoKGIiUkZCICIpOgogICAgICAgICAgICAgICAgcmV0dXJuIEZhbHNlCiAgICAgICAgY29ubmVjdGlvbiA9IGh0dHAuY2xp +ZW50LkhUVFBDb25uZWN0aW9uKCIxMjcuMC4wLjEiLCA2OTAwICsgbnVtYmVyLCB0aW1lb3V0PTAuNSkKICAgICAgICB0cnk6CiAgICAgICAgICAgIGNvbm5l +Y3Rpb24ucmVxdWVzdCgiSEVBRCIsICIvdm5jLmh0bWwiKQogICAgICAgICAgICByZXR1cm4gY29ubmVjdGlvbi5nZXRyZXNwb25zZSgpLnN0YXR1cyA9PSAy +MDAKICAgICAgICBmaW5hbGx5OgogICAgICAgICAgICBjb25uZWN0aW9uLmNsb3NlKCkKICAgIGV4Y2VwdCAoT1NFcnJvciwgaHR0cC5jbGllbnQuSFRUUEV4 +Y2VwdGlvbik6CiAgICAgICAgcmV0dXJuIEZhbHNlCgoKY2xhc3MgRGlzcGxheUNhdGFsb2c6CiAgICAiIiJTZXJpYWxpemVzIGFsbG9jYXRpb24gYW5kIHBl +cnNpc3RzIHJlcXVlc3QgcmVjZWlwdHMgYmVmb3JlIHN0YXJ0aW5nIHdvcmsuIiIiCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGRpcmVjdG9yeSwgb2NjdXBp +ZWQ9Tm9uZSk6CiAgICAgICAgc2VsZi5kaXJlY3RvcnkgPSBQYXRoKGRpcmVjdG9yeSkKICAgICAgICBzZWxmLmRpcmVjdG9yeS5ta2Rpcihtb2RlPTBvNzAw +LCBwYXJlbnRzPVRydWUsIGV4aXN0X29rPVRydWUpCiAgICAgICAgc2VsZi5wYXRoID0gc2VsZi5kaXJlY3RvcnkgLyAiY2F0YWxvZy5qc29uIgogICAgICAg +IHNlbGYubG9jayA9IHRocmVhZGluZy5STG9jaygpCiAgICAgICAgc2VsZi5vY2N1cGllZCA9IG9jY3VwaWVkIG9yIChsYW1iZGEgbjogUGF0aChmIi90bXAv +LlgxMS11bml4L1h7bn0iKS5leGlzdHMoKQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgb3IgbGlzdGVuaW5nKDU5MDAgKyBuKSBvciBs +aXN0ZW5pbmcoNjkwMCArIG4pKQogICAgICAgIHNlbGYucmVjb3JkcyA9IFtdCiAgICAgICAgaWYgc2VsZi5wYXRoLmV4aXN0cygpOgogICAgICAgICAgICBk +YXRhID0ganNvbi5sb2FkcyhzZWxmLnBhdGgucmVhZF90ZXh0KCkpCiAgICAgICAgICAgIGlmIGRhdGEuZ2V0KCJ2ZXJzaW9uIikgIT0gMToKICAgICAgICAg +ICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoInVuc3VwcG9ydGVkIGRpc3BsYXkgY2F0YWxvZyB2ZXJzaW9uIikKICAgICAgICAgICAgc2Vlbl9udW1iZXJzLCBz +ZWVuX3JlcXVlc3RzID0gc2V0KCksIHNldCgpCiAgICAgICAgICAgIGZvciByZWNvcmQgaW4gZGF0YVsiZGlzcGxheXMiXToKICAgICAgICAgICAgICAgIG51 +bWJlciwgcmVxdWVzdCA9IHJlY29yZFsibnVtYmVyIl0sIHJlY29yZFsicmVxdWVzdCJdCiAgICAgICAgICAgICAgICBpZiB0eXBlKG51bWJlcikgaXMgbm90 +IGludCBvciBub3QgMiA8PSBudW1iZXIgPD0gTUFYX0RJU1BMQVlTOgogICAgICAgICAgICAgICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoImludmFsaWQgZGlz +cGxheSBudW1iZXIiKQogICAgICAgICAgICAgICAgaWYgc3RyKHV1aWQuVVVJRChyZXF1ZXN0KSkgIT0gcmVxdWVzdCBvciBudW1iZXIgaW4gc2Vlbl9udW1i +ZXJzIG9yIHJlcXVlc3QgaW4gc2Vlbl9yZXF1ZXN0czoKICAgICAgICAgICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJpbnZhbGlkIGRpc3BsYXkgaWRl +bnRpdHkiKQogICAgICAgICAgICAgICAgc2Vlbl9udW1iZXJzLmFkZChudW1iZXIpCiAgICAgICAgICAgICAgICBzZWVuX3JlcXVlc3RzLmFkZChyZXF1ZXN0 +KQogICAgICAgICAgICAgICAgc2VsZi5yZWNvcmRzLmFwcGVuZCh7Im51bWJlciI6IG51bWJlciwgInJlcXVlc3QiOiByZXF1ZXN0fSkKCiAgICBkZWYgbnVt +YmVycyhzZWxmKToKICAgICAgICB3aXRoIHNlbGYubG9jazoKICAgICAgICAgICAgcmV0dXJuIHNvcnRlZChyZWNvcmRbIm51bWJlciJdIGZvciByZWNvcmQg +aW4gc2VsZi5yZWNvcmRzKQoKICAgIGRlZiBhbGxvY2F0ZShzZWxmLCByZXF1ZXN0KToKICAgICAgICBpZiBub3QgaXNpbnN0YW5jZShyZXF1ZXN0LCBzdHIp +IG9yIHN0cih1dWlkLlVVSUQocmVxdWVzdCkpICE9IHJlcXVlc3Q6CiAgICAgICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoImludmFsaWQgcmVxdWVzdCBpZGVu +dGl0eSIpCiAgICAgICAgd2l0aCBzZWxmLmxvY2s6CiAgICAgICAgICAgIGZvciByZWNvcmQgaW4gc2VsZi5yZWNvcmRzOgogICAgICAgICAgICAgICAgaWYg +cmVjb3JkWyJyZXF1ZXN0Il0gPT0gcmVxdWVzdDoKICAgICAgICAgICAgICAgICAgICByZXR1cm4gcmVjb3JkWyJudW1iZXIiXQogICAgICAgICAgICB1c2Vk +ID0gc2V0KHNlbGYubnVtYmVycygpKQogICAgICAgICAgICBudW1iZXIgPSBuZXh0KChuIGZvciBuIGluIHJhbmdlKDIsIE1BWF9ESVNQTEFZUyArIDEpCiAg +ICAgICAgICAgICAgICAgICAgICAgICAgIGlmIG4gbm90IGluIHVzZWQgYW5kIG5vdCBzZWxmLm9jY3VwaWVkKG4pKSwgTm9uZSkKICAgICAgICAgICAgaWYg +bnVtYmVyIGlzIE5vbmU6CiAgICAgICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJkaXNwbGF5IGNhcGFjaXR5IGV4aGF1c3RlZCIpCiAgICAgICAgICAg +IHJlY29yZHMgPSBzZWxmLnJlY29yZHMgKyBbeyJudW1iZXIiOiBudW1iZXIsICJyZXF1ZXN0IjogcmVxdWVzdH1dCiAgICAgICAgICAgIGZkLCB0ZW1wb3Jh +cnkgPSB0ZW1wZmlsZS5ta3N0ZW1wKHByZWZpeD0iY2F0YWxvZy0iLCBkaXI9c2VsZi5kaXJlY3RvcnkpCiAgICAgICAgICAgIHRyeToKICAgICAgICAgICAg +ICAgIHdpdGggb3MuZmRvcGVuKGZkLCAidyIpIGFzIG91dHB1dDoKICAgICAgICAgICAgICAgICAgICBqc29uLmR1bXAoeyJ2ZXJzaW9uIjogMSwgImRpc3Bs +YXlzIjogcmVjb3Jkc30sIG91dHB1dCkKICAgICAgICAgICAgICAgICAgICBvdXRwdXQuZmx1c2goKQogICAgICAgICAgICAgICAgICAgIG9zLmZzeW5jKG91 +dHB1dC5maWxlbm8oKSkKICAgICAgICAgICAgICAgIG9zLnJlcGxhY2UodGVtcG9yYXJ5LCBzZWxmLnBhdGgpCiAgICAgICAgICAgIGZpbmFsbHk6CiAgICAg +ICAgICAgICAgICBpZiBvcy5wYXRoLmV4aXN0cyh0ZW1wb3JhcnkpOgogICAgICAgICAgICAgICAgICAgIG9zLnVubGluayh0ZW1wb3JhcnkpCiAgICAgICAg +ICAgIHNlbGYucmVjb3JkcyA9IHJlY29yZHMKICAgICAgICAgICAgcmV0dXJuIG51bWJlcgoKCmNsYXNzIE93bmVkUHJvY2VzczoKICAgICIiIkEgaGFuZGxl +IGZvciBhIGRhZW1vbiBzdGFydGVkIGJ5IGRidXMtbGF1bmNoIHJhdGhlciB0aGFuIGJ5IFBvcGVuLiIiIgoKICAgIGRlZiBfX2luaXRfXyhzZWxmLCBwaWQp +OgogICAgICAgIHNlbGYucGlkID0gcGlkCgogICAgZGVmIHRlcm1pbmF0ZShzZWxmKToKICAgICAgICB0cnk6CiAgICAgICAgICAgIG9zLmtpbGwoc2VsZi5w +aWQsIDE1KQogICAgICAgIGV4Y2VwdCBPU0Vycm9yOgogICAgICAgICAgICBwYXNzCgogICAgZGVmIHBvbGwoc2VsZik6CiAgICAgICAgdHJ5OgogICAgICAg +ICAgICBvcy5raWxsKHNlbGYucGlkLCAwKQogICAgICAgICAgICByZXR1cm4gTm9uZQogICAgICAgIGV4Y2VwdCBPU0Vycm9yOgogICAgICAgICAgICByZXR1 +cm4gMAoKCmNsYXNzIERpc3BsYXlTZXJ2aWNlOgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGNhdGFsb2csIHJ1bnRpbWUsIHN0YXJ0ZXI9U1RBUlRFUik6CiAg +ICAgICAgc2VsZi5jYXRhbG9nID0gY2F0YWxvZwogICAgICAgIHNlbGYucnVudGltZSA9IFBhdGgocnVudGltZSkKICAgICAgICBzZWxmLnN0YXJ0ZXIgPSBz +dGFydGVyCiAgICAgICAgc2VsZi5sb2NrID0gdGhyZWFkaW5nLkxvY2soKQogICAgICAgIHNlbGYuam9icyA9IHt9CiAgICAgICAgc2VsZi5zdGF0ZXMgPSB7 +fQogICAgICAgIHNlbGYucHJvY2Vzc2VzID0ge30KICAgICAgICBzZWxmLnNodXRkb3duID0gdGhyZWFkaW5nLkV2ZW50KCkKCiAgICBkZWYgc3RhcnQoc2Vs +ZiwgbnVtYmVyKToKICAgICAgICB3aXRoIHNlbGYubG9jazoKICAgICAgICAgICAgaWYgbnVtYmVyIGluIHNlbGYuam9iczoKICAgICAgICAgICAgICAgIHJl +dHVybiBzZWxmLmpvYnNbbnVtYmVyXQogICAgICAgICAgICBmaW5pc2hlZCA9IHRocmVhZGluZy5FdmVudCgpCiAgICAgICAgICAgIHNlbGYuam9ic1tudW1i +ZXJdID0gZmluaXNoZWQKICAgICAgICAgICAgc2VsZi5zdGF0ZXNbbnVtYmVyXSA9ICJsYXVuY2hpbmciCiAgICAgICAgICAgIHRocmVhZGluZy5UaHJlYWQo +dGFyZ2V0PXNlbGYuc3VwZXJ2aXNlLCBhcmdzPShudW1iZXIsIGZpbmlzaGVkKSwgZGFlbW9uPVRydWUpLnN0YXJ0KCkKICAgICAgICAgICAgcmV0dXJuIGZp +bmlzaGVkCgogICAgZGVmIHRlcm1pbmF0ZShzZWxmLCBudW1iZXIpOgogICAgICAgIGZvciBwcm9jZXNzIGluIHNlbGYucHJvY2Vzc2VzLnBvcChudW1iZXIs +IFtdKToKICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgcHJvY2Vzcy50ZXJtaW5hdGUoKQogICAgICAgICAgICBleGNlcHQgT1NFcnJvcjoKICAg +ICAgICAgICAgICAgIHBhc3MKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgd2FpdF9mb3JfcG9ydChwb3J0LCB0aW1lb3V0PTIwKToKICAgICAgICBkZWFk +bGluZSA9IHRpbWUubW9ub3RvbmljKCkgKyB0aW1lb3V0CiAgICAgICAgd2hpbGUgdGltZS5tb25vdG9uaWMoKSA8IGRlYWRsaW5lOgogICAgICAgICAgICBp +ZiBsaXN0ZW5pbmcocG9ydCk6CiAgICAgICAgICAgICAgICByZXR1cm4gVHJ1ZQogICAgICAgICAgICB0aW1lLnNsZWVwKDAuMSkKICAgICAgICByZXR1cm4g +RmFsc2UKCiAgICBkZWYgc3RhcnRfY29tcG9uZW50cyhzZWxmLCBudW1iZXIsIGVudmlyb25tZW50LCBydW50aW1lKToKICAgICAgICBpZiByZWFkeShudW1i +ZXIpOgogICAgICAgICAgICByZXR1cm4KICAgICAgICBzZWxmLnRlcm1pbmF0ZShudW1iZXIpCiAgICAgICAgdm5jID0gc2h1dGlsLndoaWNoKCJYdm5jIikg +b3Igc2h1dGlsLndoaWNoKCJYdGlnZXJ2bmMiKQogICAgICAgIHdlYnNvY2tpZnkgPSBzaHV0aWwud2hpY2goIndlYnNvY2tpZnkiKQogICAgICAgIGlmIG5v +dCB2bmMgb3Igbm90IHdlYnNvY2tpZnk6CiAgICAgICAgICAgIHJhaXNlIFJ1bnRpbWVFcnJvcigiZGlzcGxheSBydW50aW1lIHVuYXZhaWxhYmxlIikKICAg +ICAgICByZmJfcG9ydCwgbm92bmNfcG9ydCA9IDU5MDAgKyBudW1iZXIsIDY5MDAgKyBudW1iZXIKICAgICAgICBwcm9jZXNzZXMgPSBbXQogICAgICAgIHBy +b2Nlc3Nlcy5hcHBlbmQoc3VicHJvY2Vzcy5Qb3BlbigKICAgICAgICAgICAgW3ZuYywgZiI6e251bWJlcn0iLCAiLWdlb21ldHJ5Iiwgb3MuZW52aXJvbi5n +ZXQoIkNNVVhfVk5DX0dFT01FVFJZIiwgIjE0NDB4OTAwIiksCiAgICAgICAgICAgICAiLWRlcHRoIiwgIjI0IiwgIi1yZmJwb3J0Iiwgc3RyKHJmYl9wb3J0 +KSwgIi1sb2NhbGhvc3QiLAogICAgICAgICAgICAgIi1TZWN1cml0eVR5cGVzIiwgIk5vbmUiLCAiLUFsd2F5c1NoYXJlZCJdLAogICAgICAgICAgICBlbnY9 +ZW52aXJvbm1lbnQsIHN0ZGluPXN1YnByb2Nlc3MuREVWTlVMTCwKICAgICAgICAgICAgc3Rkb3V0PXN1YnByb2Nlc3MuREVWTlVMTCwgc3RkZXJyPXN1YnBy +b2Nlc3MuREVWTlVMTCkpCiAgICAgICAgc2VsZi5wcm9jZXNzZXNbbnVtYmVyXSA9IHByb2Nlc3NlcwogICAgICAgIGlmIG5vdCBzZWxmLndhaXRfZm9yX3Bv +cnQocmZiX3BvcnQpOgogICAgICAgICAgICByYWlzZSBSdW50aW1lRXJyb3IoIlh2bmMgZGlkIG5vdCBiZWNvbWUgcmVhZHkiKQogICAgICAgIGJ1cyA9IHN1 +YnByb2Nlc3MucnVuKFsiZGJ1cy1sYXVuY2giLCAiLS1zaC1zeW50YXgiXSwgZW52PWVudmlyb25tZW50LAogICAgICAgICAgICAgICAgICAgICAgICAgICAg +IGNhcHR1cmVfb3V0cHV0PVRydWUsIHRleHQ9VHJ1ZSwgY2hlY2s9VHJ1ZSkuc3Rkb3V0CiAgICAgICAgZm9yIGxpbmUgaW4gYnVzLnNwbGl0bGluZXMoKToK +ICAgICAgICAgICAgaWYgIj0iIG5vdCBpbiBsaW5lOgogICAgICAgICAgICAgICAgY29udGludWUKICAgICAgICAgICAga2V5LCB2YWx1ZSA9IGxpbmUuc3Bs +aXQoIj0iLCAxKQogICAgICAgICAgICBlbnZpcm9ubWVudFtrZXldID0gdmFsdWUuc3RyaXAoKS5zdHJpcCgiJyIpCiAgICAgICAgdHJ5OgogICAgICAgICAg +ICBkYnVzX3BpZCA9IGludChlbnZpcm9ubWVudC5nZXQoIkRCVVNfU0VTU0lPTl9CVVNfUElEIiwgIjAiKSkKICAgICAgICBleGNlcHQgVmFsdWVFcnJvcjoK +ICAgICAgICAgICAgZGJ1c19waWQgPSAwCiAgICAgICAgaWYgZGJ1c19waWQgPiAxOgogICAgICAgICAgICBwcm9jZXNzZXMuYXBwZW5kKE93bmVkUHJvY2Vz +cyhkYnVzX3BpZCkpCiAgICAgICAgZm9yIGV4ZWN1dGFibGUsIGFyZ3MgaW4gWwogICAgICAgICAgICAoIm9wZW5ib3giLCBbXSksICgidGludDIiLCBbIi1j +IiwgIi9ldGMvY211eC90aW50MnJjIl0pLAogICAgICAgICAgICAoInZuY2NvbmZpZyIsIFsiLW5vd2luIl0pLAogICAgICAgICAgICAod2Vic29ja2lmeSwg +WyItLXdlYiIsICIvdXNyL3NoYXJlL25vdm5jIiwgIi0taGVhcnRiZWF0IiwgIjMwIiwKICAgICAgICAgICAgICAgICAgICAgICAgICBmIls6Ol06e25vdm5j +X3BvcnR9IiwgZiIxMjcuMC4wLjE6e3JmYl9wb3J0fSJdKSwKICAgICAgICBdOgogICAgICAgICAgICBwYXRoID0gZXhlY3V0YWJsZSBpZiAiLyIgaW4gZXhl +Y3V0YWJsZSBlbHNlIHNodXRpbC53aGljaChleGVjdXRhYmxlKQogICAgICAgICAgICBpZiBub3QgcGF0aDoKICAgICAgICAgICAgICAgIGlmIGV4ZWN1dGFi +bGUgPT0gInZuY2NvbmZpZyI6CiAgICAgICAgICAgICAgICAgICAgY29udGludWUKICAgICAgICAgICAgICAgIHJhaXNlIFJ1bnRpbWVFcnJvcihmIm1pc3Np +bmcgZGlzcGxheSBjb21wb25lbnQ6IHtleGVjdXRhYmxlfSIpCiAgICAgICAgICAgIHByb2Nlc3MgPSBzdWJwcm9jZXNzLlBvcGVuKFtwYXRoXSArIGFyZ3Ms +IGVudj1lbnZpcm9ubWVudCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3RkaW49c3VicHJvY2Vzcy5ERVZOVUxMLAogICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdGRvdXQ9c3VicHJvY2Vzcy5ERVZOVUxMLCBzdGRlcnI9c3VicHJvY2Vzcy5ERVZOVUxMKQogICAg +ICAgICAgICBwcm9jZXNzZXMuYXBwZW5kKHByb2Nlc3MpCiAgICAgICAgaWYgbm90IHNlbGYud2FpdF9mb3JfcG9ydChub3ZuY19wb3J0LCB0aW1lb3V0PTEw +KToKICAgICAgICAgICAgcmFpc2UgUnVudGltZUVycm9yKCJub1ZOQyBkaWQgbm90IGJlY29tZSByZWFkeSIpCiAgICAgICAgKHJ1bnRpbWUgLyAiZW52Iiku +d3JpdGVfdGV4dChmImV4cG9ydCBESVNQTEFZPTp7bnVtYmVyfVxuIikKCiAgICBkZWYgc3VwZXJ2aXNlKHNlbGYsIG51bWJlciwgZmluaXNoZWQpOgogICAg +ICAgIHJ1bnRpbWUgPSBzZWxmLnJ1bnRpbWUgLyBzdHIobnVtYmVyKQogICAgICAgIHJ1bnRpbWUubWtkaXIobW9kZT0wbzcwMCwgcGFyZW50cz1UcnVlLCBl +eGlzdF9vaz1UcnVlKQogICAgICAgIGVudmlyb25tZW50ID0gZGljdChvcy5lbnZpcm9uLCBESVNQTEFZPWYiOntudW1iZXJ9IiwgQ01VWF9ERVNLVE9QX1JV +TlRJTUVfRElSPXN0cihydW50aW1lKSkKICAgICAgICBmb3Iga2V5IGluICgiTk9USUZZX1NPQ0tFVCIsICJEQlVTX1NFU1NJT05fQlVTX0FERFJFU1MiLCAi +REJVU19TRVNTSU9OX0JVU19QSUQiLAogICAgICAgICAgICAgICAgICAgICJBVF9TUElfQlVTX0FERFJFU1MiLCAiQVRfU1BJX0JVUyIpOgogICAgICAgICAg +ICBlbnZpcm9ubWVudC5wb3Aoa2V5LCBOb25lKQogICAgICAgIHdoaWxlIG5vdCBzZWxmLnNodXRkb3duLmlzX3NldCgpOgogICAgICAgICAgICB0cnk6CiAg +ICAgICAgICAgICAgICAjIFRoZSBzdXBlcnZpc29yIGlzIGEgaGVhbHRoIGNoZWNrLCBub3QgYSB0aW1lci1kcml2ZW4gcmVzdGFydC4KICAgICAgICAgICAg +ICAgICMgUHJlc2VydmUgYW4gYWN0aXZlIFJGQi9ub1ZOQyBzZXNzaW9uIHVudGlsIGl0cyBvd25lZCBlbmRwb2ludHMgZmFpbC4KICAgICAgICAgICAgICAg +IGlmIG5vdCBzZWxmLnByb2Nlc3Nlcy5nZXQobnVtYmVyKSBvciBub3QgcmVhZHkobnVtYmVyKToKICAgICAgICAgICAgICAgICAgICBzZWxmLnN0YXJ0X2Nv +bXBvbmVudHMobnVtYmVyLCBlbnZpcm9ubWVudCwgcnVudGltZSkKICAgICAgICAgICAgICAgIHN0YXRlID0gInJ1bm5pbmciIGlmIHJlYWR5KG51bWJlcikg +ZWxzZSAidW5hdmFpbGFibGUiCiAgICAgICAgICAgIGV4Y2VwdCAoT1NFcnJvciwgUnVudGltZUVycm9yLCBzdWJwcm9jZXNzLlN1YnByb2Nlc3NFcnJvcik6 +CiAgICAgICAgICAgICAgICBzZWxmLnRlcm1pbmF0ZShudW1iZXIpCiAgICAgICAgICAgICAgICBzdGF0ZSA9ICJ1bmF2YWlsYWJsZSIKICAgICAgICAgICAg +d2l0aCBzZWxmLmxvY2s6CiAgICAgICAgICAgICAgICBzZWxmLnN0YXRlc1tudW1iZXJdID0gc3RhdGUKICAgICAgICAgICAgZmluaXNoZWQuc2V0KCkKICAg +ICAgICAgICAgc2VsZi5zaHV0ZG93bi53YWl0KDMwKQogICAgICAgIHNlbGYudGVybWluYXRlKG51bWJlcikKCiAgICBkZWYgc25hcHNob3Qoc2VsZik6CiAg +ICAgICAgd2l0aCBzZWxmLmxvY2s6CiAgICAgICAgICAgIHJldHVybiB7InZlcnNpb24iOiAxLCAiY2FuQ3JlYXRlIjogbGVuKHNlbGYuam9icykgPCBNQVhf +RElTUExBWVMgLSAxLAogICAgICAgICAgICAgICAgICAgICJkaXNwbGF5cyI6IFtkZXNjcmlwdG9yKDEsICJydW5uaW5nIiBpZiByZWFkeSgxKSBlbHNlICJ1 +bmF2YWlsYWJsZSIpXQogICAgICAgICAgICAgICAgICAgICsgW2Rlc2NyaXB0b3Iobiwgc2VsZi5zdGF0ZXMuZ2V0KG4sICJsYXVuY2hpbmciKSkgZm9yIG4g +aW4gc2VsZi5jYXRhbG9nLm51bWJlcnMoKV19CgogICAgZGVmIGhhbmRsZShzZWxmLCByZXF1ZXN0KToKICAgICAgICBpZiByZXF1ZXN0LmdldCgiYWN0aW9u +IikgPT0gImxpc3QiOgogICAgICAgICAgICByZXR1cm4gc2VsZi5zbmFwc2hvdCgpCiAgICAgICAgaWYgcmVxdWVzdC5nZXQoImFjdGlvbiIpICE9ICJjcmVh +dGUiOgogICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJ1bnN1cHBvcnRlZCBkaXNwbGF5IGFjdGlvbiIpCiAgICAgICAgbnVtYmVyID0gc2VsZi5jYXRh +bG9nLmFsbG9jYXRlKHJlcXVlc3QuZ2V0KCJyZXF1ZXN0IikpCiAgICAgICAgc2VsZi5zdGFydChudW1iZXIpLndhaXQoNTUpCiAgICAgICAgcmVzdWx0ID0g +c2VsZi5zbmFwc2hvdCgpCiAgICAgICAgcmVzdWx0WyJjcmVhdGVkIl0gPSBmImRpc3BsYXk6e251bWJlcn0iCiAgICAgICAgaWYgc2VsZi5zdGF0ZXMuZ2V0 +KG51bWJlcikgIT0gInJ1bm5pbmciOgogICAgICAgICAgICByZXN1bHRbImVycm9yIl0gPSAiZGlzcGxheV9zdGFydF9mYWlsZWQiCiAgICAgICAgcmV0dXJu +IHJlc3VsdAoKCmRlZiBzZXJ2ZShydW50aW1lKToKICAgIHJ1bnRpbWUgPSBQYXRoKHJ1bnRpbWUpCiAgICBydW50aW1lLm1rZGlyKG1vZGU9MG83MDAsIHBh +cmVudHM9VHJ1ZSwgZXhpc3Rfb2s9VHJ1ZSkKICAgIHdpdGggKHJ1bnRpbWUgLyAiZGlzcGxheS1jb250cm9sLmxvY2siKS5vcGVuKCJ3IikgYXMgbG9jazoK +ICAgICAgICB0cnk6CiAgICAgICAgICAgIGZjbnRsLmZsb2NrKGxvY2ssIGZjbnRsLkxPQ0tfRVggfCBmY250bC5MT0NLX05CKQogICAgICAgIGV4Y2VwdCBC +bG9ja2luZ0lPRXJyb3I6CiAgICAgICAgICAgIHJldHVybgogICAgICAgIGlmIG5vdCBhbGwoc2h1dGlsLndoaWNoKG5hbWUpIGZvciBuYW1lIGluICgid2Vi +c29ja2lmeSIsICJvcGVuYm94IiwgImRidXMtbGF1bmNoIikpOgogICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJkaXNwbGF5IHJ1bnRpbWUgdW5hdmFp +bGFibGUiKQogICAgICAgIGlmIG5vdCAoc2h1dGlsLndoaWNoKCJYdm5jIikgb3Igc2h1dGlsLndoaWNoKCJYdGlnZXJ2bmMiKSk6CiAgICAgICAgICAgIHJh +aXNlIFZhbHVlRXJyb3IoImRpc3BsYXkgcnVudGltZSB1bmF2YWlsYWJsZSIpCiAgICAgICAgc2VydmljZSA9IERpc3BsYXlTZXJ2aWNlKERpc3BsYXlDYXRh +bG9nKFBhdGguaG9tZSgpIC8gIi5jbXV4IiAvICJkaXNwbGF5cyIpLCBydW50aW1lIC8gImRpc3BsYXlzIikKICAgICAgICBmb3IgbnVtYmVyIGluIHNlcnZp +Y2UuY2F0YWxvZy5udW1iZXJzKCk6CiAgICAgICAgICAgIHNlcnZpY2Uuc3RhcnQobnVtYmVyKQogICAgICAgIHBhdGggPSBydW50aW1lIC8gImRpc3BsYXkt +Y29udHJvbC5zb2NrIgogICAgICAgIGlmIHBhdGguZXhpc3RzKCk6CiAgICAgICAgICAgIHBhdGgudW5saW5rKCkKCiAgICAgICAgY2xhc3MgSGFuZGxlcihz +b2NrZXRzZXJ2ZXIuU3RyZWFtUmVxdWVzdEhhbmRsZXIpOgogICAgICAgICAgICBkZWYgaGFuZGxlKHNlbGYpOgogICAgICAgICAgICAgICAgc2VsZi5jb25u +ZWN0aW9uLnNldHRpbWVvdXQoNjApCiAgICAgICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICAgICAgcmF3ID0gc2VsZi5yZmlsZS5yZWFkbGluZSg0 +MDk3KQogICAgICAgICAgICAgICAgICAgIGlmIGxlbihyYXcpID4gNDA5NjoKICAgICAgICAgICAgICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJvcigiZGlz +cGxheSByZXF1ZXN0IHRvbyBsYXJnZSIpCiAgICAgICAgICAgICAgICAgICAgcmVzdWx0ID0gc2VydmljZS5oYW5kbGUoanNvbi5sb2FkcyhyYXcpKQogICAg +ICAgICAgICAgICAgZXhjZXB0IChWYWx1ZUVycm9yLCBLZXlFcnJvciwgVHlwZUVycm9yLCBPU0Vycm9yKToKICAgICAgICAgICAgICAgICAgICByZXN1bHQg +PSB7ImVycm9yIjogImRpc3BsYXlfcmVxdWVzdF9mYWlsZWQifQogICAgICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgIHNlbGYud2ZpbGUu +d3JpdGUoanNvbi5kdW1wcyhyZXN1bHQpLmVuY29kZSgpICsgYiJcbiIpCiAgICAgICAgICAgICAgICBleGNlcHQgT1NFcnJvcjoKICAgICAgICAgICAgICAg +ICAgICBwYXNzICAjIFRoZSBwZXJzaXN0ZWQgcmVjZWlwdCByZW1haW5zIHJlcGxheWFibGUgYWZ0ZXIgZGlzY29ubmVjdC4KCiAgICAgICAgY2xhc3MgU2Vy +dmVyKHNvY2tldHNlcnZlci5UaHJlYWRpbmdVbml4U3RyZWFtU2VydmVyKToKICAgICAgICAgICAgZGFlbW9uX3RocmVhZHMgPSBUcnVlCgogICAgICAgIHBy +ZXZpb3VzX3VtYXNrID0gb3MudW1hc2soMG8wNzcpCiAgICAgICAgdHJ5OgogICAgICAgICAgICB3aXRoIFNlcnZlcihzdHIocGF0aCksIEhhbmRsZXIpIGFz +IHNlcnZlcjoKICAgICAgICAgICAgICAgIG9zLnVtYXNrKHByZXZpb3VzX3VtYXNrKQogICAgICAgICAgICAgICAgc2VydmVyLnNlcnZlX2ZvcmV2ZXIoKQog +ICAgICAgIGZpbmFsbHk6CiAgICAgICAgICAgIG9zLnVtYXNrKHByZXZpb3VzX3VtYXNrKQogICAgICAgICAgICBzZXJ2aWNlLnNodXRkb3duLnNldCgpCiAg +ICAgICAgICAgIHBhdGgudW5saW5rKG1pc3Npbmdfb2s9VHJ1ZSkKCgpkZWYgbWFpbigpOgogICAgcGFyc2VyID0gYXJncGFyc2UuQXJndW1lbnRQYXJzZXIo +ZGVzY3JpcHRpb249X19kb2NfXykKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoImFjdGlvbiIsIGNob2ljZXM9WyJsaXN0IiwgImNyZWF0ZSIsICJzZXJ2ZSJd +KQogICAgcGFyc2VyLmFkZF9hcmd1bWVudCgiLS1yZXF1ZXN0LWlkIikKICAgIGFyZ3MgPSBwYXJzZXIucGFyc2VfYXJncygpCiAgICBydW50aW1lID0gb3Mu +ZW52aXJvbi5nZXQoIkNNVVhfREVTS1RPUF9SVU5USU1FX0RJUiIsICIvcnVuL2NtdXgtZGVza3RvcCIpCiAgICBpZiBhcmdzLmFjdGlvbiA9PSAic2VydmUi +OgogICAgICAgIHNlcnZlKHJ1bnRpbWUpCiAgICAgICAgcmV0dXJuIDAKICAgIGlmIGFyZ3MuYWN0aW9uID09ICJjcmVhdGUiIGFuZCBub3QgYXJncy5yZXF1 +ZXN0X2lkOgogICAgICAgIHBhcnNlci5lcnJvcigiY3JlYXRlIHJlcXVpcmVzIC0tcmVxdWVzdC1pZCAocmV0YWluIGl0IHdoZW4gcmV0cnlpbmcpIikKICAg +IHRyeToKICAgICAgICB3aXRoIHNvY2tldC5zb2NrZXQoc29ja2V0LkFGX1VOSVgsIHNvY2tldC5TT0NLX1NUUkVBTSkgYXMgY2xpZW50OgogICAgICAgICAg +ICBjbGllbnQuc2V0dGltZW91dCg2MCkKICAgICAgICAgICAgY2xpZW50LmNvbm5lY3Qoc3RyKFBhdGgocnVudGltZSkgLyAiZGlzcGxheS1jb250cm9sLnNv +Y2siKSkKICAgICAgICAgICAgY2xpZW50LnNlbmRhbGwoanNvbi5kdW1wcyh7ImFjdGlvbiI6IGFyZ3MuYWN0aW9uLCAicmVxdWVzdCI6IGFyZ3MucmVxdWVz +dF9pZH0pLmVuY29kZSgpICsgYiJcbiIpCiAgICAgICAgICAgIHdpdGggY2xpZW50Lm1ha2VmaWxlKCJyYiIpIGFzIHN0cmVhbToKICAgICAgICAgICAgICAg +IHJlc3VsdCA9IGpzb24ubG9hZHMoc3RyZWFtLnJlYWRsaW5lKDY1NTM2KSkKICAgIGV4Y2VwdCAoT1NFcnJvciwgVmFsdWVFcnJvcik6CiAgICAgICAgcmVz +dWx0ID0geyJlcnJvciI6ICJkaXNwbGF5X3J1bnRpbWVfdW5hdmFpbGFibGUifQogICAgcHJpbnQoanNvbi5kdW1wcyhyZXN1bHQpKQogICAgcmV0dXJuIDEg +aWYgImVycm9yIiBpbiByZXN1bHQgZWxzZSAwCgoKaWYgX19uYW1lX18gPT0gIl9fbWFpbl9fIjoKICAgIHJhaXNlIFN5c3RlbUV4aXQobWFpbigpKQo= """ -import argparse -import fcntl -import http.client -import json -import os -from pathlib import Path -import shutil -import socket -import socketserver -import subprocess -import tempfile -import threading -import time -import uuid - -MAX_DISPLAYS = 16 -STARTER = "/usr/local/bin/start-vnc.sh" - - -def descriptor(number, state="running"): - return {"id": f"display:{number}", "number": number, - "port": 6900 + number, "state": state} - - -def listening(port): - try: - with socket.create_connection(("127.0.0.1", port), timeout=0.3): - return True - except OSError: - return False - - -def ready(number): - """A bound HTTP port alone does not prove that an X/RFB server exists.""" - try: - with socket.create_connection(("127.0.0.1", 5900 + number), timeout=0.5) as rfb: - version = b"" - while len(version) < 12: - part = rfb.recv(12 - len(version)) - if not part: - return False - version += part - if not version.startswith(b"RFB "): - return False - connection = http.client.HTTPConnection("127.0.0.1", 6900 + number, timeout=0.5) - try: - connection.request("HEAD", "/vnc.html") - return connection.getresponse().status == 200 - finally: - connection.close() - except (OSError, http.client.HTTPException): - return False - - -class DisplayCatalog: - """Serializes allocation and persists request receipts before starting work.""" - - def __init__(self, directory, occupied=None): - self.directory = Path(directory) - self.directory.mkdir(mode=0o700, parents=True, exist_ok=True) - self.path = self.directory / "catalog.json" - self.lock = threading.RLock() - self.occupied = occupied or (lambda n: Path(f"/tmp/.X11-unix/X{n}").exists() - or listening(5900 + n) or listening(6900 + n)) - self.records = [] - if self.path.exists(): - data = json.loads(self.path.read_text()) - if data.get("version") != 1: - raise ValueError("unsupported display catalog version") - seen_numbers, seen_requests = set(), set() - for record in data["displays"]: - number, request = record["number"], record["request"] - if type(number) is not int or not 2 <= number <= MAX_DISPLAYS: - raise ValueError("invalid display number") - if str(uuid.UUID(request)) != request or number in seen_numbers or request in seen_requests: - raise ValueError("invalid display identity") - seen_numbers.add(number) - seen_requests.add(request) - self.records.append({"number": number, "request": request}) - - def numbers(self): - with self.lock: - return sorted(record["number"] for record in self.records) - - def allocate(self, request): - if not isinstance(request, str) or str(uuid.UUID(request)) != request: - raise ValueError("invalid request identity") - with self.lock: - for record in self.records: - if record["request"] == request: - return record["number"] - used = set(self.numbers()) - number = next((n for n in range(2, MAX_DISPLAYS + 1) - if n not in used and not self.occupied(n)), None) - if number is None: - raise ValueError("display capacity exhausted") - records = self.records + [{"number": number, "request": request}] - fd, temporary = tempfile.mkstemp(prefix="catalog-", dir=self.directory) - try: - with os.fdopen(fd, "w") as output: - json.dump({"version": 1, "displays": records}, output) - output.flush() - os.fsync(output.fileno()) - os.replace(temporary, self.path) - finally: - if os.path.exists(temporary): - os.unlink(temporary) - self.records = records - return number - - -class OwnedProcess: - """A handle for a daemon started by dbus-launch rather than by Popen.""" - - def __init__(self, pid): - self.pid = pid - - def terminate(self): - try: - os.kill(self.pid, 15) - except OSError: - pass - - def poll(self): - try: - os.kill(self.pid, 0) - return None - except OSError: - return 0 - - -class DisplayService: - def __init__(self, catalog, runtime, starter=STARTER): - self.catalog = catalog - self.runtime = Path(runtime) - self.starter = starter - self.lock = threading.Lock() - self.jobs = {} - self.states = {} - self.processes = {} - self.shutdown = threading.Event() - - def start(self, number): - with self.lock: - if number in self.jobs: - return self.jobs[number] - finished = threading.Event() - self.jobs[number] = finished - self.states[number] = "launching" - threading.Thread(target=self.supervise, args=(number, finished), daemon=True).start() - return finished - - def terminate(self, number): - for process in self.processes.pop(number, []): - try: - process.terminate() - except OSError: - pass - - @staticmethod - def wait_for_port(port, timeout=20): - deadline = time.monotonic() + timeout - while time.monotonic() < deadline: - if listening(port): - return True - time.sleep(0.1) - return False - - def start_components(self, number, environment, runtime): - self.terminate(number) - vnc = shutil.which("Xvnc") or shutil.which("Xtigervnc") - websockify = shutil.which("websockify") - if not vnc or not websockify: - raise RuntimeError("display runtime unavailable") - rfb_port, novnc_port = 5900 + number, 6900 + number - processes = [] - processes.append(subprocess.Popen( - [vnc, f":{number}", "-geometry", os.environ.get("CMUX_VNC_GEOMETRY", "1440x900"), - "-depth", "24", "-rfbport", str(rfb_port), "-localhost", - "-SecurityTypes", "None", "-AlwaysShared"], - env=environment, stdin=subprocess.DEVNULL, - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)) - self.processes[number] = processes - if not self.wait_for_port(rfb_port): - raise RuntimeError("Xvnc did not become ready") - bus = subprocess.run(["dbus-launch", "--sh-syntax"], env=environment, - capture_output=True, text=True, check=True).stdout - for line in bus.splitlines(): - if "=" not in line: - continue - key, value = line.split("=", 1) - environment[key] = value.strip().strip("'") - try: - dbus_pid = int(environment.get("DBUS_SESSION_BUS_PID", "0")) - except ValueError: - dbus_pid = 0 - if dbus_pid > 1: - processes.append(OwnedProcess(dbus_pid)) - for executable, args in [ - ("openbox", []), ("tint2", ["-c", "/etc/cmux/tint2rc"]), - ("vncconfig", ["-nowin"]), - (websockify, ["--web", "/usr/share/novnc", "--heartbeat", "30", - f"[::]:{novnc_port}", f"127.0.0.1:{rfb_port}"]), - ]: - path = executable if "/" in executable else shutil.which(executable) - if not path: - if executable == "vncconfig": - continue - raise RuntimeError(f"missing display component: {executable}") - process = subprocess.Popen([path] + args, env=environment, - stdin=subprocess.DEVNULL, - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - processes.append(process) - if not self.wait_for_port(novnc_port, timeout=10): - raise RuntimeError("noVNC did not become ready") - (runtime / "env").write_text(f"export DISPLAY=:{number}\n") - - def supervise(self, number, finished): - runtime = self.runtime / str(number) - runtime.mkdir(mode=0o700, parents=True, exist_ok=True) - environment = dict(os.environ, DISPLAY=f":{number}", CMUX_DESKTOP_RUNTIME_DIR=str(runtime)) - for key in ("NOTIFY_SOCKET", "DBUS_SESSION_BUS_ADDRESS", "DBUS_SESSION_BUS_PID", - "AT_SPI_BUS_ADDRESS", "AT_SPI_BUS"): - environment.pop(key, None) - while not self.shutdown.is_set(): - try: - # The supervisor is a health check, not a timer-driven restart. - # Preserve an active RFB/noVNC session until its owned endpoints fail. - if not self.processes.get(number) or not ready(number): - self.start_components(number, environment, runtime) - state = "running" if ready(number) else "unavailable" - except (OSError, RuntimeError, subprocess.SubprocessError): - self.terminate(number) - state = "unavailable" - with self.lock: - self.states[number] = state - finished.set() - self.shutdown.wait(30) - self.terminate(number) - - def snapshot(self): - with self.lock: - return {"version": 1, "canCreate": len(self.jobs) < MAX_DISPLAYS - 1, - "displays": [descriptor(1, "running" if ready(1) else "unavailable")] - + [descriptor(n, self.states.get(n, "launching")) for n in self.catalog.numbers()]} - - def handle(self, request): - if request.get("action") == "list": - return self.snapshot() - if request.get("action") != "create": - raise ValueError("unsupported display action") - number = self.catalog.allocate(request.get("request")) - self.start(number).wait(55) - result = self.snapshot() - result["created"] = f"display:{number}" - if self.states.get(number) != "running": - result["error"] = "display_start_failed" - return result - - -def serve(runtime): - runtime = Path(runtime) - runtime.mkdir(mode=0o700, parents=True, exist_ok=True) - with (runtime / "display-control.lock").open("w") as lock: - try: - fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) - except BlockingIOError: - return - if not all(shutil.which(name) for name in ("websockify", "openbox", "dbus-launch")): - raise ValueError("display runtime unavailable") - if not (shutil.which("Xvnc") or shutil.which("Xtigervnc")): - raise ValueError("display runtime unavailable") - service = DisplayService(DisplayCatalog(Path.home() / ".cmux" / "displays"), runtime / "displays") - for number in service.catalog.numbers(): - service.start(number) - path = runtime / "display-control.sock" - if path.exists(): - path.unlink() - - class Handler(socketserver.StreamRequestHandler): - def handle(self): - self.connection.settimeout(60) - try: - raw = self.rfile.readline(4097) - if len(raw) > 4096: - raise ValueError("display request too large") - result = service.handle(json.loads(raw)) - except (ValueError, KeyError, TypeError, OSError): - result = {"error": "display_request_failed"} - try: - self.wfile.write(json.dumps(result).encode() + b"\n") - except OSError: - pass # The persisted receipt remains replayable after disconnect. - - class Server(socketserver.ThreadingUnixStreamServer): - daemon_threads = True - - previous_umask = os.umask(0o077) - try: - with Server(str(path), Handler) as server: - os.umask(previous_umask) - server.serve_forever() - finally: - os.umask(previous_umask) - service.shutdown.set() - path.unlink(missing_ok=True) - - -def main(): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("action", choices=["list", "create", "serve"]) - parser.add_argument("--request-id") - args = parser.parse_args() - runtime = os.environ.get("CMUX_DESKTOP_RUNTIME_DIR", "/run/cmux-desktop") - if args.action == "serve": - serve(runtime) - return 0 - if args.action == "create" and not args.request_id: - parser.error("create requires --request-id (retain it when retrying)") - try: - with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client: - client.settimeout(60) - client.connect(str(Path(runtime) / "display-control.sock")) - client.sendall(json.dumps({"action": args.action, "request": args.request_id}).encode() + b"\n") - with client.makefile("rb") as stream: - result = json.loads(stream.readline(65536)) - except (OSError, ValueError): - result = {"error": "display_runtime_unavailable"} - print(json.dumps(result)) - return 1 if "error" in result else 0 - - -if __name__ == "__main__": - raise SystemExit(main()) - -"""# private static let path = "$HOME/.cmux/cmux-display" static func command(action: String, requestID: UUID? = nil) -> String { - let encoded = Data(source.utf8).base64EncodedString() let argument = requestID.map { " --request-id \($0.uuidString.lowercased())" } ?? "" return """ set -eu path=\"\(path)\" mkdir -p \"$HOME/.cmux\" - if [ ! -x \"$path\" ]; then printf '%s' \"\(encoded)\" | base64 -d > \"$path\"; chmod 700 \"$path\"; fi + if [ ! -x \"$path\" ]; then printf '%s' \"\(encodedSource)\" | base64 -d > \"$path\"; chmod 700 \"$path\"; fi if ! pgrep -u \"$(id -u)\" -f \"$path serve\" >/dev/null 2>&1; then nohup \"$path\" serve > \"$HOME/.cmux/display-service.log\" 2>&1 & for i in $(seq 1 100); do [ -S /run/cmux-desktop/display-control.sock ] && break; sleep 0.1; done diff --git a/Sources/Cloud/CloudGuestDisplaySnapshot.swift b/Sources/Cloud/CloudGuestDisplaySnapshot.swift index b4f54c713868..39b5c99f0128 100644 --- a/Sources/Cloud/CloudGuestDisplaySnapshot.swift +++ b/Sources/Cloud/CloudGuestDisplaySnapshot.swift @@ -3,27 +3,9 @@ import Foundation /// A guest-issued catalog. Connection targets are derived from validated guest /// slots and the authenticated provider's VM address, never from a guest URL. struct CloudGuestDisplaySnapshot: Decodable, Sendable { - struct Display: Decodable, Sendable { - let id: String - let number: Int - let port: Int - let state: SurfaceLifecycle - - func resource(on machine: SurfaceMachineID, address: String?) -> SurfaceResource { - SurfaceResource( - id: SurfaceResourceID(machine: machine, kind: .display, key: id), - title: number == 1 - ? String(localized: "cloudTree.node.desktop", defaultValue: "Desktop") - : String(format: String(localized: "cloud.display.numberedTitle", defaultValue: "Desktop %d"), number), - detail: "noVNC", lifecycle: state, agent: nil, remoteWorkspace: nil, - port: port, url: address.map { CmuxTuiSurfaceProvider.privateDesktopURL(privateAddress: $0, port: port) } - ) - } - } - let version: Int let canCreate: Bool - let displays: [Display] + let displays: [CloudGuestDisplay] let created: String? let error: String? diff --git a/Sources/Surfaces/CmuxTuiSurfaceProvider+Displays.swift b/Sources/Surfaces/CmuxTuiSurfaceProvider+Displays.swift index da1eb3593540..8abe8d34e3ee 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProvider+Displays.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProvider+Displays.swift @@ -22,7 +22,7 @@ extension CmuxTuiSurfaceProvider { let generation = currentLifecycleGeneration let refresh = refreshGeneration await displayCoordinator.refresh() - guard isCurrentRefresh(lifecycle: generation, refresh: refresh) else { return } + guard isCurrentRefresh(lifecycle: generation, refresh: refresh), displayCoordinator.isAvailable else { return } publishDisplays() } diff --git a/cmuxTests/CloudDisplayCatalogTests.swift b/cmuxTests/CloudDisplayCatalogTests.swift index 5a5dba9bace8..a621f81e9a92 100644 --- a/cmuxTests/CloudDisplayCatalogTests.swift +++ b/cmuxTests/CloudDisplayCatalogTests.swift @@ -17,7 +17,7 @@ struct CloudDisplayCatalogTests { func embeddedGuestScriptIsExecutableText() { let command = CloudGuestDisplayScript.command(action: "list") #expect(!command.contains(#"\"\"\""#)) - #expect(command.contains("DisplayService")) + #expect(command.contains("base64 -d")) } @Test("A lost creation reply replays its receipt rather than allocating another display") @@ -65,6 +65,7 @@ struct CloudDisplayCatalogTests { #expect(!service.canCreate && service.snapshot == nil) } + @Test("Display IDs and connection targets are scoped to the authenticated VM") func independentTargets() throws { let snapshot = try decode(""" diff --git a/tests/fixtures/cmux-display b/tests/fixtures/cmux-display index 801bac8e9baa..67ddf6698e03 100755 --- a/tests/fixtures/cmux-display +++ b/tests/fixtures/cmux-display @@ -174,6 +174,8 @@ class DisplayService: return False def start_components(self, number, environment, runtime): + if ready(number): + return self.terminate(number) vnc = shutil.which("Xvnc") or shutil.which("Xtigervnc") websockify = shutil.which("websockify") From 5db9dc31bdb389b26e38a0b5149206f46e4b8835 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 06:37:01 -0700 Subject: [PATCH 08/64] Finish Cloud display build and readiness guards --- .../PortForward/CloudBrowserAccessState.swift | 3 ++- cmux.xcodeproj/project.pbxproj | 4 ++++ cmuxTests/CloudDesktopAccessTests.swift | 19 +++++++++++++++++++ tests/test_cloud_display_catalog.py | 9 +++++++-- 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/Sources/Cloud/PortForward/CloudBrowserAccessState.swift b/Sources/Cloud/PortForward/CloudBrowserAccessState.swift index 7a124d2a672e..ca2129f73e5a 100644 --- a/Sources/Cloud/PortForward/CloudBrowserAccessState.swift +++ b/Sources/Cloud/PortForward/CloudBrowserAccessState.swift @@ -65,7 +65,7 @@ final class CloudBrowserAccessState { } private func startDeadline() { - guard isDesktop else { return } + guard isDesktop, !connectionDeadline.isScheduled else { return } connectionDeadline.schedule(after: .seconds(45)) { [weak self] in guard let self, self.isDesktop, !self.desktopConnected, self.failureMessage == nil else { return } self.desktopFailure = String(localized: "cloud.display.connectionTimedOut", defaultValue: "The Cloud display did not connect within 45 seconds. Retry to reconnect.") @@ -251,6 +251,7 @@ final class CloudBrowserAccessState { dismissedFailure = nil desktopConnected = false activeNavigationID = nil + connectionDeadline.cancel() startDeadline() model?.retry() observeRoute() diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index e97ab15e3c3f..24143b72159c 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -748,6 +748,7 @@ C3CD2FF9F6F1489C8EDD0C7B /* CloudFeatureFlagTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBF28FA7AB264C1AA7821248 /* CloudFeatureFlagTests.swift */; }; 00034889FEF18E9AE0AFDEEC /* CloudFileDelivery.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA32812C9009969F4AEA2380 /* CloudFileDelivery.swift */; }; 57BC801370636A4E2C1B31B7 /* CloudFileDeliveryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8FED5F6485C390988555CDA /* CloudFileDeliveryTests.swift */; }; + 4D0EE5FBA3B6457CBC1762E5 /* CloudGuestDisplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93BEBD29E3494F6A847F9CC8 /* CloudGuestDisplay.swift */; }; 777A119455C940AC95CF68F9 /* CloudGuestDisplayScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6E533611CF47E29BC45008 /* CloudGuestDisplayScript.swift */; }; B844B6722D714B769A32A05F /* CloudGuestDisplaySnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1257ADDE94A4743AA16C593 /* CloudGuestDisplaySnapshot.swift */; }; F7925DA2C43A12723611F26E /* CloudGuestURLRoutingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 935A3D4E1C9FB4A5E94E2770 /* CloudGuestURLRoutingTests.swift */; }; @@ -4840,6 +4841,7 @@ EBF28FA7AB264C1AA7821248 /* CloudFeatureFlagTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudFeatureFlagTests.swift; sourceTree = ""; }; DA32812C9009969F4AEA2380 /* CloudFileDelivery.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CloudFileDelivery.swift; sourceTree = ""; }; F8FED5F6485C390988555CDA /* CloudFileDeliveryTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CloudFileDeliveryTests.swift; sourceTree = ""; }; + 93BEBD29E3494F6A847F9CC8 /* CloudGuestDisplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudGuestDisplay.swift"; sourceTree = ""; }; DD6E533611CF47E29BC45008 /* CloudGuestDisplayScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudGuestDisplayScript.swift"; sourceTree = ""; }; F1257ADDE94A4743AA16C593 /* CloudGuestDisplaySnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudGuestDisplaySnapshot.swift"; sourceTree = ""; }; 935A3D4E1C9FB4A5E94E2770 /* CloudGuestURLRoutingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CloudGuestURLRoutingTests.swift"; sourceTree = ""; }; @@ -8763,6 +8765,7 @@ A1B2C3D4E5F6071827364950 /* CloudMachineSurfacePresentation.swift */, E12747253A744B63A30EFA27 /* CloudTreeOutlineView+Displays.swift */, F1257ADDE94A4743AA16C593 /* CloudGuestDisplaySnapshot.swift */, + 93BEBD29E3494F6A847F9CC8 /* CloudGuestDisplay.swift */, 2F99243AE43E429485F65C00 /* CloudDisplayCoordinator.swift */, DD6E533611CF47E29BC45008 /* CloudGuestDisplayScript.swift */, 1CE0A82A4E20E446A21A97BB /* CloudTreeNode.swift */, @@ -13220,6 +13223,7 @@ F8111B8E2214BA971C8EB6E0 /* CloudErrorCopy.swift in Sources */, 5EDE6BFD28BE4BD58EF84889 /* CloudFeatureAvailabilityObserver.swift in Sources */, 00034889FEF18E9AE0AFDEEC /* CloudFileDelivery.swift in Sources */, + 4D0EE5FBA3B6457CBC1762E5 /* CloudGuestDisplay.swift in Sources */, 777A119455C940AC95CF68F9 /* CloudGuestDisplayScript.swift in Sources */, B844B6722D714B769A32A05F /* CloudGuestDisplaySnapshot.swift in Sources */, 902ED2709F9B61E9E4B59A24 /* CloudGuestURLService.swift in Sources */, diff --git a/cmuxTests/CloudDesktopAccessTests.swift b/cmuxTests/CloudDesktopAccessTests.swift index 58e7f3648b05..30abb4b9e647 100644 --- a/cmuxTests/CloudDesktopAccessTests.swift +++ b/cmuxTests/CloudDesktopAccessTests.swift @@ -68,6 +68,25 @@ struct CloudDesktopAccessTests { await model.retire() } + @Test("Automatic reconnect attempts do not extend the display deadline") + func reconnectDoesNotResetDeadline() async throws { + let clock = CloudCommandDeadlineClock() + let model = CloudPortAccessModel(target: .init(host: "10.0.0.7", port: 6901), coordinator: nil, + wake: {}, startForward: { _ in 46901 }, stopForward: {}, route: .loopback) + let state = CloudBrowserAccessState(clock: clock) + state.configure(model: model, url: URL(string: "http://10.0.0.7:6901/vnc.html")!) + model.connect() + #expect(await wait { model.isReady }) + let url = try #require(state.nextURL()) + state.didCommit(url: url) + state.desktopConnectionIsConnecting(url: url) + await clock.waitUntilSleeping() + state.desktopConnectionIsConnecting(url: url) + clock.advance(by: .seconds(46)) + #expect(await wait { state.showsFailureAlert }) + await model.retire() + } + @Test("A cancelled old navigation cannot cancel a newer display attempt") func cancellationUsesNavigationIdentity() async throws { let old = NSObject(), current = NSObject() diff --git a/tests/test_cloud_display_catalog.py b/tests/test_cloud_display_catalog.py index c10dc8067455..d763c64fc175 100644 --- a/tests/test_cloud_display_catalog.py +++ b/tests/test_cloud_display_catalog.py @@ -106,11 +106,16 @@ def spawn(_command, **options): def launch(_command, **_options): return mock.Mock(stdout=f"DBUS_SESSION_BUS_ADDRESS='unix:path=/tmp/bus-{len(environments)}'\nDBUS_SESSION_BUS_PID=1\n") + readiness_calls = {} + def readiness(number): + readiness_calls[number] = readiness_calls.get(number, 0) + 1 + return readiness_calls[number] > 1 + with mock.patch.object(display.subprocess, "Popen", side_effect=spawn), \ mock.patch.object(display.subprocess, "run", side_effect=launch), \ mock.patch.object(display.shutil, "which", side_effect=lambda name: name), \ mock.patch.object(service, "wait_for_port", return_value=True), \ - mock.patch.object(display, "ready", return_value=True): + mock.patch.object(display, "ready", side_effect=readiness): try: first = service.handle({"action": "create", "request": str(uuid.uuid4())}) second = service.handle({"action": "create", "request": str(uuid.uuid4())}) @@ -130,7 +135,7 @@ def launch(_command, **_options): def test_start_failure_retains_resource_and_replay_receipt(self): service = display.DisplayService(self.catalog(), self.root / "runtime") request = str(uuid.uuid4()) - with mock.patch.object(display.subprocess, "run", side_effect=OSError("starter unavailable")), mock.patch.object(display, "ready", return_value=True): + with mock.patch.object(display.subprocess, "run", side_effect=OSError("starter unavailable")), mock.patch.object(display, "ready", return_value=False): try: result = service.handle({"action": "create", "request": request}) retried = service.handle({"action": "create", "request": request}) From 91d6bdb3551e99914cd326bdeaf8d899c79dc2e0 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 07:16:54 -0700 Subject: [PATCH 09/64] Preserve display state and bind guest listeners privately --- Sources/Cloud/CloudDisplayCoordinator.swift | 1 - Sources/Cloud/CloudGuestDisplayScript.swift | 135 +++++++++--------- .../PortForward/CloudBrowserAccessState.swift | 4 +- cmuxTests/CloudDesktopAccessTests.swift | 10 ++ tests/fixtures/cmux-display | 2 +- 5 files changed, 82 insertions(+), 70 deletions(-) diff --git a/Sources/Cloud/CloudDisplayCoordinator.swift b/Sources/Cloud/CloudDisplayCoordinator.swift index 695699644b14..1bbcb554b9f1 100644 --- a/Sources/Cloud/CloudDisplayCoordinator.swift +++ b/Sources/Cloud/CloudDisplayCoordinator.swift @@ -31,7 +31,6 @@ final class CloudDisplayCoordinator { isAvailable = response.exitCode == 0 } catch { guard token == generation else { return } - snapshot = nil isAvailable = false } } diff --git a/Sources/Cloud/CloudGuestDisplayScript.swift b/Sources/Cloud/CloudGuestDisplayScript.swift index caffe5cbe99e..8673a2822f6b 100644 --- a/Sources/Cloud/CloudGuestDisplayScript.swift +++ b/Sources/Cloud/CloudGuestDisplayScript.swift @@ -90,73 +90,74 @@ ICBkYnVzX3BpZCA9IGludChlbnZpcm9ubWVudC5nZXQoIkRCVVNfU0VTU0lPTl9CVVNfUElEIiwgIjAi ICAgICAgICAgICAgZGJ1c19waWQgPSAwCiAgICAgICAgaWYgZGJ1c19waWQgPiAxOgogICAgICAgICAgICBwcm9jZXNzZXMuYXBwZW5kKE93bmVkUHJvY2Vz cyhkYnVzX3BpZCkpCiAgICAgICAgZm9yIGV4ZWN1dGFibGUsIGFyZ3MgaW4gWwogICAgICAgICAgICAoIm9wZW5ib3giLCBbXSksICgidGludDIiLCBbIi1j IiwgIi9ldGMvY211eC90aW50MnJjIl0pLAogICAgICAgICAgICAoInZuY2NvbmZpZyIsIFsiLW5vd2luIl0pLAogICAgICAgICAgICAod2Vic29ja2lmeSwg -WyItLXdlYiIsICIvdXNyL3NoYXJlL25vdm5jIiwgIi0taGVhcnRiZWF0IiwgIjMwIiwKICAgICAgICAgICAgICAgICAgICAgICAgICBmIls6Ol06e25vdm5j -X3BvcnR9IiwgZiIxMjcuMC4wLjE6e3JmYl9wb3J0fSJdKSwKICAgICAgICBdOgogICAgICAgICAgICBwYXRoID0gZXhlY3V0YWJsZSBpZiAiLyIgaW4gZXhl -Y3V0YWJsZSBlbHNlIHNodXRpbC53aGljaChleGVjdXRhYmxlKQogICAgICAgICAgICBpZiBub3QgcGF0aDoKICAgICAgICAgICAgICAgIGlmIGV4ZWN1dGFi -bGUgPT0gInZuY2NvbmZpZyI6CiAgICAgICAgICAgICAgICAgICAgY29udGludWUKICAgICAgICAgICAgICAgIHJhaXNlIFJ1bnRpbWVFcnJvcihmIm1pc3Np -bmcgZGlzcGxheSBjb21wb25lbnQ6IHtleGVjdXRhYmxlfSIpCiAgICAgICAgICAgIHByb2Nlc3MgPSBzdWJwcm9jZXNzLlBvcGVuKFtwYXRoXSArIGFyZ3Ms -IGVudj1lbnZpcm9ubWVudCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3RkaW49c3VicHJvY2Vzcy5ERVZOVUxMLAogICAgICAg -ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdGRvdXQ9c3VicHJvY2Vzcy5ERVZOVUxMLCBzdGRlcnI9c3VicHJvY2Vzcy5ERVZOVUxMKQogICAg -ICAgICAgICBwcm9jZXNzZXMuYXBwZW5kKHByb2Nlc3MpCiAgICAgICAgaWYgbm90IHNlbGYud2FpdF9mb3JfcG9ydChub3ZuY19wb3J0LCB0aW1lb3V0PTEw -KToKICAgICAgICAgICAgcmFpc2UgUnVudGltZUVycm9yKCJub1ZOQyBkaWQgbm90IGJlY29tZSByZWFkeSIpCiAgICAgICAgKHJ1bnRpbWUgLyAiZW52Iiku -d3JpdGVfdGV4dChmImV4cG9ydCBESVNQTEFZPTp7bnVtYmVyfVxuIikKCiAgICBkZWYgc3VwZXJ2aXNlKHNlbGYsIG51bWJlciwgZmluaXNoZWQpOgogICAg -ICAgIHJ1bnRpbWUgPSBzZWxmLnJ1bnRpbWUgLyBzdHIobnVtYmVyKQogICAgICAgIHJ1bnRpbWUubWtkaXIobW9kZT0wbzcwMCwgcGFyZW50cz1UcnVlLCBl -eGlzdF9vaz1UcnVlKQogICAgICAgIGVudmlyb25tZW50ID0gZGljdChvcy5lbnZpcm9uLCBESVNQTEFZPWYiOntudW1iZXJ9IiwgQ01VWF9ERVNLVE9QX1JV -TlRJTUVfRElSPXN0cihydW50aW1lKSkKICAgICAgICBmb3Iga2V5IGluICgiTk9USUZZX1NPQ0tFVCIsICJEQlVTX1NFU1NJT05fQlVTX0FERFJFU1MiLCAi -REJVU19TRVNTSU9OX0JVU19QSUQiLAogICAgICAgICAgICAgICAgICAgICJBVF9TUElfQlVTX0FERFJFU1MiLCAiQVRfU1BJX0JVUyIpOgogICAgICAgICAg -ICBlbnZpcm9ubWVudC5wb3Aoa2V5LCBOb25lKQogICAgICAgIHdoaWxlIG5vdCBzZWxmLnNodXRkb3duLmlzX3NldCgpOgogICAgICAgICAgICB0cnk6CiAg -ICAgICAgICAgICAgICAjIFRoZSBzdXBlcnZpc29yIGlzIGEgaGVhbHRoIGNoZWNrLCBub3QgYSB0aW1lci1kcml2ZW4gcmVzdGFydC4KICAgICAgICAgICAg -ICAgICMgUHJlc2VydmUgYW4gYWN0aXZlIFJGQi9ub1ZOQyBzZXNzaW9uIHVudGlsIGl0cyBvd25lZCBlbmRwb2ludHMgZmFpbC4KICAgICAgICAgICAgICAg -IGlmIG5vdCBzZWxmLnByb2Nlc3Nlcy5nZXQobnVtYmVyKSBvciBub3QgcmVhZHkobnVtYmVyKToKICAgICAgICAgICAgICAgICAgICBzZWxmLnN0YXJ0X2Nv -bXBvbmVudHMobnVtYmVyLCBlbnZpcm9ubWVudCwgcnVudGltZSkKICAgICAgICAgICAgICAgIHN0YXRlID0gInJ1bm5pbmciIGlmIHJlYWR5KG51bWJlcikg -ZWxzZSAidW5hdmFpbGFibGUiCiAgICAgICAgICAgIGV4Y2VwdCAoT1NFcnJvciwgUnVudGltZUVycm9yLCBzdWJwcm9jZXNzLlN1YnByb2Nlc3NFcnJvcik6 -CiAgICAgICAgICAgICAgICBzZWxmLnRlcm1pbmF0ZShudW1iZXIpCiAgICAgICAgICAgICAgICBzdGF0ZSA9ICJ1bmF2YWlsYWJsZSIKICAgICAgICAgICAg -d2l0aCBzZWxmLmxvY2s6CiAgICAgICAgICAgICAgICBzZWxmLnN0YXRlc1tudW1iZXJdID0gc3RhdGUKICAgICAgICAgICAgZmluaXNoZWQuc2V0KCkKICAg -ICAgICAgICAgc2VsZi5zaHV0ZG93bi53YWl0KDMwKQogICAgICAgIHNlbGYudGVybWluYXRlKG51bWJlcikKCiAgICBkZWYgc25hcHNob3Qoc2VsZik6CiAg -ICAgICAgd2l0aCBzZWxmLmxvY2s6CiAgICAgICAgICAgIHJldHVybiB7InZlcnNpb24iOiAxLCAiY2FuQ3JlYXRlIjogbGVuKHNlbGYuam9icykgPCBNQVhf -RElTUExBWVMgLSAxLAogICAgICAgICAgICAgICAgICAgICJkaXNwbGF5cyI6IFtkZXNjcmlwdG9yKDEsICJydW5uaW5nIiBpZiByZWFkeSgxKSBlbHNlICJ1 -bmF2YWlsYWJsZSIpXQogICAgICAgICAgICAgICAgICAgICsgW2Rlc2NyaXB0b3Iobiwgc2VsZi5zdGF0ZXMuZ2V0KG4sICJsYXVuY2hpbmciKSkgZm9yIG4g -aW4gc2VsZi5jYXRhbG9nLm51bWJlcnMoKV19CgogICAgZGVmIGhhbmRsZShzZWxmLCByZXF1ZXN0KToKICAgICAgICBpZiByZXF1ZXN0LmdldCgiYWN0aW9u -IikgPT0gImxpc3QiOgogICAgICAgICAgICByZXR1cm4gc2VsZi5zbmFwc2hvdCgpCiAgICAgICAgaWYgcmVxdWVzdC5nZXQoImFjdGlvbiIpICE9ICJjcmVh -dGUiOgogICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJ1bnN1cHBvcnRlZCBkaXNwbGF5IGFjdGlvbiIpCiAgICAgICAgbnVtYmVyID0gc2VsZi5jYXRh -bG9nLmFsbG9jYXRlKHJlcXVlc3QuZ2V0KCJyZXF1ZXN0IikpCiAgICAgICAgc2VsZi5zdGFydChudW1iZXIpLndhaXQoNTUpCiAgICAgICAgcmVzdWx0ID0g -c2VsZi5zbmFwc2hvdCgpCiAgICAgICAgcmVzdWx0WyJjcmVhdGVkIl0gPSBmImRpc3BsYXk6e251bWJlcn0iCiAgICAgICAgaWYgc2VsZi5zdGF0ZXMuZ2V0 -KG51bWJlcikgIT0gInJ1bm5pbmciOgogICAgICAgICAgICByZXN1bHRbImVycm9yIl0gPSAiZGlzcGxheV9zdGFydF9mYWlsZWQiCiAgICAgICAgcmV0dXJu -IHJlc3VsdAoKCmRlZiBzZXJ2ZShydW50aW1lKToKICAgIHJ1bnRpbWUgPSBQYXRoKHJ1bnRpbWUpCiAgICBydW50aW1lLm1rZGlyKG1vZGU9MG83MDAsIHBh -cmVudHM9VHJ1ZSwgZXhpc3Rfb2s9VHJ1ZSkKICAgIHdpdGggKHJ1bnRpbWUgLyAiZGlzcGxheS1jb250cm9sLmxvY2siKS5vcGVuKCJ3IikgYXMgbG9jazoK -ICAgICAgICB0cnk6CiAgICAgICAgICAgIGZjbnRsLmZsb2NrKGxvY2ssIGZjbnRsLkxPQ0tfRVggfCBmY250bC5MT0NLX05CKQogICAgICAgIGV4Y2VwdCBC -bG9ja2luZ0lPRXJyb3I6CiAgICAgICAgICAgIHJldHVybgogICAgICAgIGlmIG5vdCBhbGwoc2h1dGlsLndoaWNoKG5hbWUpIGZvciBuYW1lIGluICgid2Vi -c29ja2lmeSIsICJvcGVuYm94IiwgImRidXMtbGF1bmNoIikpOgogICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJkaXNwbGF5IHJ1bnRpbWUgdW5hdmFp -bGFibGUiKQogICAgICAgIGlmIG5vdCAoc2h1dGlsLndoaWNoKCJYdm5jIikgb3Igc2h1dGlsLndoaWNoKCJYdGlnZXJ2bmMiKSk6CiAgICAgICAgICAgIHJh -aXNlIFZhbHVlRXJyb3IoImRpc3BsYXkgcnVudGltZSB1bmF2YWlsYWJsZSIpCiAgICAgICAgc2VydmljZSA9IERpc3BsYXlTZXJ2aWNlKERpc3BsYXlDYXRh -bG9nKFBhdGguaG9tZSgpIC8gIi5jbXV4IiAvICJkaXNwbGF5cyIpLCBydW50aW1lIC8gImRpc3BsYXlzIikKICAgICAgICBmb3IgbnVtYmVyIGluIHNlcnZp -Y2UuY2F0YWxvZy5udW1iZXJzKCk6CiAgICAgICAgICAgIHNlcnZpY2Uuc3RhcnQobnVtYmVyKQogICAgICAgIHBhdGggPSBydW50aW1lIC8gImRpc3BsYXkt -Y29udHJvbC5zb2NrIgogICAgICAgIGlmIHBhdGguZXhpc3RzKCk6CiAgICAgICAgICAgIHBhdGgudW5saW5rKCkKCiAgICAgICAgY2xhc3MgSGFuZGxlcihz -b2NrZXRzZXJ2ZXIuU3RyZWFtUmVxdWVzdEhhbmRsZXIpOgogICAgICAgICAgICBkZWYgaGFuZGxlKHNlbGYpOgogICAgICAgICAgICAgICAgc2VsZi5jb25u -ZWN0aW9uLnNldHRpbWVvdXQoNjApCiAgICAgICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICAgICAgcmF3ID0gc2VsZi5yZmlsZS5yZWFkbGluZSg0 -MDk3KQogICAgICAgICAgICAgICAgICAgIGlmIGxlbihyYXcpID4gNDA5NjoKICAgICAgICAgICAgICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJvcigiZGlz -cGxheSByZXF1ZXN0IHRvbyBsYXJnZSIpCiAgICAgICAgICAgICAgICAgICAgcmVzdWx0ID0gc2VydmljZS5oYW5kbGUoanNvbi5sb2FkcyhyYXcpKQogICAg -ICAgICAgICAgICAgZXhjZXB0IChWYWx1ZUVycm9yLCBLZXlFcnJvciwgVHlwZUVycm9yLCBPU0Vycm9yKToKICAgICAgICAgICAgICAgICAgICByZXN1bHQg -PSB7ImVycm9yIjogImRpc3BsYXlfcmVxdWVzdF9mYWlsZWQifQogICAgICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgIHNlbGYud2ZpbGUu -d3JpdGUoanNvbi5kdW1wcyhyZXN1bHQpLmVuY29kZSgpICsgYiJcbiIpCiAgICAgICAgICAgICAgICBleGNlcHQgT1NFcnJvcjoKICAgICAgICAgICAgICAg -ICAgICBwYXNzICAjIFRoZSBwZXJzaXN0ZWQgcmVjZWlwdCByZW1haW5zIHJlcGxheWFibGUgYWZ0ZXIgZGlzY29ubmVjdC4KCiAgICAgICAgY2xhc3MgU2Vy -dmVyKHNvY2tldHNlcnZlci5UaHJlYWRpbmdVbml4U3RyZWFtU2VydmVyKToKICAgICAgICAgICAgZGFlbW9uX3RocmVhZHMgPSBUcnVlCgogICAgICAgIHBy -ZXZpb3VzX3VtYXNrID0gb3MudW1hc2soMG8wNzcpCiAgICAgICAgdHJ5OgogICAgICAgICAgICB3aXRoIFNlcnZlcihzdHIocGF0aCksIEhhbmRsZXIpIGFz -IHNlcnZlcjoKICAgICAgICAgICAgICAgIG9zLnVtYXNrKHByZXZpb3VzX3VtYXNrKQogICAgICAgICAgICAgICAgc2VydmVyLnNlcnZlX2ZvcmV2ZXIoKQog -ICAgICAgIGZpbmFsbHk6CiAgICAgICAgICAgIG9zLnVtYXNrKHByZXZpb3VzX3VtYXNrKQogICAgICAgICAgICBzZXJ2aWNlLnNodXRkb3duLnNldCgpCiAg -ICAgICAgICAgIHBhdGgudW5saW5rKG1pc3Npbmdfb2s9VHJ1ZSkKCgpkZWYgbWFpbigpOgogICAgcGFyc2VyID0gYXJncGFyc2UuQXJndW1lbnRQYXJzZXIo -ZGVzY3JpcHRpb249X19kb2NfXykKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoImFjdGlvbiIsIGNob2ljZXM9WyJsaXN0IiwgImNyZWF0ZSIsICJzZXJ2ZSJd -KQogICAgcGFyc2VyLmFkZF9hcmd1bWVudCgiLS1yZXF1ZXN0LWlkIikKICAgIGFyZ3MgPSBwYXJzZXIucGFyc2VfYXJncygpCiAgICBydW50aW1lID0gb3Mu -ZW52aXJvbi5nZXQoIkNNVVhfREVTS1RPUF9SVU5USU1FX0RJUiIsICIvcnVuL2NtdXgtZGVza3RvcCIpCiAgICBpZiBhcmdzLmFjdGlvbiA9PSAic2VydmUi -OgogICAgICAgIHNlcnZlKHJ1bnRpbWUpCiAgICAgICAgcmV0dXJuIDAKICAgIGlmIGFyZ3MuYWN0aW9uID09ICJjcmVhdGUiIGFuZCBub3QgYXJncy5yZXF1 -ZXN0X2lkOgogICAgICAgIHBhcnNlci5lcnJvcigiY3JlYXRlIHJlcXVpcmVzIC0tcmVxdWVzdC1pZCAocmV0YWluIGl0IHdoZW4gcmV0cnlpbmcpIikKICAg -IHRyeToKICAgICAgICB3aXRoIHNvY2tldC5zb2NrZXQoc29ja2V0LkFGX1VOSVgsIHNvY2tldC5TT0NLX1NUUkVBTSkgYXMgY2xpZW50OgogICAgICAgICAg -ICBjbGllbnQuc2V0dGltZW91dCg2MCkKICAgICAgICAgICAgY2xpZW50LmNvbm5lY3Qoc3RyKFBhdGgocnVudGltZSkgLyAiZGlzcGxheS1jb250cm9sLnNv -Y2siKSkKICAgICAgICAgICAgY2xpZW50LnNlbmRhbGwoanNvbi5kdW1wcyh7ImFjdGlvbiI6IGFyZ3MuYWN0aW9uLCAicmVxdWVzdCI6IGFyZ3MucmVxdWVz -dF9pZH0pLmVuY29kZSgpICsgYiJcbiIpCiAgICAgICAgICAgIHdpdGggY2xpZW50Lm1ha2VmaWxlKCJyYiIpIGFzIHN0cmVhbToKICAgICAgICAgICAgICAg -IHJlc3VsdCA9IGpzb24ubG9hZHMoc3RyZWFtLnJlYWRsaW5lKDY1NTM2KSkKICAgIGV4Y2VwdCAoT1NFcnJvciwgVmFsdWVFcnJvcik6CiAgICAgICAgcmVz -dWx0ID0geyJlcnJvciI6ICJkaXNwbGF5X3J1bnRpbWVfdW5hdmFpbGFibGUifQogICAgcHJpbnQoanNvbi5kdW1wcyhyZXN1bHQpKQogICAgcmV0dXJuIDEg -aWYgImVycm9yIiBpbiByZXN1bHQgZWxzZSAwCgoKaWYgX19uYW1lX18gPT0gIl9fbWFpbl9fIjoKICAgIHJhaXNlIFN5c3RlbUV4aXQobWFpbigpKQo= +WyItLXdlYiIsICIvdXNyL3NoYXJlL25vdm5jIiwgIi0taGVhcnRiZWF0IiwgIjMwIiwKICAgICAgICAgICAgICAgICAgICAgICAgICBmIjEyNy4wLjAuMTp7 +bm92bmNfcG9ydH0iLCBmIjEyNy4wLjAuMTp7cmZiX3BvcnR9Il0pLAogICAgICAgIF06CiAgICAgICAgICAgIHBhdGggPSBleGVjdXRhYmxlIGlmICIvIiBp +biBleGVjdXRhYmxlIGVsc2Ugc2h1dGlsLndoaWNoKGV4ZWN1dGFibGUpCiAgICAgICAgICAgIGlmIG5vdCBwYXRoOgogICAgICAgICAgICAgICAgaWYgZXhl +Y3V0YWJsZSA9PSAidm5jY29uZmlnIjoKICAgICAgICAgICAgICAgICAgICBjb250aW51ZQogICAgICAgICAgICAgICAgcmFpc2UgUnVudGltZUVycm9yKGYi +bWlzc2luZyBkaXNwbGF5IGNvbXBvbmVudDoge2V4ZWN1dGFibGV9IikKICAgICAgICAgICAgcHJvY2VzcyA9IHN1YnByb2Nlc3MuUG9wZW4oW3BhdGhdICsg +YXJncywgZW52PWVudmlyb25tZW50LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdGRpbj1zdWJwcm9jZXNzLkRFVk5VTEwsCiAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0ZG91dD1zdWJwcm9jZXNzLkRFVk5VTEwsIHN0ZGVycj1zdWJwcm9jZXNzLkRFVk5VTEwp +CiAgICAgICAgICAgIHByb2Nlc3Nlcy5hcHBlbmQocHJvY2VzcykKICAgICAgICBpZiBub3Qgc2VsZi53YWl0X2Zvcl9wb3J0KG5vdm5jX3BvcnQsIHRpbWVv +dXQ9MTApOgogICAgICAgICAgICByYWlzZSBSdW50aW1lRXJyb3IoIm5vVk5DIGRpZCBub3QgYmVjb21lIHJlYWR5IikKICAgICAgICAocnVudGltZSAvICJl +bnYiKS53cml0ZV90ZXh0KGYiZXhwb3J0IERJU1BMQVk9OntudW1iZXJ9XG4iKQoKICAgIGRlZiBzdXBlcnZpc2Uoc2VsZiwgbnVtYmVyLCBmaW5pc2hlZCk6 +CiAgICAgICAgcnVudGltZSA9IHNlbGYucnVudGltZSAvIHN0cihudW1iZXIpCiAgICAgICAgcnVudGltZS5ta2Rpcihtb2RlPTBvNzAwLCBwYXJlbnRzPVRy +dWUsIGV4aXN0X29rPVRydWUpCiAgICAgICAgZW52aXJvbm1lbnQgPSBkaWN0KG9zLmVudmlyb24sIERJU1BMQVk9ZiI6e251bWJlcn0iLCBDTVVYX0RFU0tU +T1BfUlVOVElNRV9ESVI9c3RyKHJ1bnRpbWUpKQogICAgICAgIGZvciBrZXkgaW4gKCJOT1RJRllfU09DS0VUIiwgIkRCVVNfU0VTU0lPTl9CVVNfQUREUkVT +UyIsICJEQlVTX1NFU1NJT05fQlVTX1BJRCIsCiAgICAgICAgICAgICAgICAgICAgIkFUX1NQSV9CVVNfQUREUkVTUyIsICJBVF9TUElfQlVTIik6CiAgICAg +ICAgICAgIGVudmlyb25tZW50LnBvcChrZXksIE5vbmUpCiAgICAgICAgd2hpbGUgbm90IHNlbGYuc2h1dGRvd24uaXNfc2V0KCk6CiAgICAgICAgICAgIHRy +eToKICAgICAgICAgICAgICAgICMgVGhlIHN1cGVydmlzb3IgaXMgYSBoZWFsdGggY2hlY2ssIG5vdCBhIHRpbWVyLWRyaXZlbiByZXN0YXJ0LgogICAgICAg +ICAgICAgICAgIyBQcmVzZXJ2ZSBhbiBhY3RpdmUgUkZCL25vVk5DIHNlc3Npb24gdW50aWwgaXRzIG93bmVkIGVuZHBvaW50cyBmYWlsLgogICAgICAgICAg +ICAgICAgaWYgbm90IHNlbGYucHJvY2Vzc2VzLmdldChudW1iZXIpIG9yIG5vdCByZWFkeShudW1iZXIpOgogICAgICAgICAgICAgICAgICAgIHNlbGYuc3Rh +cnRfY29tcG9uZW50cyhudW1iZXIsIGVudmlyb25tZW50LCBydW50aW1lKQogICAgICAgICAgICAgICAgc3RhdGUgPSAicnVubmluZyIgaWYgcmVhZHkobnVt +YmVyKSBlbHNlICJ1bmF2YWlsYWJsZSIKICAgICAgICAgICAgZXhjZXB0IChPU0Vycm9yLCBSdW50aW1lRXJyb3IsIHN1YnByb2Nlc3MuU3VicHJvY2Vzc0Vy +cm9yKToKICAgICAgICAgICAgICAgIHNlbGYudGVybWluYXRlKG51bWJlcikKICAgICAgICAgICAgICAgIHN0YXRlID0gInVuYXZhaWxhYmxlIgogICAgICAg +ICAgICB3aXRoIHNlbGYubG9jazoKICAgICAgICAgICAgICAgIHNlbGYuc3RhdGVzW251bWJlcl0gPSBzdGF0ZQogICAgICAgICAgICBmaW5pc2hlZC5zZXQo +KQogICAgICAgICAgICBzZWxmLnNodXRkb3duLndhaXQoMzApCiAgICAgICAgc2VsZi50ZXJtaW5hdGUobnVtYmVyKQoKICAgIGRlZiBzbmFwc2hvdChzZWxm +KToKICAgICAgICB3aXRoIHNlbGYubG9jazoKICAgICAgICAgICAgcmV0dXJuIHsidmVyc2lvbiI6IDEsICJjYW5DcmVhdGUiOiBsZW4oc2VsZi5qb2JzKSA8 +IE1BWF9ESVNQTEFZUyAtIDEsCiAgICAgICAgICAgICAgICAgICAgImRpc3BsYXlzIjogW2Rlc2NyaXB0b3IoMSwgInJ1bm5pbmciIGlmIHJlYWR5KDEpIGVs +c2UgInVuYXZhaWxhYmxlIildCiAgICAgICAgICAgICAgICAgICAgKyBbZGVzY3JpcHRvcihuLCBzZWxmLnN0YXRlcy5nZXQobiwgImxhdW5jaGluZyIpKSBm +b3IgbiBpbiBzZWxmLmNhdGFsb2cubnVtYmVycygpXX0KCiAgICBkZWYgaGFuZGxlKHNlbGYsIHJlcXVlc3QpOgogICAgICAgIGlmIHJlcXVlc3QuZ2V0KCJh +Y3Rpb24iKSA9PSAibGlzdCI6CiAgICAgICAgICAgIHJldHVybiBzZWxmLnNuYXBzaG90KCkKICAgICAgICBpZiByZXF1ZXN0LmdldCgiYWN0aW9uIikgIT0g +ImNyZWF0ZSI6CiAgICAgICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoInVuc3VwcG9ydGVkIGRpc3BsYXkgYWN0aW9uIikKICAgICAgICBudW1iZXIgPSBzZWxm +LmNhdGFsb2cuYWxsb2NhdGUocmVxdWVzdC5nZXQoInJlcXVlc3QiKSkKICAgICAgICBzZWxmLnN0YXJ0KG51bWJlcikud2FpdCg1NSkKICAgICAgICByZXN1 +bHQgPSBzZWxmLnNuYXBzaG90KCkKICAgICAgICByZXN1bHRbImNyZWF0ZWQiXSA9IGYiZGlzcGxheTp7bnVtYmVyfSIKICAgICAgICBpZiBzZWxmLnN0YXRl +cy5nZXQobnVtYmVyKSAhPSAicnVubmluZyI6CiAgICAgICAgICAgIHJlc3VsdFsiZXJyb3IiXSA9ICJkaXNwbGF5X3N0YXJ0X2ZhaWxlZCIKICAgICAgICBy +ZXR1cm4gcmVzdWx0CgoKZGVmIHNlcnZlKHJ1bnRpbWUpOgogICAgcnVudGltZSA9IFBhdGgocnVudGltZSkKICAgIHJ1bnRpbWUubWtkaXIobW9kZT0wbzcw +MCwgcGFyZW50cz1UcnVlLCBleGlzdF9vaz1UcnVlKQogICAgd2l0aCAocnVudGltZSAvICJkaXNwbGF5LWNvbnRyb2wubG9jayIpLm9wZW4oInciKSBhcyBs +b2NrOgogICAgICAgIHRyeToKICAgICAgICAgICAgZmNudGwuZmxvY2sobG9jaywgZmNudGwuTE9DS19FWCB8IGZjbnRsLkxPQ0tfTkIpCiAgICAgICAgZXhj +ZXB0IEJsb2NraW5nSU9FcnJvcjoKICAgICAgICAgICAgcmV0dXJuCiAgICAgICAgaWYgbm90IGFsbChzaHV0aWwud2hpY2gobmFtZSkgZm9yIG5hbWUgaW4g +KCJ3ZWJzb2NraWZ5IiwgIm9wZW5ib3giLCAiZGJ1cy1sYXVuY2giKSk6CiAgICAgICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoImRpc3BsYXkgcnVudGltZSB1 +bmF2YWlsYWJsZSIpCiAgICAgICAgaWYgbm90IChzaHV0aWwud2hpY2goIlh2bmMiKSBvciBzaHV0aWwud2hpY2goIlh0aWdlcnZuYyIpKToKICAgICAgICAg +ICAgcmFpc2UgVmFsdWVFcnJvcigiZGlzcGxheSBydW50aW1lIHVuYXZhaWxhYmxlIikKICAgICAgICBzZXJ2aWNlID0gRGlzcGxheVNlcnZpY2UoRGlzcGxh +eUNhdGFsb2coUGF0aC5ob21lKCkgLyAiLmNtdXgiIC8gImRpc3BsYXlzIiksIHJ1bnRpbWUgLyAiZGlzcGxheXMiKQogICAgICAgIGZvciBudW1iZXIgaW4g +c2VydmljZS5jYXRhbG9nLm51bWJlcnMoKToKICAgICAgICAgICAgc2VydmljZS5zdGFydChudW1iZXIpCiAgICAgICAgcGF0aCA9IHJ1bnRpbWUgLyAiZGlz +cGxheS1jb250cm9sLnNvY2siCiAgICAgICAgaWYgcGF0aC5leGlzdHMoKToKICAgICAgICAgICAgcGF0aC51bmxpbmsoKQoKICAgICAgICBjbGFzcyBIYW5k +bGVyKHNvY2tldHNlcnZlci5TdHJlYW1SZXF1ZXN0SGFuZGxlcik6CiAgICAgICAgICAgIGRlZiBoYW5kbGUoc2VsZik6CiAgICAgICAgICAgICAgICBzZWxm +LmNvbm5lY3Rpb24uc2V0dGltZW91dCg2MCkKICAgICAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgICAgICByYXcgPSBzZWxmLnJmaWxlLnJlYWRs +aW5lKDQwOTcpCiAgICAgICAgICAgICAgICAgICAgaWYgbGVuKHJhdykgPiA0MDk2OgogICAgICAgICAgICAgICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9y +KCJkaXNwbGF5IHJlcXVlc3QgdG9vIGxhcmdlIikKICAgICAgICAgICAgICAgICAgICByZXN1bHQgPSBzZXJ2aWNlLmhhbmRsZShqc29uLmxvYWRzKHJhdykp +CiAgICAgICAgICAgICAgICBleGNlcHQgKFZhbHVlRXJyb3IsIEtleUVycm9yLCBUeXBlRXJyb3IsIE9TRXJyb3IpOgogICAgICAgICAgICAgICAgICAgIHJl +c3VsdCA9IHsiZXJyb3IiOiAiZGlzcGxheV9yZXF1ZXN0X2ZhaWxlZCJ9CiAgICAgICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICAgICAgc2VsZi53 +ZmlsZS53cml0ZShqc29uLmR1bXBzKHJlc3VsdCkuZW5jb2RlKCkgKyBiIlxuIikKICAgICAgICAgICAgICAgIGV4Y2VwdCBPU0Vycm9yOgogICAgICAgICAg +ICAgICAgICAgIHBhc3MgICMgVGhlIHBlcnNpc3RlZCByZWNlaXB0IHJlbWFpbnMgcmVwbGF5YWJsZSBhZnRlciBkaXNjb25uZWN0LgoKICAgICAgICBjbGFz +cyBTZXJ2ZXIoc29ja2V0c2VydmVyLlRocmVhZGluZ1VuaXhTdHJlYW1TZXJ2ZXIpOgogICAgICAgICAgICBkYWVtb25fdGhyZWFkcyA9IFRydWUKCiAgICAg +ICAgcHJldmlvdXNfdW1hc2sgPSBvcy51bWFzaygwbzA3NykKICAgICAgICB0cnk6CiAgICAgICAgICAgIHdpdGggU2VydmVyKHN0cihwYXRoKSwgSGFuZGxl +cikgYXMgc2VydmVyOgogICAgICAgICAgICAgICAgb3MudW1hc2socHJldmlvdXNfdW1hc2spCiAgICAgICAgICAgICAgICBzZXJ2ZXIuc2VydmVfZm9yZXZl +cigpCiAgICAgICAgZmluYWxseToKICAgICAgICAgICAgb3MudW1hc2socHJldmlvdXNfdW1hc2spCiAgICAgICAgICAgIHNlcnZpY2Uuc2h1dGRvd24uc2V0 +KCkKICAgICAgICAgICAgcGF0aC51bmxpbmsobWlzc2luZ19vaz1UcnVlKQoKCmRlZiBtYWluKCk6CiAgICBwYXJzZXIgPSBhcmdwYXJzZS5Bcmd1bWVudFBh +cnNlcihkZXNjcmlwdGlvbj1fX2RvY19fKQogICAgcGFyc2VyLmFkZF9hcmd1bWVudCgiYWN0aW9uIiwgY2hvaWNlcz1bImxpc3QiLCAiY3JlYXRlIiwgInNl +cnZlIl0pCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50KCItLXJlcXVlc3QtaWQiKQogICAgYXJncyA9IHBhcnNlci5wYXJzZV9hcmdzKCkKICAgIHJ1bnRpbWUg +PSBvcy5lbnZpcm9uLmdldCgiQ01VWF9ERVNLVE9QX1JVTlRJTUVfRElSIiwgIi9ydW4vY211eC1kZXNrdG9wIikKICAgIGlmIGFyZ3MuYWN0aW9uID09ICJz +ZXJ2ZSI6CiAgICAgICAgc2VydmUocnVudGltZSkKICAgICAgICByZXR1cm4gMAogICAgaWYgYXJncy5hY3Rpb24gPT0gImNyZWF0ZSIgYW5kIG5vdCBhcmdz +LnJlcXVlc3RfaWQ6CiAgICAgICAgcGFyc2VyLmVycm9yKCJjcmVhdGUgcmVxdWlyZXMgLS1yZXF1ZXN0LWlkIChyZXRhaW4gaXQgd2hlbiByZXRyeWluZyki +KQogICAgdHJ5OgogICAgICAgIHdpdGggc29ja2V0LnNvY2tldChzb2NrZXQuQUZfVU5JWCwgc29ja2V0LlNPQ0tfU1RSRUFNKSBhcyBjbGllbnQ6CiAgICAg +ICAgICAgIGNsaWVudC5zZXR0aW1lb3V0KDYwKQogICAgICAgICAgICBjbGllbnQuY29ubmVjdChzdHIoUGF0aChydW50aW1lKSAvICJkaXNwbGF5LWNvbnRy +b2wuc29jayIpKQogICAgICAgICAgICBjbGllbnQuc2VuZGFsbChqc29uLmR1bXBzKHsiYWN0aW9uIjogYXJncy5hY3Rpb24sICJyZXF1ZXN0IjogYXJncy5y +ZXF1ZXN0X2lkfSkuZW5jb2RlKCkgKyBiIlxuIikKICAgICAgICAgICAgd2l0aCBjbGllbnQubWFrZWZpbGUoInJiIikgYXMgc3RyZWFtOgogICAgICAgICAg +ICAgICAgcmVzdWx0ID0ganNvbi5sb2FkcyhzdHJlYW0ucmVhZGxpbmUoNjU1MzYpKQogICAgZXhjZXB0IChPU0Vycm9yLCBWYWx1ZUVycm9yKToKICAgICAg +ICByZXN1bHQgPSB7ImVycm9yIjogImRpc3BsYXlfcnVudGltZV91bmF2YWlsYWJsZSJ9CiAgICBwcmludChqc29uLmR1bXBzKHJlc3VsdCkpCiAgICByZXR1 +cm4gMSBpZiAiZXJyb3IiIGluIHJlc3VsdCBlbHNlIDAKCgppZiBfX25hbWVfXyA9PSAiX19tYWluX18iOgogICAgcmFpc2UgU3lzdGVtRXhpdChtYWluKCkp +Cg== """ private static let path = "$HOME/.cmux/cmux-display" diff --git a/Sources/Cloud/PortForward/CloudBrowserAccessState.swift b/Sources/Cloud/PortForward/CloudBrowserAccessState.swift index ca2129f73e5a..4cf9da151b00 100644 --- a/Sources/Cloud/PortForward/CloudBrowserAccessState.swift +++ b/Sources/Cloud/PortForward/CloudBrowserAccessState.swift @@ -86,7 +86,9 @@ final class CloudBrowserAccessState { var isPreparingDocument: Bool { model != nil && !loaded && failureMessage == nil } var isDesktop: Bool { - (resourceID?.kind == .display && remoteURL == nil) || (model != nil && remoteURL?.path == "/vnc.html") + if resourceID?.kind == .display { return true } + guard resourceID == nil else { return false } + return model?.target.port == CmuxTuiSnapshotParser.desktopPort && remoteURL?.path == "/vnc.html" } var failureMessage: String? { diff --git a/cmuxTests/CloudDesktopAccessTests.swift b/cmuxTests/CloudDesktopAccessTests.swift index 30abb4b9e647..96b21126ce95 100644 --- a/cmuxTests/CloudDesktopAccessTests.swift +++ b/cmuxTests/CloudDesktopAccessTests.swift @@ -263,6 +263,16 @@ struct CloudDesktopAccessTests { #expect(state.resourceID == nil) } + @Test("A forwarded /vnc.html URL is not a display when its resource is a browser") + func nonDisplayVNCPathDoesNotUseDesktopReadiness() { + let state = CloudBrowserAccessState() + let model = CloudPortAccessModel(target: .init(host: "10.0.0.7", port: 8000), coordinator: nil, + wake: {}, startForward: { _ in 48000 }, stopForward: {}, route: .loopback) + state.configure(model: model, url: URL(string: "http://10.0.0.7:8000/vnc.html")!, + resourceID: SurfaceResourceID(machine: .cloud("a"), kind: .browser, key: "port:8000")) + #expect(!state.isDesktop) + } + @Test("Desktop bootstrap does not paint WebKit's default white background") func desktopBackgroundUsesNativeBackingUntilCanvasPaints() async throws { let browser = BrowserPanel(workspaceId: UUID(), websiteDataStore: .nonPersistent()) diff --git a/tests/fixtures/cmux-display b/tests/fixtures/cmux-display index 67ddf6698e03..74b377f6a4b2 100755 --- a/tests/fixtures/cmux-display +++ b/tests/fixtures/cmux-display @@ -209,7 +209,7 @@ class DisplayService: ("openbox", []), ("tint2", ["-c", "/etc/cmux/tint2rc"]), ("vncconfig", ["-nowin"]), (websockify, ["--web", "/usr/share/novnc", "--heartbeat", "30", - f"[::]:{novnc_port}", f"127.0.0.1:{rfb_port}"]), + f"127.0.0.1:{novnc_port}", f"127.0.0.1:{rfb_port}"]), ]: path = executable if "/" in executable else shutil.which(executable) if not path: From 657285f16e3b8259f6df2203df0a930619156e21 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 07:43:13 -0700 Subject: [PATCH 10/64] Invalidate display catalogs when VM state changes --- Sources/Cloud/CloudDisplayCoordinator.swift | 7 +++++++ Sources/Cloud/CloudTreeRowContentView.swift | 2 +- Sources/Surfaces/CmuxTuiSurfaceProviders.swift | 1 + cmuxTests/CloudDisplayCatalogTests.swift | 11 +++++++++++ 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Sources/Cloud/CloudDisplayCoordinator.swift b/Sources/Cloud/CloudDisplayCoordinator.swift index 1bbcb554b9f1..0201a1c3193d 100644 --- a/Sources/Cloud/CloudDisplayCoordinator.swift +++ b/Sources/Cloud/CloudDisplayCoordinator.swift @@ -65,9 +65,16 @@ final class CloudDisplayCoordinator { } func stop() { + invalidate() + } + + /// Drops guest state when the provider identity or account scope changes. + func invalidate() { generation &+= 1 creation?.cancel() creation = nil + snapshot = nil + requestID = nil isAvailable = false } } diff --git a/Sources/Cloud/CloudTreeRowContentView.swift b/Sources/Cloud/CloudTreeRowContentView.swift index 07f904cd195b..818e9ed124b3 100644 --- a/Sources/Cloud/CloudTreeRowContentView.swift +++ b/Sources/Cloud/CloudTreeRowContentView.swift @@ -458,7 +458,7 @@ struct CloudTreeRowHoverButtons: View { /// Returns whether the row kind has a hover action to lay out. static func hasButtons(for kind: CloudTreeNode.Kind) -> Bool { switch kind { - case .machine, .localMachine, .terminalsPool, .workspacesGroup, .workspace: + case .machine, .localMachine, .terminalsPool, .displaysPool, .workspacesGroup, .workspace: return true case .pendingMachine: return true diff --git a/Sources/Surfaces/CmuxTuiSurfaceProviders.swift b/Sources/Surfaces/CmuxTuiSurfaceProviders.swift index fdffa89b33f3..a0fd871dad13 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProviders.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProviders.swift @@ -151,6 +151,7 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { let previousPrivateAddress = info.privateAddress refreshGeneration &+= 1 refreshCoordinator.invalidate() + displayCoordinator.invalidate() self.summary = summary if !supportsPortPreviews { portsCache = nil diff --git a/cmuxTests/CloudDisplayCatalogTests.swift b/cmuxTests/CloudDisplayCatalogTests.swift index a621f81e9a92..b731aef66dd9 100644 --- a/cmuxTests/CloudDisplayCatalogTests.swift +++ b/cmuxTests/CloudDisplayCatalogTests.swift @@ -65,6 +65,17 @@ struct CloudDisplayCatalogTests { #expect(!service.canCreate && service.snapshot == nil) } + @Test("Provider invalidation clears a previously discovered guest catalog") + func invalidationClearsSnapshot() async { + let service = CloudDisplayCoordinator { _, _ in + .init(exitCode: 0, stdout: initial, stderr: "") + } + await service.refresh() + #expect(service.snapshot != nil) + service.invalidate() + #expect(service.snapshot == nil && !service.isAvailable) + } + @Test("Display IDs and connection targets are scoped to the authenticated VM") func independentTargets() throws { From a440be5ae148381f71481de16a776f8b466d47f4 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 08:07:07 -0700 Subject: [PATCH 11/64] Preserve Dock display duplication identity --- Sources/Cloud/CloudGuestDisplaySnapshot.swift | 2 +- Sources/DockSplitStore+BrowserActions.swift | 13 ++++++++++++- cmuxTests/CloudDisplayCatalogTests.swift | 6 ++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Sources/Cloud/CloudGuestDisplaySnapshot.swift b/Sources/Cloud/CloudGuestDisplaySnapshot.swift index 39b5c99f0128..11b1b576bd51 100644 --- a/Sources/Cloud/CloudGuestDisplaySnapshot.swift +++ b/Sources/Cloud/CloudGuestDisplaySnapshot.swift @@ -15,7 +15,7 @@ struct CloudGuestDisplaySnapshot: Decodable, Sendable { Set(displays.map(\.id)).count == displays.count, displays.contains(where: { $0.number == 1 }), displays.allSatisfy({ (1...16).contains($0.number) && $0.id == "display:\($0.number)" && $0.port == 6900 + $0.number }), - created == nil || displays.contains(where: { $0.id == created }) else { + created == nil || (displays.contains(where: { $0.id == created }) && created != "display:1") else { throw SurfaceCatalogError.unsupported(Self.unavailableMessage) } } diff --git a/Sources/DockSplitStore+BrowserActions.swift b/Sources/DockSplitStore+BrowserActions.swift index b14c94fef48c..3cb1c910b4ad 100644 --- a/Sources/DockSplitStore+BrowserActions.swift +++ b/Sources/DockSplitStore+BrowserActions.swift @@ -15,13 +15,18 @@ extension DockSplitStore { return nil } let tabs = bonsplitController.tabs(inPane: paneId) + let catalog = SurfaceCatalog.shared + let record = catalog.projectionRecord(forPanel: panelId).flatMap { $0.resource.machine.isLocal ? nil : $0 } + let resource = record?.resource ?? browser.cloudAccess.resourceID + let isCloud = resource?.machine.isLocal == false + guard surfaceOwnershipPolicy.rejection(for: machineOwningSurface(panelId)) == nil else { return nil } guard let anchorIndex = tabs.firstIndex(where: { $0.id == anchorTabId }), let duplicatedPanelId = newSurface( kind: .browser, inPane: paneId, - url: browser.currentURLForTabDuplication, + url: isCloud ? nil : browser.currentURLForTabDuplication, focus: false, preferredProfileID: browser.profileID, chromeVisibility: browser.chromeVisibility, @@ -36,6 +41,12 @@ extension DockSplitStore { } let focusWindow = NSApp.keyWindow ?? NSApp.mainWindow + if let resource, isCloud { + duplicatedPanel.retainTransferredSurfaceMachine(resource.machine) + catalog.restore([SurfaceProjectionRecord(panelID: duplicatedPanel.id, resource: resource, + remoteWorkspaceID: record?.remoteWorkspaceID)], workspaceID: workspaceId) + duplicatedPanel.restoreCloudResource(resource) + } if focus { noteKeyboardFocusIntent(window: focusWindow) } diff --git a/cmuxTests/CloudDisplayCatalogTests.swift b/cmuxTests/CloudDisplayCatalogTests.swift index b731aef66dd9..81955602b213 100644 --- a/cmuxTests/CloudDisplayCatalogTests.swift +++ b/cmuxTests/CloudDisplayCatalogTests.swift @@ -65,6 +65,12 @@ struct CloudDisplayCatalogTests { #expect(!service.canCreate && service.snapshot == nil) } + @Test("The existing Desktop cannot be reported as a newly created display") + func rejectsDesktopCreationReceipt() { + let raw = #"{"version":1,"canCreate":true,"displays":[{"id":"display:1","number":1,"port":6901,"state":"running"}],"created":"display:1"}"# + #expect(throws: (any Error).self) { try CloudGuestDisplaySnapshot(data: Data(raw.utf8)) } + } + @Test("Provider invalidation clears a previously discovered guest catalog") func invalidationClearsSnapshot() async { let service = CloudDisplayCoordinator { _, _ in From 27a7fe891ba908d9d0bfbd13ead8c3732906b22a Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 08:18:24 -0700 Subject: [PATCH 12/64] Preserve Cloud display refresh and browser locations --- Sources/Cloud/CloudGuestDisplayScript.swift | 146 +++++++++--------- Sources/DockSplitStore+BrowserActions.swift | 2 +- .../Panels/BrowserPanel+CloudConnection.swift | 15 +- Sources/Panels/BrowserPanel.swift | 2 +- .../Surfaces/CmuxTuiSurfaceProviders.swift | 5 +- .../Workspace+BrowserDuplication.swift | 2 +- tests/fixtures/cmux-display | 2 +- tests/test_cloud_display_catalog.py | 2 +- 8 files changed, 95 insertions(+), 81 deletions(-) diff --git a/Sources/Cloud/CloudGuestDisplayScript.swift b/Sources/Cloud/CloudGuestDisplayScript.swift index 8673a2822f6b..6da456b3d3f7 100644 --- a/Sources/Cloud/CloudGuestDisplayScript.swift +++ b/Sources/Cloud/CloudGuestDisplayScript.swift @@ -85,79 +85,79 @@ cnQocmZiX3BvcnQpOgogICAgICAgICAgICByYWlzZSBSdW50aW1lRXJyb3IoIlh2bmMgZGlkIG5vdCBi YnByb2Nlc3MucnVuKFsiZGJ1cy1sYXVuY2giLCAiLS1zaC1zeW50YXgiXSwgZW52PWVudmlyb25tZW50LAogICAgICAgICAgICAgICAgICAgICAgICAgICAg IGNhcHR1cmVfb3V0cHV0PVRydWUsIHRleHQ9VHJ1ZSwgY2hlY2s9VHJ1ZSkuc3Rkb3V0CiAgICAgICAgZm9yIGxpbmUgaW4gYnVzLnNwbGl0bGluZXMoKToK ICAgICAgICAgICAgaWYgIj0iIG5vdCBpbiBsaW5lOgogICAgICAgICAgICAgICAgY29udGludWUKICAgICAgICAgICAga2V5LCB2YWx1ZSA9IGxpbmUuc3Bs -aXQoIj0iLCAxKQogICAgICAgICAgICBlbnZpcm9ubWVudFtrZXldID0gdmFsdWUuc3RyaXAoKS5zdHJpcCgiJyIpCiAgICAgICAgdHJ5OgogICAgICAgICAg -ICBkYnVzX3BpZCA9IGludChlbnZpcm9ubWVudC5nZXQoIkRCVVNfU0VTU0lPTl9CVVNfUElEIiwgIjAiKSkKICAgICAgICBleGNlcHQgVmFsdWVFcnJvcjoK -ICAgICAgICAgICAgZGJ1c19waWQgPSAwCiAgICAgICAgaWYgZGJ1c19waWQgPiAxOgogICAgICAgICAgICBwcm9jZXNzZXMuYXBwZW5kKE93bmVkUHJvY2Vz -cyhkYnVzX3BpZCkpCiAgICAgICAgZm9yIGV4ZWN1dGFibGUsIGFyZ3MgaW4gWwogICAgICAgICAgICAoIm9wZW5ib3giLCBbXSksICgidGludDIiLCBbIi1j -IiwgIi9ldGMvY211eC90aW50MnJjIl0pLAogICAgICAgICAgICAoInZuY2NvbmZpZyIsIFsiLW5vd2luIl0pLAogICAgICAgICAgICAod2Vic29ja2lmeSwg -WyItLXdlYiIsICIvdXNyL3NoYXJlL25vdm5jIiwgIi0taGVhcnRiZWF0IiwgIjMwIiwKICAgICAgICAgICAgICAgICAgICAgICAgICBmIjEyNy4wLjAuMTp7 -bm92bmNfcG9ydH0iLCBmIjEyNy4wLjAuMTp7cmZiX3BvcnR9Il0pLAogICAgICAgIF06CiAgICAgICAgICAgIHBhdGggPSBleGVjdXRhYmxlIGlmICIvIiBp -biBleGVjdXRhYmxlIGVsc2Ugc2h1dGlsLndoaWNoKGV4ZWN1dGFibGUpCiAgICAgICAgICAgIGlmIG5vdCBwYXRoOgogICAgICAgICAgICAgICAgaWYgZXhl -Y3V0YWJsZSA9PSAidm5jY29uZmlnIjoKICAgICAgICAgICAgICAgICAgICBjb250aW51ZQogICAgICAgICAgICAgICAgcmFpc2UgUnVudGltZUVycm9yKGYi -bWlzc2luZyBkaXNwbGF5IGNvbXBvbmVudDoge2V4ZWN1dGFibGV9IikKICAgICAgICAgICAgcHJvY2VzcyA9IHN1YnByb2Nlc3MuUG9wZW4oW3BhdGhdICsg -YXJncywgZW52PWVudmlyb25tZW50LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdGRpbj1zdWJwcm9jZXNzLkRFVk5VTEwsCiAg -ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0ZG91dD1zdWJwcm9jZXNzLkRFVk5VTEwsIHN0ZGVycj1zdWJwcm9jZXNzLkRFVk5VTEwp -CiAgICAgICAgICAgIHByb2Nlc3Nlcy5hcHBlbmQocHJvY2VzcykKICAgICAgICBpZiBub3Qgc2VsZi53YWl0X2Zvcl9wb3J0KG5vdm5jX3BvcnQsIHRpbWVv -dXQ9MTApOgogICAgICAgICAgICByYWlzZSBSdW50aW1lRXJyb3IoIm5vVk5DIGRpZCBub3QgYmVjb21lIHJlYWR5IikKICAgICAgICAocnVudGltZSAvICJl -bnYiKS53cml0ZV90ZXh0KGYiZXhwb3J0IERJU1BMQVk9OntudW1iZXJ9XG4iKQoKICAgIGRlZiBzdXBlcnZpc2Uoc2VsZiwgbnVtYmVyLCBmaW5pc2hlZCk6 -CiAgICAgICAgcnVudGltZSA9IHNlbGYucnVudGltZSAvIHN0cihudW1iZXIpCiAgICAgICAgcnVudGltZS5ta2Rpcihtb2RlPTBvNzAwLCBwYXJlbnRzPVRy -dWUsIGV4aXN0X29rPVRydWUpCiAgICAgICAgZW52aXJvbm1lbnQgPSBkaWN0KG9zLmVudmlyb24sIERJU1BMQVk9ZiI6e251bWJlcn0iLCBDTVVYX0RFU0tU -T1BfUlVOVElNRV9ESVI9c3RyKHJ1bnRpbWUpKQogICAgICAgIGZvciBrZXkgaW4gKCJOT1RJRllfU09DS0VUIiwgIkRCVVNfU0VTU0lPTl9CVVNfQUREUkVT -UyIsICJEQlVTX1NFU1NJT05fQlVTX1BJRCIsCiAgICAgICAgICAgICAgICAgICAgIkFUX1NQSV9CVVNfQUREUkVTUyIsICJBVF9TUElfQlVTIik6CiAgICAg -ICAgICAgIGVudmlyb25tZW50LnBvcChrZXksIE5vbmUpCiAgICAgICAgd2hpbGUgbm90IHNlbGYuc2h1dGRvd24uaXNfc2V0KCk6CiAgICAgICAgICAgIHRy -eToKICAgICAgICAgICAgICAgICMgVGhlIHN1cGVydmlzb3IgaXMgYSBoZWFsdGggY2hlY2ssIG5vdCBhIHRpbWVyLWRyaXZlbiByZXN0YXJ0LgogICAgICAg -ICAgICAgICAgIyBQcmVzZXJ2ZSBhbiBhY3RpdmUgUkZCL25vVk5DIHNlc3Npb24gdW50aWwgaXRzIG93bmVkIGVuZHBvaW50cyBmYWlsLgogICAgICAgICAg -ICAgICAgaWYgbm90IHNlbGYucHJvY2Vzc2VzLmdldChudW1iZXIpIG9yIG5vdCByZWFkeShudW1iZXIpOgogICAgICAgICAgICAgICAgICAgIHNlbGYuc3Rh -cnRfY29tcG9uZW50cyhudW1iZXIsIGVudmlyb25tZW50LCBydW50aW1lKQogICAgICAgICAgICAgICAgc3RhdGUgPSAicnVubmluZyIgaWYgcmVhZHkobnVt -YmVyKSBlbHNlICJ1bmF2YWlsYWJsZSIKICAgICAgICAgICAgZXhjZXB0IChPU0Vycm9yLCBSdW50aW1lRXJyb3IsIHN1YnByb2Nlc3MuU3VicHJvY2Vzc0Vy -cm9yKToKICAgICAgICAgICAgICAgIHNlbGYudGVybWluYXRlKG51bWJlcikKICAgICAgICAgICAgICAgIHN0YXRlID0gInVuYXZhaWxhYmxlIgogICAgICAg -ICAgICB3aXRoIHNlbGYubG9jazoKICAgICAgICAgICAgICAgIHNlbGYuc3RhdGVzW251bWJlcl0gPSBzdGF0ZQogICAgICAgICAgICBmaW5pc2hlZC5zZXQo -KQogICAgICAgICAgICBzZWxmLnNodXRkb3duLndhaXQoMzApCiAgICAgICAgc2VsZi50ZXJtaW5hdGUobnVtYmVyKQoKICAgIGRlZiBzbmFwc2hvdChzZWxm -KToKICAgICAgICB3aXRoIHNlbGYubG9jazoKICAgICAgICAgICAgcmV0dXJuIHsidmVyc2lvbiI6IDEsICJjYW5DcmVhdGUiOiBsZW4oc2VsZi5qb2JzKSA8 -IE1BWF9ESVNQTEFZUyAtIDEsCiAgICAgICAgICAgICAgICAgICAgImRpc3BsYXlzIjogW2Rlc2NyaXB0b3IoMSwgInJ1bm5pbmciIGlmIHJlYWR5KDEpIGVs -c2UgInVuYXZhaWxhYmxlIildCiAgICAgICAgICAgICAgICAgICAgKyBbZGVzY3JpcHRvcihuLCBzZWxmLnN0YXRlcy5nZXQobiwgImxhdW5jaGluZyIpKSBm -b3IgbiBpbiBzZWxmLmNhdGFsb2cubnVtYmVycygpXX0KCiAgICBkZWYgaGFuZGxlKHNlbGYsIHJlcXVlc3QpOgogICAgICAgIGlmIHJlcXVlc3QuZ2V0KCJh -Y3Rpb24iKSA9PSAibGlzdCI6CiAgICAgICAgICAgIHJldHVybiBzZWxmLnNuYXBzaG90KCkKICAgICAgICBpZiByZXF1ZXN0LmdldCgiYWN0aW9uIikgIT0g -ImNyZWF0ZSI6CiAgICAgICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoInVuc3VwcG9ydGVkIGRpc3BsYXkgYWN0aW9uIikKICAgICAgICBudW1iZXIgPSBzZWxm -LmNhdGFsb2cuYWxsb2NhdGUocmVxdWVzdC5nZXQoInJlcXVlc3QiKSkKICAgICAgICBzZWxmLnN0YXJ0KG51bWJlcikud2FpdCg1NSkKICAgICAgICByZXN1 -bHQgPSBzZWxmLnNuYXBzaG90KCkKICAgICAgICByZXN1bHRbImNyZWF0ZWQiXSA9IGYiZGlzcGxheTp7bnVtYmVyfSIKICAgICAgICBpZiBzZWxmLnN0YXRl -cy5nZXQobnVtYmVyKSAhPSAicnVubmluZyI6CiAgICAgICAgICAgIHJlc3VsdFsiZXJyb3IiXSA9ICJkaXNwbGF5X3N0YXJ0X2ZhaWxlZCIKICAgICAgICBy -ZXR1cm4gcmVzdWx0CgoKZGVmIHNlcnZlKHJ1bnRpbWUpOgogICAgcnVudGltZSA9IFBhdGgocnVudGltZSkKICAgIHJ1bnRpbWUubWtkaXIobW9kZT0wbzcw -MCwgcGFyZW50cz1UcnVlLCBleGlzdF9vaz1UcnVlKQogICAgd2l0aCAocnVudGltZSAvICJkaXNwbGF5LWNvbnRyb2wubG9jayIpLm9wZW4oInciKSBhcyBs -b2NrOgogICAgICAgIHRyeToKICAgICAgICAgICAgZmNudGwuZmxvY2sobG9jaywgZmNudGwuTE9DS19FWCB8IGZjbnRsLkxPQ0tfTkIpCiAgICAgICAgZXhj -ZXB0IEJsb2NraW5nSU9FcnJvcjoKICAgICAgICAgICAgcmV0dXJuCiAgICAgICAgaWYgbm90IGFsbChzaHV0aWwud2hpY2gobmFtZSkgZm9yIG5hbWUgaW4g -KCJ3ZWJzb2NraWZ5IiwgIm9wZW5ib3giLCAiZGJ1cy1sYXVuY2giKSk6CiAgICAgICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoImRpc3BsYXkgcnVudGltZSB1 -bmF2YWlsYWJsZSIpCiAgICAgICAgaWYgbm90IChzaHV0aWwud2hpY2goIlh2bmMiKSBvciBzaHV0aWwud2hpY2goIlh0aWdlcnZuYyIpKToKICAgICAgICAg -ICAgcmFpc2UgVmFsdWVFcnJvcigiZGlzcGxheSBydW50aW1lIHVuYXZhaWxhYmxlIikKICAgICAgICBzZXJ2aWNlID0gRGlzcGxheVNlcnZpY2UoRGlzcGxh -eUNhdGFsb2coUGF0aC5ob21lKCkgLyAiLmNtdXgiIC8gImRpc3BsYXlzIiksIHJ1bnRpbWUgLyAiZGlzcGxheXMiKQogICAgICAgIGZvciBudW1iZXIgaW4g -c2VydmljZS5jYXRhbG9nLm51bWJlcnMoKToKICAgICAgICAgICAgc2VydmljZS5zdGFydChudW1iZXIpCiAgICAgICAgcGF0aCA9IHJ1bnRpbWUgLyAiZGlz -cGxheS1jb250cm9sLnNvY2siCiAgICAgICAgaWYgcGF0aC5leGlzdHMoKToKICAgICAgICAgICAgcGF0aC51bmxpbmsoKQoKICAgICAgICBjbGFzcyBIYW5k -bGVyKHNvY2tldHNlcnZlci5TdHJlYW1SZXF1ZXN0SGFuZGxlcik6CiAgICAgICAgICAgIGRlZiBoYW5kbGUoc2VsZik6CiAgICAgICAgICAgICAgICBzZWxm -LmNvbm5lY3Rpb24uc2V0dGltZW91dCg2MCkKICAgICAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgICAgICByYXcgPSBzZWxmLnJmaWxlLnJlYWRs -aW5lKDQwOTcpCiAgICAgICAgICAgICAgICAgICAgaWYgbGVuKHJhdykgPiA0MDk2OgogICAgICAgICAgICAgICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9y -KCJkaXNwbGF5IHJlcXVlc3QgdG9vIGxhcmdlIikKICAgICAgICAgICAgICAgICAgICByZXN1bHQgPSBzZXJ2aWNlLmhhbmRsZShqc29uLmxvYWRzKHJhdykp -CiAgICAgICAgICAgICAgICBleGNlcHQgKFZhbHVlRXJyb3IsIEtleUVycm9yLCBUeXBlRXJyb3IsIE9TRXJyb3IpOgogICAgICAgICAgICAgICAgICAgIHJl -c3VsdCA9IHsiZXJyb3IiOiAiZGlzcGxheV9yZXF1ZXN0X2ZhaWxlZCJ9CiAgICAgICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICAgICAgc2VsZi53 -ZmlsZS53cml0ZShqc29uLmR1bXBzKHJlc3VsdCkuZW5jb2RlKCkgKyBiIlxuIikKICAgICAgICAgICAgICAgIGV4Y2VwdCBPU0Vycm9yOgogICAgICAgICAg -ICAgICAgICAgIHBhc3MgICMgVGhlIHBlcnNpc3RlZCByZWNlaXB0IHJlbWFpbnMgcmVwbGF5YWJsZSBhZnRlciBkaXNjb25uZWN0LgoKICAgICAgICBjbGFz -cyBTZXJ2ZXIoc29ja2V0c2VydmVyLlRocmVhZGluZ1VuaXhTdHJlYW1TZXJ2ZXIpOgogICAgICAgICAgICBkYWVtb25fdGhyZWFkcyA9IFRydWUKCiAgICAg -ICAgcHJldmlvdXNfdW1hc2sgPSBvcy51bWFzaygwbzA3NykKICAgICAgICB0cnk6CiAgICAgICAgICAgIHdpdGggU2VydmVyKHN0cihwYXRoKSwgSGFuZGxl -cikgYXMgc2VydmVyOgogICAgICAgICAgICAgICAgb3MudW1hc2socHJldmlvdXNfdW1hc2spCiAgICAgICAgICAgICAgICBzZXJ2ZXIuc2VydmVfZm9yZXZl -cigpCiAgICAgICAgZmluYWxseToKICAgICAgICAgICAgb3MudW1hc2socHJldmlvdXNfdW1hc2spCiAgICAgICAgICAgIHNlcnZpY2Uuc2h1dGRvd24uc2V0 -KCkKICAgICAgICAgICAgcGF0aC51bmxpbmsobWlzc2luZ19vaz1UcnVlKQoKCmRlZiBtYWluKCk6CiAgICBwYXJzZXIgPSBhcmdwYXJzZS5Bcmd1bWVudFBh -cnNlcihkZXNjcmlwdGlvbj1fX2RvY19fKQogICAgcGFyc2VyLmFkZF9hcmd1bWVudCgiYWN0aW9uIiwgY2hvaWNlcz1bImxpc3QiLCAiY3JlYXRlIiwgInNl -cnZlIl0pCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50KCItLXJlcXVlc3QtaWQiKQogICAgYXJncyA9IHBhcnNlci5wYXJzZV9hcmdzKCkKICAgIHJ1bnRpbWUg -PSBvcy5lbnZpcm9uLmdldCgiQ01VWF9ERVNLVE9QX1JVTlRJTUVfRElSIiwgIi9ydW4vY211eC1kZXNrdG9wIikKICAgIGlmIGFyZ3MuYWN0aW9uID09ICJz -ZXJ2ZSI6CiAgICAgICAgc2VydmUocnVudGltZSkKICAgICAgICByZXR1cm4gMAogICAgaWYgYXJncy5hY3Rpb24gPT0gImNyZWF0ZSIgYW5kIG5vdCBhcmdz -LnJlcXVlc3RfaWQ6CiAgICAgICAgcGFyc2VyLmVycm9yKCJjcmVhdGUgcmVxdWlyZXMgLS1yZXF1ZXN0LWlkIChyZXRhaW4gaXQgd2hlbiByZXRyeWluZyki -KQogICAgdHJ5OgogICAgICAgIHdpdGggc29ja2V0LnNvY2tldChzb2NrZXQuQUZfVU5JWCwgc29ja2V0LlNPQ0tfU1RSRUFNKSBhcyBjbGllbnQ6CiAgICAg -ICAgICAgIGNsaWVudC5zZXR0aW1lb3V0KDYwKQogICAgICAgICAgICBjbGllbnQuY29ubmVjdChzdHIoUGF0aChydW50aW1lKSAvICJkaXNwbGF5LWNvbnRy -b2wuc29jayIpKQogICAgICAgICAgICBjbGllbnQuc2VuZGFsbChqc29uLmR1bXBzKHsiYWN0aW9uIjogYXJncy5hY3Rpb24sICJyZXF1ZXN0IjogYXJncy5y -ZXF1ZXN0X2lkfSkuZW5jb2RlKCkgKyBiIlxuIikKICAgICAgICAgICAgd2l0aCBjbGllbnQubWFrZWZpbGUoInJiIikgYXMgc3RyZWFtOgogICAgICAgICAg -ICAgICAgcmVzdWx0ID0ganNvbi5sb2FkcyhzdHJlYW0ucmVhZGxpbmUoNjU1MzYpKQogICAgZXhjZXB0IChPU0Vycm9yLCBWYWx1ZUVycm9yKToKICAgICAg -ICByZXN1bHQgPSB7ImVycm9yIjogImRpc3BsYXlfcnVudGltZV91bmF2YWlsYWJsZSJ9CiAgICBwcmludChqc29uLmR1bXBzKHJlc3VsdCkpCiAgICByZXR1 -cm4gMSBpZiAiZXJyb3IiIGluIHJlc3VsdCBlbHNlIDAKCgppZiBfX25hbWVfXyA9PSAiX19tYWluX18iOgogICAgcmFpc2UgU3lzdGVtRXhpdChtYWluKCkp -Cg== +aXQoIj0iLCAxKQogICAgICAgICAgICBlbnZpcm9ubWVudFtrZXldID0gdmFsdWUuc3RyaXAoKS5yc3RyaXAoIjsiKS5zdHJpcCgpLnN0cmlwKCInIikKICAg +ICAgICB0cnk6CiAgICAgICAgICAgIGRidXNfcGlkID0gaW50KGVudmlyb25tZW50LmdldCgiREJVU19TRVNTSU9OX0JVU19QSUQiLCAiMCIpKQogICAgICAg +IGV4Y2VwdCBWYWx1ZUVycm9yOgogICAgICAgICAgICBkYnVzX3BpZCA9IDAKICAgICAgICBpZiBkYnVzX3BpZCA+IDE6CiAgICAgICAgICAgIHByb2Nlc3Nl +cy5hcHBlbmQoT3duZWRQcm9jZXNzKGRidXNfcGlkKSkKICAgICAgICBmb3IgZXhlY3V0YWJsZSwgYXJncyBpbiBbCiAgICAgICAgICAgICgib3BlbmJveCIs +IFtdKSwgKCJ0aW50MiIsIFsiLWMiLCAiL2V0Yy9jbXV4L3RpbnQycmMiXSksCiAgICAgICAgICAgICgidm5jY29uZmlnIiwgWyItbm93aW4iXSksCiAgICAg +ICAgICAgICh3ZWJzb2NraWZ5LCBbIi0td2ViIiwgIi91c3Ivc2hhcmUvbm92bmMiLCAiLS1oZWFydGJlYXQiLCAiMzAiLAogICAgICAgICAgICAgICAgICAg +ICAgICAgIGYiMTI3LjAuMC4xOntub3ZuY19wb3J0fSIsIGYiMTI3LjAuMC4xOntyZmJfcG9ydH0iXSksCiAgICAgICAgXToKICAgICAgICAgICAgcGF0aCA9 +IGV4ZWN1dGFibGUgaWYgIi8iIGluIGV4ZWN1dGFibGUgZWxzZSBzaHV0aWwud2hpY2goZXhlY3V0YWJsZSkKICAgICAgICAgICAgaWYgbm90IHBhdGg6CiAg +ICAgICAgICAgICAgICBpZiBleGVjdXRhYmxlID09ICJ2bmNjb25maWciOgogICAgICAgICAgICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgICAgICAgICBy +YWlzZSBSdW50aW1lRXJyb3IoZiJtaXNzaW5nIGRpc3BsYXkgY29tcG9uZW50OiB7ZXhlY3V0YWJsZX0iKQogICAgICAgICAgICBwcm9jZXNzID0gc3VicHJv +Y2Vzcy5Qb3BlbihbcGF0aF0gKyBhcmdzLCBlbnY9ZW52aXJvbm1lbnQsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0ZGluPXN1 +YnByb2Nlc3MuREVWTlVMTCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3Rkb3V0PXN1YnByb2Nlc3MuREVWTlVMTCwgc3RkZXJy +PXN1YnByb2Nlc3MuREVWTlVMTCkKICAgICAgICAgICAgcHJvY2Vzc2VzLmFwcGVuZChwcm9jZXNzKQogICAgICAgIGlmIG5vdCBzZWxmLndhaXRfZm9yX3Bv +cnQobm92bmNfcG9ydCwgdGltZW91dD0xMCk6CiAgICAgICAgICAgIHJhaXNlIFJ1bnRpbWVFcnJvcigibm9WTkMgZGlkIG5vdCBiZWNvbWUgcmVhZHkiKQog +ICAgICAgIChydW50aW1lIC8gImVudiIpLndyaXRlX3RleHQoZiJleHBvcnQgRElTUExBWT06e251bWJlcn1cbiIpCgogICAgZGVmIHN1cGVydmlzZShzZWxm +LCBudW1iZXIsIGZpbmlzaGVkKToKICAgICAgICBydW50aW1lID0gc2VsZi5ydW50aW1lIC8gc3RyKG51bWJlcikKICAgICAgICBydW50aW1lLm1rZGlyKG1v +ZGU9MG83MDAsIHBhcmVudHM9VHJ1ZSwgZXhpc3Rfb2s9VHJ1ZSkKICAgICAgICBlbnZpcm9ubWVudCA9IGRpY3Qob3MuZW52aXJvbiwgRElTUExBWT1mIjp7 +bnVtYmVyfSIsIENNVVhfREVTS1RPUF9SVU5USU1FX0RJUj1zdHIocnVudGltZSkpCiAgICAgICAgZm9yIGtleSBpbiAoIk5PVElGWV9TT0NLRVQiLCAiREJV +U19TRVNTSU9OX0JVU19BRERSRVNTIiwgIkRCVVNfU0VTU0lPTl9CVVNfUElEIiwKICAgICAgICAgICAgICAgICAgICAiQVRfU1BJX0JVU19BRERSRVNTIiwg +IkFUX1NQSV9CVVMiKToKICAgICAgICAgICAgZW52aXJvbm1lbnQucG9wKGtleSwgTm9uZSkKICAgICAgICB3aGlsZSBub3Qgc2VsZi5zaHV0ZG93bi5pc19z +ZXQoKToKICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgIyBUaGUgc3VwZXJ2aXNvciBpcyBhIGhlYWx0aCBjaGVjaywgbm90IGEgdGltZXItZHJp +dmVuIHJlc3RhcnQuCiAgICAgICAgICAgICAgICAjIFByZXNlcnZlIGFuIGFjdGl2ZSBSRkIvbm9WTkMgc2Vzc2lvbiB1bnRpbCBpdHMgb3duZWQgZW5kcG9p +bnRzIGZhaWwuCiAgICAgICAgICAgICAgICBpZiBub3Qgc2VsZi5wcm9jZXNzZXMuZ2V0KG51bWJlcikgb3Igbm90IHJlYWR5KG51bWJlcik6CiAgICAgICAg +ICAgICAgICAgICAgc2VsZi5zdGFydF9jb21wb25lbnRzKG51bWJlciwgZW52aXJvbm1lbnQsIHJ1bnRpbWUpCiAgICAgICAgICAgICAgICBzdGF0ZSA9ICJy +dW5uaW5nIiBpZiByZWFkeShudW1iZXIpIGVsc2UgInVuYXZhaWxhYmxlIgogICAgICAgICAgICBleGNlcHQgKE9TRXJyb3IsIFJ1bnRpbWVFcnJvciwgc3Vi +cHJvY2Vzcy5TdWJwcm9jZXNzRXJyb3IpOgogICAgICAgICAgICAgICAgc2VsZi50ZXJtaW5hdGUobnVtYmVyKQogICAgICAgICAgICAgICAgc3RhdGUgPSAi +dW5hdmFpbGFibGUiCiAgICAgICAgICAgIHdpdGggc2VsZi5sb2NrOgogICAgICAgICAgICAgICAgc2VsZi5zdGF0ZXNbbnVtYmVyXSA9IHN0YXRlCiAgICAg +ICAgICAgIGZpbmlzaGVkLnNldCgpCiAgICAgICAgICAgIHNlbGYuc2h1dGRvd24ud2FpdCgzMCkKICAgICAgICBzZWxmLnRlcm1pbmF0ZShudW1iZXIpCgog +ICAgZGVmIHNuYXBzaG90KHNlbGYpOgogICAgICAgIHdpdGggc2VsZi5sb2NrOgogICAgICAgICAgICByZXR1cm4geyJ2ZXJzaW9uIjogMSwgImNhbkNyZWF0 +ZSI6IGxlbihzZWxmLmpvYnMpIDwgTUFYX0RJU1BMQVlTIC0gMSwKICAgICAgICAgICAgICAgICAgICAiZGlzcGxheXMiOiBbZGVzY3JpcHRvcigxLCAicnVu +bmluZyIgaWYgcmVhZHkoMSkgZWxzZSAidW5hdmFpbGFibGUiKV0KICAgICAgICAgICAgICAgICAgICArIFtkZXNjcmlwdG9yKG4sIHNlbGYuc3RhdGVzLmdl +dChuLCAibGF1bmNoaW5nIikpIGZvciBuIGluIHNlbGYuY2F0YWxvZy5udW1iZXJzKCldfQoKICAgIGRlZiBoYW5kbGUoc2VsZiwgcmVxdWVzdCk6CiAgICAg +ICAgaWYgcmVxdWVzdC5nZXQoImFjdGlvbiIpID09ICJsaXN0IjoKICAgICAgICAgICAgcmV0dXJuIHNlbGYuc25hcHNob3QoKQogICAgICAgIGlmIHJlcXVl +c3QuZ2V0KCJhY3Rpb24iKSAhPSAiY3JlYXRlIjoKICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJvcigidW5zdXBwb3J0ZWQgZGlzcGxheSBhY3Rpb24iKQog +ICAgICAgIG51bWJlciA9IHNlbGYuY2F0YWxvZy5hbGxvY2F0ZShyZXF1ZXN0LmdldCgicmVxdWVzdCIpKQogICAgICAgIHNlbGYuc3RhcnQobnVtYmVyKS53 +YWl0KDU1KQogICAgICAgIHJlc3VsdCA9IHNlbGYuc25hcHNob3QoKQogICAgICAgIHJlc3VsdFsiY3JlYXRlZCJdID0gZiJkaXNwbGF5OntudW1iZXJ9Igog +ICAgICAgIGlmIHNlbGYuc3RhdGVzLmdldChudW1iZXIpICE9ICJydW5uaW5nIjoKICAgICAgICAgICAgcmVzdWx0WyJlcnJvciJdID0gImRpc3BsYXlfc3Rh +cnRfZmFpbGVkIgogICAgICAgIHJldHVybiByZXN1bHQKCgpkZWYgc2VydmUocnVudGltZSk6CiAgICBydW50aW1lID0gUGF0aChydW50aW1lKQogICAgcnVu +dGltZS5ta2Rpcihtb2RlPTBvNzAwLCBwYXJlbnRzPVRydWUsIGV4aXN0X29rPVRydWUpCiAgICB3aXRoIChydW50aW1lIC8gImRpc3BsYXktY29udHJvbC5s +b2NrIikub3BlbigidyIpIGFzIGxvY2s6CiAgICAgICAgdHJ5OgogICAgICAgICAgICBmY250bC5mbG9jayhsb2NrLCBmY250bC5MT0NLX0VYIHwgZmNudGwu +TE9DS19OQikKICAgICAgICBleGNlcHQgQmxvY2tpbmdJT0Vycm9yOgogICAgICAgICAgICByZXR1cm4KICAgICAgICBpZiBub3QgYWxsKHNodXRpbC53aGlj +aChuYW1lKSBmb3IgbmFtZSBpbiAoIndlYnNvY2tpZnkiLCAib3BlbmJveCIsICJkYnVzLWxhdW5jaCIpKToKICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJv +cigiZGlzcGxheSBydW50aW1lIHVuYXZhaWxhYmxlIikKICAgICAgICBpZiBub3QgKHNodXRpbC53aGljaCgiWHZuYyIpIG9yIHNodXRpbC53aGljaCgiWHRp +Z2Vydm5jIikpOgogICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJkaXNwbGF5IHJ1bnRpbWUgdW5hdmFpbGFibGUiKQogICAgICAgIHNlcnZpY2UgPSBE +aXNwbGF5U2VydmljZShEaXNwbGF5Q2F0YWxvZyhQYXRoLmhvbWUoKSAvICIuY211eCIgLyAiZGlzcGxheXMiKSwgcnVudGltZSAvICJkaXNwbGF5cyIpCiAg +ICAgICAgZm9yIG51bWJlciBpbiBzZXJ2aWNlLmNhdGFsb2cubnVtYmVycygpOgogICAgICAgICAgICBzZXJ2aWNlLnN0YXJ0KG51bWJlcikKICAgICAgICBw +YXRoID0gcnVudGltZSAvICJkaXNwbGF5LWNvbnRyb2wuc29jayIKICAgICAgICBpZiBwYXRoLmV4aXN0cygpOgogICAgICAgICAgICBwYXRoLnVubGluaygp +CgogICAgICAgIGNsYXNzIEhhbmRsZXIoc29ja2V0c2VydmVyLlN0cmVhbVJlcXVlc3RIYW5kbGVyKToKICAgICAgICAgICAgZGVmIGhhbmRsZShzZWxmKToK +ICAgICAgICAgICAgICAgIHNlbGYuY29ubmVjdGlvbi5zZXR0aW1lb3V0KDYwKQogICAgICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgIHJh +dyA9IHNlbGYucmZpbGUucmVhZGxpbmUoNDA5NykKICAgICAgICAgICAgICAgICAgICBpZiBsZW4ocmF3KSA+IDQwOTY6CiAgICAgICAgICAgICAgICAgICAg +ICAgIHJhaXNlIFZhbHVlRXJyb3IoImRpc3BsYXkgcmVxdWVzdCB0b28gbGFyZ2UiKQogICAgICAgICAgICAgICAgICAgIHJlc3VsdCA9IHNlcnZpY2UuaGFu +ZGxlKGpzb24ubG9hZHMocmF3KSkKICAgICAgICAgICAgICAgIGV4Y2VwdCAoVmFsdWVFcnJvciwgS2V5RXJyb3IsIFR5cGVFcnJvciwgT1NFcnJvcik6CiAg +ICAgICAgICAgICAgICAgICAgcmVzdWx0ID0geyJlcnJvciI6ICJkaXNwbGF5X3JlcXVlc3RfZmFpbGVkIn0KICAgICAgICAgICAgICAgIHRyeToKICAgICAg +ICAgICAgICAgICAgICBzZWxmLndmaWxlLndyaXRlKGpzb24uZHVtcHMocmVzdWx0KS5lbmNvZGUoKSArIGIiXG4iKQogICAgICAgICAgICAgICAgZXhjZXB0 +IE9TRXJyb3I6CiAgICAgICAgICAgICAgICAgICAgcGFzcyAgIyBUaGUgcGVyc2lzdGVkIHJlY2VpcHQgcmVtYWlucyByZXBsYXlhYmxlIGFmdGVyIGRpc2Nv +bm5lY3QuCgogICAgICAgIGNsYXNzIFNlcnZlcihzb2NrZXRzZXJ2ZXIuVGhyZWFkaW5nVW5peFN0cmVhbVNlcnZlcik6CiAgICAgICAgICAgIGRhZW1vbl90 +aHJlYWRzID0gVHJ1ZQoKICAgICAgICBwcmV2aW91c191bWFzayA9IG9zLnVtYXNrKDBvMDc3KQogICAgICAgIHRyeToKICAgICAgICAgICAgd2l0aCBTZXJ2 +ZXIoc3RyKHBhdGgpLCBIYW5kbGVyKSBhcyBzZXJ2ZXI6CiAgICAgICAgICAgICAgICBvcy51bWFzayhwcmV2aW91c191bWFzaykKICAgICAgICAgICAgICAg +IHNlcnZlci5zZXJ2ZV9mb3JldmVyKCkKICAgICAgICBmaW5hbGx5OgogICAgICAgICAgICBvcy51bWFzayhwcmV2aW91c191bWFzaykKICAgICAgICAgICAg +c2VydmljZS5zaHV0ZG93bi5zZXQoKQogICAgICAgICAgICBwYXRoLnVubGluayhtaXNzaW5nX29rPVRydWUpCgoKZGVmIG1haW4oKToKICAgIHBhcnNlciA9 +IGFyZ3BhcnNlLkFyZ3VtZW50UGFyc2VyKGRlc2NyaXB0aW9uPV9fZG9jX18pCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50KCJhY3Rpb24iLCBjaG9pY2VzPVsi +bGlzdCIsICJjcmVhdGUiLCAic2VydmUiXSkKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoIi0tcmVxdWVzdC1pZCIpCiAgICBhcmdzID0gcGFyc2VyLnBhcnNl +X2FyZ3MoKQogICAgcnVudGltZSA9IG9zLmVudmlyb24uZ2V0KCJDTVVYX0RFU0tUT1BfUlVOVElNRV9ESVIiLCAiL3J1bi9jbXV4LWRlc2t0b3AiKQogICAg +aWYgYXJncy5hY3Rpb24gPT0gInNlcnZlIjoKICAgICAgICBzZXJ2ZShydW50aW1lKQogICAgICAgIHJldHVybiAwCiAgICBpZiBhcmdzLmFjdGlvbiA9PSAi +Y3JlYXRlIiBhbmQgbm90IGFyZ3MucmVxdWVzdF9pZDoKICAgICAgICBwYXJzZXIuZXJyb3IoImNyZWF0ZSByZXF1aXJlcyAtLXJlcXVlc3QtaWQgKHJldGFp +biBpdCB3aGVuIHJldHJ5aW5nKSIpCiAgICB0cnk6CiAgICAgICAgd2l0aCBzb2NrZXQuc29ja2V0KHNvY2tldC5BRl9VTklYLCBzb2NrZXQuU09DS19TVFJF +QU0pIGFzIGNsaWVudDoKICAgICAgICAgICAgY2xpZW50LnNldHRpbWVvdXQoNjApCiAgICAgICAgICAgIGNsaWVudC5jb25uZWN0KHN0cihQYXRoKHJ1bnRp +bWUpIC8gImRpc3BsYXktY29udHJvbC5zb2NrIikpCiAgICAgICAgICAgIGNsaWVudC5zZW5kYWxsKGpzb24uZHVtcHMoeyJhY3Rpb24iOiBhcmdzLmFjdGlv +biwgInJlcXVlc3QiOiBhcmdzLnJlcXVlc3RfaWR9KS5lbmNvZGUoKSArIGIiXG4iKQogICAgICAgICAgICB3aXRoIGNsaWVudC5tYWtlZmlsZSgicmIiKSBh +cyBzdHJlYW06CiAgICAgICAgICAgICAgICByZXN1bHQgPSBqc29uLmxvYWRzKHN0cmVhbS5yZWFkbGluZSg2NTUzNikpCiAgICBleGNlcHQgKE9TRXJyb3Is +IFZhbHVlRXJyb3IpOgogICAgICAgIHJlc3VsdCA9IHsiZXJyb3IiOiAiZGlzcGxheV9ydW50aW1lX3VuYXZhaWxhYmxlIn0KICAgIHByaW50KGpzb24uZHVt +cHMocmVzdWx0KSkKICAgIHJldHVybiAxIGlmICJlcnJvciIgaW4gcmVzdWx0IGVsc2UgMAoKCmlmIF9fbmFtZV9fID09ICJfX21haW5fXyI6CiAgICByYWlz +ZSBTeXN0ZW1FeGl0KG1haW4oKSkK """ private static let path = "$HOME/.cmux/cmux-display" diff --git a/Sources/DockSplitStore+BrowserActions.swift b/Sources/DockSplitStore+BrowserActions.swift index 3cb1c910b4ad..5e16039ab189 100644 --- a/Sources/DockSplitStore+BrowserActions.swift +++ b/Sources/DockSplitStore+BrowserActions.swift @@ -45,7 +45,7 @@ extension DockSplitStore { duplicatedPanel.retainTransferredSurfaceMachine(resource.machine) catalog.restore([SurfaceProjectionRecord(panelID: duplicatedPanel.id, resource: resource, remoteWorkspaceID: record?.remoteWorkspaceID)], workspaceID: workspaceId) - duplicatedPanel.restoreCloudResource(resource) + duplicatedPanel.restoreCloudResource(resource, preferredURL: browser.currentURLForTabDuplication) } if focus { noteKeyboardFocusIntent(window: focusWindow) diff --git a/Sources/Panels/BrowserPanel+CloudConnection.swift b/Sources/Panels/BrowserPanel+CloudConnection.swift index 361b9540cd47..9836e10b6da2 100644 --- a/Sources/Panels/BrowserPanel+CloudConnection.swift +++ b/Sources/Panels/BrowserPanel+CloudConnection.swift @@ -9,7 +9,7 @@ extension BrowserPanel { /// Restore by stable resource identity before loading any saved address. /// A stale/unknown provider leaves an owned placeholder, never a local page. - func restoreCloudResource(_ resource: SurfaceResourceID) { + func restoreCloudResource(_ resource: SurfaceResourceID, preferredURL: URL? = nil) { let catalog = SurfaceCatalog.shared do { try catalog.validateOwnership(of: [resource], at: .workspace(id: workspaceId, placement: .tab)) } catch { cloudAccess.showUnavailable(SurfaceTransferRejection.cloudMachineMismatch.message); return } @@ -23,11 +23,22 @@ extension BrowserPanel { } switch CloudPortRoutePlan.plan(resource: known, privateAddress: provider.info.privateAddress) { case .privateDirect(let raw): - if let url = URL(string: raw) { provider.configureBrowser(self, url: url, resourceID: resource) } + if let url = URL(string: raw) { + provider.configureBrowser(self, url: Self.cloudRestoredURL(preferredURL, on: url), resourceID: resource) + } case .unsupported(let message): cloudAccess.showUnavailable(message) } } + private static func cloudRestoredURL(_ preferred: URL?, on target: URL) -> URL { + guard let preferred, var components = URLComponents(url: target, resolvingAgainstBaseURL: false), + let saved = URLComponents(url: preferred, resolvingAgainstBaseURL: false) else { return target } + components.path = saved.path.isEmpty ? components.path : saved.path + components.query = saved.query + components.fragment = saved.fragment + return components.url ?? target + } + /// Cloud panes use their own persistent data store so configuring one VM cannot reroute another. func prepareCloudBrowserStore(machineID: String) { let identifier = CloudBrowserRouting.storeID(panelID: id, profileID: profileID, machineID: machineID) diff --git a/Sources/Panels/BrowserPanel.swift b/Sources/Panels/BrowserPanel.swift index d2190b7c34d3..fed568668362 100644 --- a/Sources/Panels/BrowserPanel.swift +++ b/Sources/Panels/BrowserPanel.swift @@ -4518,7 +4518,7 @@ final class BrowserPanel: Panel, ObservableObject { ) currentURL = restoredURL - if let resource = snapshot.cloudResource { restoreCloudResource(resource); return } + if let resource = snapshot.cloudResource { restoreCloudResource(resource, preferredURL: restoredURL); return } guard shouldRenderRestoredWebView, let restoredURL else { shouldRenderWebView = false refreshNavigationAvailability() diff --git a/Sources/Surfaces/CmuxTuiSurfaceProviders.swift b/Sources/Surfaces/CmuxTuiSurfaceProviders.swift index a0fd871dad13..fa630da426f1 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProviders.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProviders.swift @@ -148,10 +148,13 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { func update(summary: VMSummary) { guard let current = catalog.provider(for: machine), ObjectIdentifier(current) == ObjectIdentifier(self) else { return } isFeatureSuspended = false + let displayIdentityChanged = self.summary.id != summary.id + || self.summary.provider != summary.provider + || self.summary.image != summary.image let previousPrivateAddress = info.privateAddress refreshGeneration &+= 1 refreshCoordinator.invalidate() - displayCoordinator.invalidate() + if displayIdentityChanged { displayCoordinator.invalidate() } self.summary = summary if !supportsPortPreviews { portsCache = nil diff --git a/Sources/Surfaces/Workspace+BrowserDuplication.swift b/Sources/Surfaces/Workspace+BrowserDuplication.swift index 5e199c243d42..5fd9eed297a9 100644 --- a/Sources/Surfaces/Workspace+BrowserDuplication.swift +++ b/Sources/Surfaces/Workspace+BrowserDuplication.swift @@ -34,7 +34,7 @@ extension Workspace { newPanel.showCloudAddress(url) model.connect() } else { - newPanel.restoreCloudResource(resource) + newPanel.restoreCloudResource(resource, preferredURL: browser.currentURLForTabDuplication) } } newPanel.setMuted(browser.isMuted) diff --git a/tests/fixtures/cmux-display b/tests/fixtures/cmux-display index 74b377f6a4b2..10509e01b758 100755 --- a/tests/fixtures/cmux-display +++ b/tests/fixtures/cmux-display @@ -198,7 +198,7 @@ class DisplayService: if "=" not in line: continue key, value = line.split("=", 1) - environment[key] = value.strip().strip("'") + environment[key] = value.strip().rstrip(";").strip().strip("'") try: dbus_pid = int(environment.get("DBUS_SESSION_BUS_PID", "0")) except ValueError: diff --git a/tests/test_cloud_display_catalog.py b/tests/test_cloud_display_catalog.py index d763c64fc175..4cb54f9823de 100644 --- a/tests/test_cloud_display_catalog.py +++ b/tests/test_cloud_display_catalog.py @@ -104,7 +104,7 @@ def spawn(_command, **options): return Process() def launch(_command, **_options): - return mock.Mock(stdout=f"DBUS_SESSION_BUS_ADDRESS='unix:path=/tmp/bus-{len(environments)}'\nDBUS_SESSION_BUS_PID=1\n") + return mock.Mock(stdout=f"DBUS_SESSION_BUS_ADDRESS='unix:path=/tmp/bus-{len(environments)}'; export DBUS_SESSION_BUS_ADDRESS;\nDBUS_SESSION_BUS_PID=999999; export DBUS_SESSION_BUS_PID;\n") readiness_calls = {} def readiness(number): From c92eb8a91273a086e813ed38feb997a32102da6a Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 08:28:02 -0700 Subject: [PATCH 13/64] Run guest display service as the desktop user --- Sources/Cloud/CloudGuestDisplayScript.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/Cloud/CloudGuestDisplayScript.swift b/Sources/Cloud/CloudGuestDisplayScript.swift index 6da456b3d3f7..c52e6d292923 100644 --- a/Sources/Cloud/CloudGuestDisplayScript.swift +++ b/Sources/Cloud/CloudGuestDisplayScript.swift @@ -163,16 +163,17 @@ ZSBTeXN0ZW1FeGl0KG1haW4oKSkK static func command(action: String, requestID: UUID? = nil) -> String { let argument = requestID.map { " --request-id \($0.uuidString.lowercased())" } ?? "" - return """ + let body = """ set -eu path=\"\(path)\" mkdir -p \"$HOME/.cmux\" - if [ ! -x \"$path\" ]; then printf '%s' \"\(encodedSource)\" | base64 -d > \"$path\"; chmod 700 \"$path\"; fi + if [ ! -x \"$path\" ]; then printf %s \"\(encodedSource)\" | base64 -d > \"$path\"; chmod 700 \"$path\"; fi if ! pgrep -u \"$(id -u)\" -f \"$path serve\" >/dev/null 2>&1; then nohup \"$path\" serve > \"$HOME/.cmux/display-service.log\" 2>&1 & for i in $(seq 1 100); do [ -S /run/cmux-desktop/display-control.sock ] && break; sleep 0.1; done fi \"$path\" \(action)\(argument) """ + return "runuser -u cmux -- env HOME=/home/cmux USER=cmux /bin/bash -lc '\(body)'" } } From 44e6a17b4f9bbffc9dc851ccf84820deddb1eb8e Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 08:39:36 -0700 Subject: [PATCH 14/64] Align Ghostty submodule with current main --- ghostty | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghostty b/ghostty index 35ae29b7c2bc..c5c31ce81913 160000 --- a/ghostty +++ b/ghostty @@ -1 +1 @@ -Subproject commit 35ae29b7c2bcee7c721d515d0096a9bc3f3242bb +Subproject commit c5c31ce819131ebb2deb4c4d4a75beffe4340c8d From fe6811312d322f8e09f6ecbaf2dd268cdc3f43ef Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 09:15:56 -0700 Subject: [PATCH 15/64] Invalidate terminal Cloud navigation callbacks --- Sources/Cloud/PortForward/CloudBrowserAccessState.swift | 6 ++++++ cmuxTests/CloudDesktopAccessTests.swift | 3 +++ 2 files changed, 9 insertions(+) diff --git a/Sources/Cloud/PortForward/CloudBrowserAccessState.swift b/Sources/Cloud/PortForward/CloudBrowserAccessState.swift index 4cf9da151b00..8e8e8d165d4f 100644 --- a/Sources/Cloud/PortForward/CloudBrowserAccessState.swift +++ b/Sources/Cloud/PortForward/CloudBrowserAccessState.swift @@ -230,6 +230,9 @@ final class CloudBrowserAccessState { guard let url, navigationURL != nil, owns(url) else { return } loaded = false error = message + hasCommittedNavigation = false + activeNavigationID = nil + desktopConnected = false connectionDeadline.cancel() trace("navigation_failed") } @@ -239,6 +242,9 @@ final class CloudBrowserAccessState { navigationID == nil || navigationID == activeNavigationID else { return } connectionDeadline.cancel() error = String(localized: "cloud.display.connectionCancelled", defaultValue: "The Cloud page connection was cancelled. Retry to connect.") + hasCommittedNavigation = false + activeNavigationID = nil + desktopConnected = false trace("navigation_cancelled") } diff --git a/cmuxTests/CloudDesktopAccessTests.swift b/cmuxTests/CloudDesktopAccessTests.swift index 96b21126ce95..e0cdc259d864 100644 --- a/cmuxTests/CloudDesktopAccessTests.swift +++ b/cmuxTests/CloudDesktopAccessTests.swift @@ -102,6 +102,9 @@ struct CloudDesktopAccessTests { #expect(state.error == nil) state.didCancel(navigationID: ObjectIdentifier(current)) #expect(state.error != nil && !state.showsPage) + state.didCommit(url: url, navigationID: ObjectIdentifier(current)) + state.desktopConnectionDidChange(url: url, isConnected: true) + #expect(!state.showsPage && !state.desktopConnected) state.leave() await model.retire() } From 3aeafaa952887feb28760cc6eb8cb122768a7b03 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 09:39:56 -0700 Subject: [PATCH 16/64] Keep guest display ports out of forwarded resources --- Sources/Surfaces/CmuxTuiSnapshotParser.swift | 4 +++- cmuxTests/CmuxTuiSurfaceProviderTests.swift | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/Surfaces/CmuxTuiSnapshotParser.swift b/Sources/Surfaces/CmuxTuiSnapshotParser.swift index 1774228100a3..f94df7168eb6 100644 --- a/Sources/Surfaces/CmuxTuiSnapshotParser.swift +++ b/Sources/Surfaces/CmuxTuiSnapshotParser.swift @@ -1753,7 +1753,9 @@ struct CmuxTuiSnapshotParser: Sendable { /// publish them: SSH (22), the cmux-tui daemon (1337), the VNC server /// (5901) and its noVNC front end (6901, the Desktop surface), and the /// image's internal 8080 listener. - static let internalPorts: Set = [22, 1337, 5901, 6901, 8080] + /// Guest display slots use these private RFB/noVNC ports; they are owned by + /// the display catalog and must never become generic forwarded-port rows. + static let internalPorts: Set = Set([22, 1337, 8080] + Array(5901...5916) + Array(6901...6916)) static let desktopPort = 6901 diff --git a/cmuxTests/CmuxTuiSurfaceProviderTests.swift b/cmuxTests/CmuxTuiSurfaceProviderTests.swift index f659c49174a1..481da33ec782 100644 --- a/cmuxTests/CmuxTuiSurfaceProviderTests.swift +++ b/cmuxTests/CmuxTuiSurfaceProviderTests.swift @@ -1009,7 +1009,7 @@ import Testing LISTEN 0 128 0.0.0.0:3000 0.0.0.0:* """ #expect(CmuxTuiSnapshotParser.listeningPorts(fromSocketListing: ss) == [1337, 3000, 5901]) - #expect(CmuxTuiSnapshotParser.internalPorts.isSuperset(of: [1337, 5901, 6901])) + #expect(CmuxTuiSnapshotParser.internalPorts.isSuperset(of: [1337, 5901, 5902, 5916, 6901, 6902, 6916])) #expect(CmuxTuiSnapshotParser.machineHasDesktop(image: "cmux-xfce-vnc:latest")) #expect(!CmuxTuiSnapshotParser.machineHasDesktop(image: "cmuxd-ws:tooling-20260509f")) From 89be2783c6f71d9f2b3ca11a9585eec197d04132 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 09:52:36 -0700 Subject: [PATCH 17/64] Complete additional display recovery paths --- Sources/Surfaces/CmuxTuiSurfaceProvider+Displays.swift | 3 +-- .../Surfaces/CmuxTuiSurfaceProvider+PortForward.swift | 9 +++++++-- cmuxTests/CloudDesktopAccessTests.swift | 3 ++- cmuxTests/CloudDisplayCatalogTests.swift | 2 +- tests/fixtures/cmux-display | 2 ++ 5 files changed, 13 insertions(+), 6 deletions(-) diff --git a/Sources/Surfaces/CmuxTuiSurfaceProvider+Displays.swift b/Sources/Surfaces/CmuxTuiSurfaceProvider+Displays.swift index 8abe8d34e3ee..e3a3459d3dad 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProvider+Displays.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProvider+Displays.swift @@ -29,12 +29,11 @@ extension CmuxTuiSurfaceProvider { func createDisplay() async throws -> SurfaceResource { guard supportsDisplayCreation else { throw SurfaceCatalogError.unsupported(CloudGuestDisplaySnapshot.unavailableMessage) } let generation = currentLifecycleGeneration - let refresh = refreshGeneration defer { if isCurrentLifecycleGeneration(generation), isRegisteredInCatalog() { publishDisplays() } } let snapshot = try await displayCoordinator.create() - guard isCurrentRefresh(lifecycle: generation, refresh: refresh) else { throw CancellationError() } + guard isCurrentLifecycleGeneration(generation), isRegisteredInCatalog() else { throw CancellationError() } guard let display = snapshot.displays.first(where: { $0.id == snapshot.created }) else { throw SurfaceCatalogError.unsupported(CloudGuestDisplaySnapshot.unavailableMessage) } diff --git a/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift b/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift index 45220abfcdfe..f553d3594ca7 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift @@ -134,8 +134,13 @@ extension CmuxTuiSurfaceProvider { materializedPanels.insert(projection.panelID) switch CloudPortRoutePlan.plan(resource: resource, privateAddress: info.privateAddress) { case .privateDirect(let raw): - if let url = URL(string: raw) { configureBrowser(browser, url: url, resourceID: resource.id) } - case .unsupported(let message): browser.cloudAccess.showUnavailable(message) + guard let url = URL(string: raw) else { continue } + configureBrowser(browser, url: url, resourceID: resource.id) + materializedPanels.insert(projection.panelID) + case .unsupported: + // Keep the placeholder eligible for a later explicit display + // discovery; its target may be supplied by the guest catalog. + continue } } } diff --git a/cmuxTests/CloudDesktopAccessTests.swift b/cmuxTests/CloudDesktopAccessTests.swift index e0cdc259d864..feecb7b25a9d 100644 --- a/cmuxTests/CloudDesktopAccessTests.swift +++ b/cmuxTests/CloudDesktopAccessTests.swift @@ -115,7 +115,8 @@ struct CloudDesktopAccessTests { wake: {}, startForward: { _ in UInt16(40_000 + port) }, stopForward: {}, route: .loopback) let state = CloudBrowserAccessState() let remote = try #require(URL(string: "http://10.0.0.7:\(port)/vnc.html")) - state.configure(model: model, url: remote) + state.configure(model: model, url: remote, + resourceID: SurfaceResourceID(machine: .cloud("display-test"), kind: .display, key: "display:\(port == 6901 ? 1 : 2)")) model.connect() #expect(await wait { model.isReady }) let url = try #require(state.nextURL()) diff --git a/cmuxTests/CloudDisplayCatalogTests.swift b/cmuxTests/CloudDisplayCatalogTests.swift index 81955602b213..624e075be2a9 100644 --- a/cmuxTests/CloudDisplayCatalogTests.swift +++ b/cmuxTests/CloudDisplayCatalogTests.swift @@ -55,7 +55,7 @@ struct CloudDisplayCatalogTests { response.resolve(true) do { _ = try await operation.value; Issue.record("Retired creation succeeded") } catch {} #expect(!service.canCreate) - #expect(service.snapshot?.displays.count == 1) + #expect(service.snapshot == nil) } @Test("Unsupported images never expose a fake creation action") diff --git a/tests/fixtures/cmux-display b/tests/fixtures/cmux-display index 10509e01b758..c5408b5a539f 100755 --- a/tests/fixtures/cmux-display +++ b/tests/fixtures/cmux-display @@ -228,6 +228,8 @@ class DisplayService: runtime = self.runtime / str(number) runtime.mkdir(mode=0o700, parents=True, exist_ok=True) environment = dict(os.environ, DISPLAY=f":{number}", CMUX_DESKTOP_RUNTIME_DIR=str(runtime)) + environment["CHROME_CONFIG_HOME"] = str(runtime / "chrome-config") + environment["XDG_CONFIG_HOME"] = str(runtime / "xdg-config") for key in ("NOTIFY_SOCKET", "DBUS_SESSION_BUS_ADDRESS", "DBUS_SESSION_BUS_PID", "AT_SPI_BUS_ADDRESS", "AT_SPI_BUS"): environment.pop(key, None) From 220c02722287e0ac1ed1ac4ee3bf9303450a1524 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 10:04:46 -0700 Subject: [PATCH 18/64] Synchronize guest profile and slow-route readiness --- Sources/Cloud/CloudGuestDisplayScript.swift | 113 +++++++++--------- .../PortForward/CloudBrowserAccessState.swift | 4 + 2 files changed, 61 insertions(+), 56 deletions(-) diff --git a/Sources/Cloud/CloudGuestDisplayScript.swift b/Sources/Cloud/CloudGuestDisplayScript.swift index c52e6d292923..939929b88b31 100644 --- a/Sources/Cloud/CloudGuestDisplayScript.swift +++ b/Sources/Cloud/CloudGuestDisplayScript.swift @@ -102,62 +102,63 @@ cnQobm92bmNfcG9ydCwgdGltZW91dD0xMCk6CiAgICAgICAgICAgIHJhaXNlIFJ1bnRpbWVFcnJvcigi ICAgICAgIChydW50aW1lIC8gImVudiIpLndyaXRlX3RleHQoZiJleHBvcnQgRElTUExBWT06e251bWJlcn1cbiIpCgogICAgZGVmIHN1cGVydmlzZShzZWxm LCBudW1iZXIsIGZpbmlzaGVkKToKICAgICAgICBydW50aW1lID0gc2VsZi5ydW50aW1lIC8gc3RyKG51bWJlcikKICAgICAgICBydW50aW1lLm1rZGlyKG1v ZGU9MG83MDAsIHBhcmVudHM9VHJ1ZSwgZXhpc3Rfb2s9VHJ1ZSkKICAgICAgICBlbnZpcm9ubWVudCA9IGRpY3Qob3MuZW52aXJvbiwgRElTUExBWT1mIjp7 -bnVtYmVyfSIsIENNVVhfREVTS1RPUF9SVU5USU1FX0RJUj1zdHIocnVudGltZSkpCiAgICAgICAgZm9yIGtleSBpbiAoIk5PVElGWV9TT0NLRVQiLCAiREJV -U19TRVNTSU9OX0JVU19BRERSRVNTIiwgIkRCVVNfU0VTU0lPTl9CVVNfUElEIiwKICAgICAgICAgICAgICAgICAgICAiQVRfU1BJX0JVU19BRERSRVNTIiwg -IkFUX1NQSV9CVVMiKToKICAgICAgICAgICAgZW52aXJvbm1lbnQucG9wKGtleSwgTm9uZSkKICAgICAgICB3aGlsZSBub3Qgc2VsZi5zaHV0ZG93bi5pc19z -ZXQoKToKICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgIyBUaGUgc3VwZXJ2aXNvciBpcyBhIGhlYWx0aCBjaGVjaywgbm90IGEgdGltZXItZHJp -dmVuIHJlc3RhcnQuCiAgICAgICAgICAgICAgICAjIFByZXNlcnZlIGFuIGFjdGl2ZSBSRkIvbm9WTkMgc2Vzc2lvbiB1bnRpbCBpdHMgb3duZWQgZW5kcG9p -bnRzIGZhaWwuCiAgICAgICAgICAgICAgICBpZiBub3Qgc2VsZi5wcm9jZXNzZXMuZ2V0KG51bWJlcikgb3Igbm90IHJlYWR5KG51bWJlcik6CiAgICAgICAg -ICAgICAgICAgICAgc2VsZi5zdGFydF9jb21wb25lbnRzKG51bWJlciwgZW52aXJvbm1lbnQsIHJ1bnRpbWUpCiAgICAgICAgICAgICAgICBzdGF0ZSA9ICJy -dW5uaW5nIiBpZiByZWFkeShudW1iZXIpIGVsc2UgInVuYXZhaWxhYmxlIgogICAgICAgICAgICBleGNlcHQgKE9TRXJyb3IsIFJ1bnRpbWVFcnJvciwgc3Vi -cHJvY2Vzcy5TdWJwcm9jZXNzRXJyb3IpOgogICAgICAgICAgICAgICAgc2VsZi50ZXJtaW5hdGUobnVtYmVyKQogICAgICAgICAgICAgICAgc3RhdGUgPSAi -dW5hdmFpbGFibGUiCiAgICAgICAgICAgIHdpdGggc2VsZi5sb2NrOgogICAgICAgICAgICAgICAgc2VsZi5zdGF0ZXNbbnVtYmVyXSA9IHN0YXRlCiAgICAg -ICAgICAgIGZpbmlzaGVkLnNldCgpCiAgICAgICAgICAgIHNlbGYuc2h1dGRvd24ud2FpdCgzMCkKICAgICAgICBzZWxmLnRlcm1pbmF0ZShudW1iZXIpCgog -ICAgZGVmIHNuYXBzaG90KHNlbGYpOgogICAgICAgIHdpdGggc2VsZi5sb2NrOgogICAgICAgICAgICByZXR1cm4geyJ2ZXJzaW9uIjogMSwgImNhbkNyZWF0 -ZSI6IGxlbihzZWxmLmpvYnMpIDwgTUFYX0RJU1BMQVlTIC0gMSwKICAgICAgICAgICAgICAgICAgICAiZGlzcGxheXMiOiBbZGVzY3JpcHRvcigxLCAicnVu -bmluZyIgaWYgcmVhZHkoMSkgZWxzZSAidW5hdmFpbGFibGUiKV0KICAgICAgICAgICAgICAgICAgICArIFtkZXNjcmlwdG9yKG4sIHNlbGYuc3RhdGVzLmdl -dChuLCAibGF1bmNoaW5nIikpIGZvciBuIGluIHNlbGYuY2F0YWxvZy5udW1iZXJzKCldfQoKICAgIGRlZiBoYW5kbGUoc2VsZiwgcmVxdWVzdCk6CiAgICAg -ICAgaWYgcmVxdWVzdC5nZXQoImFjdGlvbiIpID09ICJsaXN0IjoKICAgICAgICAgICAgcmV0dXJuIHNlbGYuc25hcHNob3QoKQogICAgICAgIGlmIHJlcXVl -c3QuZ2V0KCJhY3Rpb24iKSAhPSAiY3JlYXRlIjoKICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJvcigidW5zdXBwb3J0ZWQgZGlzcGxheSBhY3Rpb24iKQog -ICAgICAgIG51bWJlciA9IHNlbGYuY2F0YWxvZy5hbGxvY2F0ZShyZXF1ZXN0LmdldCgicmVxdWVzdCIpKQogICAgICAgIHNlbGYuc3RhcnQobnVtYmVyKS53 -YWl0KDU1KQogICAgICAgIHJlc3VsdCA9IHNlbGYuc25hcHNob3QoKQogICAgICAgIHJlc3VsdFsiY3JlYXRlZCJdID0gZiJkaXNwbGF5OntudW1iZXJ9Igog -ICAgICAgIGlmIHNlbGYuc3RhdGVzLmdldChudW1iZXIpICE9ICJydW5uaW5nIjoKICAgICAgICAgICAgcmVzdWx0WyJlcnJvciJdID0gImRpc3BsYXlfc3Rh -cnRfZmFpbGVkIgogICAgICAgIHJldHVybiByZXN1bHQKCgpkZWYgc2VydmUocnVudGltZSk6CiAgICBydW50aW1lID0gUGF0aChydW50aW1lKQogICAgcnVu -dGltZS5ta2Rpcihtb2RlPTBvNzAwLCBwYXJlbnRzPVRydWUsIGV4aXN0X29rPVRydWUpCiAgICB3aXRoIChydW50aW1lIC8gImRpc3BsYXktY29udHJvbC5s -b2NrIikub3BlbigidyIpIGFzIGxvY2s6CiAgICAgICAgdHJ5OgogICAgICAgICAgICBmY250bC5mbG9jayhsb2NrLCBmY250bC5MT0NLX0VYIHwgZmNudGwu -TE9DS19OQikKICAgICAgICBleGNlcHQgQmxvY2tpbmdJT0Vycm9yOgogICAgICAgICAgICByZXR1cm4KICAgICAgICBpZiBub3QgYWxsKHNodXRpbC53aGlj -aChuYW1lKSBmb3IgbmFtZSBpbiAoIndlYnNvY2tpZnkiLCAib3BlbmJveCIsICJkYnVzLWxhdW5jaCIpKToKICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJv -cigiZGlzcGxheSBydW50aW1lIHVuYXZhaWxhYmxlIikKICAgICAgICBpZiBub3QgKHNodXRpbC53aGljaCgiWHZuYyIpIG9yIHNodXRpbC53aGljaCgiWHRp -Z2Vydm5jIikpOgogICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJkaXNwbGF5IHJ1bnRpbWUgdW5hdmFpbGFibGUiKQogICAgICAgIHNlcnZpY2UgPSBE -aXNwbGF5U2VydmljZShEaXNwbGF5Q2F0YWxvZyhQYXRoLmhvbWUoKSAvICIuY211eCIgLyAiZGlzcGxheXMiKSwgcnVudGltZSAvICJkaXNwbGF5cyIpCiAg -ICAgICAgZm9yIG51bWJlciBpbiBzZXJ2aWNlLmNhdGFsb2cubnVtYmVycygpOgogICAgICAgICAgICBzZXJ2aWNlLnN0YXJ0KG51bWJlcikKICAgICAgICBw -YXRoID0gcnVudGltZSAvICJkaXNwbGF5LWNvbnRyb2wuc29jayIKICAgICAgICBpZiBwYXRoLmV4aXN0cygpOgogICAgICAgICAgICBwYXRoLnVubGluaygp -CgogICAgICAgIGNsYXNzIEhhbmRsZXIoc29ja2V0c2VydmVyLlN0cmVhbVJlcXVlc3RIYW5kbGVyKToKICAgICAgICAgICAgZGVmIGhhbmRsZShzZWxmKToK -ICAgICAgICAgICAgICAgIHNlbGYuY29ubmVjdGlvbi5zZXR0aW1lb3V0KDYwKQogICAgICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgIHJh -dyA9IHNlbGYucmZpbGUucmVhZGxpbmUoNDA5NykKICAgICAgICAgICAgICAgICAgICBpZiBsZW4ocmF3KSA+IDQwOTY6CiAgICAgICAgICAgICAgICAgICAg -ICAgIHJhaXNlIFZhbHVlRXJyb3IoImRpc3BsYXkgcmVxdWVzdCB0b28gbGFyZ2UiKQogICAgICAgICAgICAgICAgICAgIHJlc3VsdCA9IHNlcnZpY2UuaGFu -ZGxlKGpzb24ubG9hZHMocmF3KSkKICAgICAgICAgICAgICAgIGV4Y2VwdCAoVmFsdWVFcnJvciwgS2V5RXJyb3IsIFR5cGVFcnJvciwgT1NFcnJvcik6CiAg -ICAgICAgICAgICAgICAgICAgcmVzdWx0ID0geyJlcnJvciI6ICJkaXNwbGF5X3JlcXVlc3RfZmFpbGVkIn0KICAgICAgICAgICAgICAgIHRyeToKICAgICAg -ICAgICAgICAgICAgICBzZWxmLndmaWxlLndyaXRlKGpzb24uZHVtcHMocmVzdWx0KS5lbmNvZGUoKSArIGIiXG4iKQogICAgICAgICAgICAgICAgZXhjZXB0 -IE9TRXJyb3I6CiAgICAgICAgICAgICAgICAgICAgcGFzcyAgIyBUaGUgcGVyc2lzdGVkIHJlY2VpcHQgcmVtYWlucyByZXBsYXlhYmxlIGFmdGVyIGRpc2Nv -bm5lY3QuCgogICAgICAgIGNsYXNzIFNlcnZlcihzb2NrZXRzZXJ2ZXIuVGhyZWFkaW5nVW5peFN0cmVhbVNlcnZlcik6CiAgICAgICAgICAgIGRhZW1vbl90 -aHJlYWRzID0gVHJ1ZQoKICAgICAgICBwcmV2aW91c191bWFzayA9IG9zLnVtYXNrKDBvMDc3KQogICAgICAgIHRyeToKICAgICAgICAgICAgd2l0aCBTZXJ2 -ZXIoc3RyKHBhdGgpLCBIYW5kbGVyKSBhcyBzZXJ2ZXI6CiAgICAgICAgICAgICAgICBvcy51bWFzayhwcmV2aW91c191bWFzaykKICAgICAgICAgICAgICAg -IHNlcnZlci5zZXJ2ZV9mb3JldmVyKCkKICAgICAgICBmaW5hbGx5OgogICAgICAgICAgICBvcy51bWFzayhwcmV2aW91c191bWFzaykKICAgICAgICAgICAg -c2VydmljZS5zaHV0ZG93bi5zZXQoKQogICAgICAgICAgICBwYXRoLnVubGluayhtaXNzaW5nX29rPVRydWUpCgoKZGVmIG1haW4oKToKICAgIHBhcnNlciA9 -IGFyZ3BhcnNlLkFyZ3VtZW50UGFyc2VyKGRlc2NyaXB0aW9uPV9fZG9jX18pCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50KCJhY3Rpb24iLCBjaG9pY2VzPVsi -bGlzdCIsICJjcmVhdGUiLCAic2VydmUiXSkKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoIi0tcmVxdWVzdC1pZCIpCiAgICBhcmdzID0gcGFyc2VyLnBhcnNl -X2FyZ3MoKQogICAgcnVudGltZSA9IG9zLmVudmlyb24uZ2V0KCJDTVVYX0RFU0tUT1BfUlVOVElNRV9ESVIiLCAiL3J1bi9jbXV4LWRlc2t0b3AiKQogICAg -aWYgYXJncy5hY3Rpb24gPT0gInNlcnZlIjoKICAgICAgICBzZXJ2ZShydW50aW1lKQogICAgICAgIHJldHVybiAwCiAgICBpZiBhcmdzLmFjdGlvbiA9PSAi -Y3JlYXRlIiBhbmQgbm90IGFyZ3MucmVxdWVzdF9pZDoKICAgICAgICBwYXJzZXIuZXJyb3IoImNyZWF0ZSByZXF1aXJlcyAtLXJlcXVlc3QtaWQgKHJldGFp -biBpdCB3aGVuIHJldHJ5aW5nKSIpCiAgICB0cnk6CiAgICAgICAgd2l0aCBzb2NrZXQuc29ja2V0KHNvY2tldC5BRl9VTklYLCBzb2NrZXQuU09DS19TVFJF -QU0pIGFzIGNsaWVudDoKICAgICAgICAgICAgY2xpZW50LnNldHRpbWVvdXQoNjApCiAgICAgICAgICAgIGNsaWVudC5jb25uZWN0KHN0cihQYXRoKHJ1bnRp -bWUpIC8gImRpc3BsYXktY29udHJvbC5zb2NrIikpCiAgICAgICAgICAgIGNsaWVudC5zZW5kYWxsKGpzb24uZHVtcHMoeyJhY3Rpb24iOiBhcmdzLmFjdGlv -biwgInJlcXVlc3QiOiBhcmdzLnJlcXVlc3RfaWR9KS5lbmNvZGUoKSArIGIiXG4iKQogICAgICAgICAgICB3aXRoIGNsaWVudC5tYWtlZmlsZSgicmIiKSBh -cyBzdHJlYW06CiAgICAgICAgICAgICAgICByZXN1bHQgPSBqc29uLmxvYWRzKHN0cmVhbS5yZWFkbGluZSg2NTUzNikpCiAgICBleGNlcHQgKE9TRXJyb3Is -IFZhbHVlRXJyb3IpOgogICAgICAgIHJlc3VsdCA9IHsiZXJyb3IiOiAiZGlzcGxheV9ydW50aW1lX3VuYXZhaWxhYmxlIn0KICAgIHByaW50KGpzb24uZHVt -cHMocmVzdWx0KSkKICAgIHJldHVybiAxIGlmICJlcnJvciIgaW4gcmVzdWx0IGVsc2UgMAoKCmlmIF9fbmFtZV9fID09ICJfX21haW5fXyI6CiAgICByYWlz -ZSBTeXN0ZW1FeGl0KG1haW4oKSkK +bnVtYmVyfSIsIENNVVhfREVTS1RPUF9SVU5USU1FX0RJUj1zdHIocnVudGltZSkpCiAgICAgICAgZW52aXJvbm1lbnRbIkNIUk9NRV9DT05GSUdfSE9NRSJd +ID0gc3RyKHJ1bnRpbWUgLyAiY2hyb21lLWNvbmZpZyIpCiAgICAgICAgZW52aXJvbm1lbnRbIlhER19DT05GSUdfSE9NRSJdID0gc3RyKHJ1bnRpbWUgLyAi +eGRnLWNvbmZpZyIpCiAgICAgICAgZm9yIGtleSBpbiAoIk5PVElGWV9TT0NLRVQiLCAiREJVU19TRVNTSU9OX0JVU19BRERSRVNTIiwgIkRCVVNfU0VTU0lP +Tl9CVVNfUElEIiwKICAgICAgICAgICAgICAgICAgICAiQVRfU1BJX0JVU19BRERSRVNTIiwgIkFUX1NQSV9CVVMiKToKICAgICAgICAgICAgZW52aXJvbm1l +bnQucG9wKGtleSwgTm9uZSkKICAgICAgICB3aGlsZSBub3Qgc2VsZi5zaHV0ZG93bi5pc19zZXQoKToKICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAg +ICAgIyBUaGUgc3VwZXJ2aXNvciBpcyBhIGhlYWx0aCBjaGVjaywgbm90IGEgdGltZXItZHJpdmVuIHJlc3RhcnQuCiAgICAgICAgICAgICAgICAjIFByZXNl +cnZlIGFuIGFjdGl2ZSBSRkIvbm9WTkMgc2Vzc2lvbiB1bnRpbCBpdHMgb3duZWQgZW5kcG9pbnRzIGZhaWwuCiAgICAgICAgICAgICAgICBpZiBub3Qgc2Vs +Zi5wcm9jZXNzZXMuZ2V0KG51bWJlcikgb3Igbm90IHJlYWR5KG51bWJlcik6CiAgICAgICAgICAgICAgICAgICAgc2VsZi5zdGFydF9jb21wb25lbnRzKG51 +bWJlciwgZW52aXJvbm1lbnQsIHJ1bnRpbWUpCiAgICAgICAgICAgICAgICBzdGF0ZSA9ICJydW5uaW5nIiBpZiByZWFkeShudW1iZXIpIGVsc2UgInVuYXZh +aWxhYmxlIgogICAgICAgICAgICBleGNlcHQgKE9TRXJyb3IsIFJ1bnRpbWVFcnJvciwgc3VicHJvY2Vzcy5TdWJwcm9jZXNzRXJyb3IpOgogICAgICAgICAg +ICAgICAgc2VsZi50ZXJtaW5hdGUobnVtYmVyKQogICAgICAgICAgICAgICAgc3RhdGUgPSAidW5hdmFpbGFibGUiCiAgICAgICAgICAgIHdpdGggc2VsZi5s +b2NrOgogICAgICAgICAgICAgICAgc2VsZi5zdGF0ZXNbbnVtYmVyXSA9IHN0YXRlCiAgICAgICAgICAgIGZpbmlzaGVkLnNldCgpCiAgICAgICAgICAgIHNl +bGYuc2h1dGRvd24ud2FpdCgzMCkKICAgICAgICBzZWxmLnRlcm1pbmF0ZShudW1iZXIpCgogICAgZGVmIHNuYXBzaG90KHNlbGYpOgogICAgICAgIHdpdGgg +c2VsZi5sb2NrOgogICAgICAgICAgICByZXR1cm4geyJ2ZXJzaW9uIjogMSwgImNhbkNyZWF0ZSI6IGxlbihzZWxmLmpvYnMpIDwgTUFYX0RJU1BMQVlTIC0g +MSwKICAgICAgICAgICAgICAgICAgICAiZGlzcGxheXMiOiBbZGVzY3JpcHRvcigxLCAicnVubmluZyIgaWYgcmVhZHkoMSkgZWxzZSAidW5hdmFpbGFibGUi +KV0KICAgICAgICAgICAgICAgICAgICArIFtkZXNjcmlwdG9yKG4sIHNlbGYuc3RhdGVzLmdldChuLCAibGF1bmNoaW5nIikpIGZvciBuIGluIHNlbGYuY2F0 +YWxvZy5udW1iZXJzKCldfQoKICAgIGRlZiBoYW5kbGUoc2VsZiwgcmVxdWVzdCk6CiAgICAgICAgaWYgcmVxdWVzdC5nZXQoImFjdGlvbiIpID09ICJsaXN0 +IjoKICAgICAgICAgICAgcmV0dXJuIHNlbGYuc25hcHNob3QoKQogICAgICAgIGlmIHJlcXVlc3QuZ2V0KCJhY3Rpb24iKSAhPSAiY3JlYXRlIjoKICAgICAg +ICAgICAgcmFpc2UgVmFsdWVFcnJvcigidW5zdXBwb3J0ZWQgZGlzcGxheSBhY3Rpb24iKQogICAgICAgIG51bWJlciA9IHNlbGYuY2F0YWxvZy5hbGxvY2F0 +ZShyZXF1ZXN0LmdldCgicmVxdWVzdCIpKQogICAgICAgIHNlbGYuc3RhcnQobnVtYmVyKS53YWl0KDU1KQogICAgICAgIHJlc3VsdCA9IHNlbGYuc25hcHNo +b3QoKQogICAgICAgIHJlc3VsdFsiY3JlYXRlZCJdID0gZiJkaXNwbGF5OntudW1iZXJ9IgogICAgICAgIGlmIHNlbGYuc3RhdGVzLmdldChudW1iZXIpICE9 +ICJydW5uaW5nIjoKICAgICAgICAgICAgcmVzdWx0WyJlcnJvciJdID0gImRpc3BsYXlfc3RhcnRfZmFpbGVkIgogICAgICAgIHJldHVybiByZXN1bHQKCgpk +ZWYgc2VydmUocnVudGltZSk6CiAgICBydW50aW1lID0gUGF0aChydW50aW1lKQogICAgcnVudGltZS5ta2Rpcihtb2RlPTBvNzAwLCBwYXJlbnRzPVRydWUs +IGV4aXN0X29rPVRydWUpCiAgICB3aXRoIChydW50aW1lIC8gImRpc3BsYXktY29udHJvbC5sb2NrIikub3BlbigidyIpIGFzIGxvY2s6CiAgICAgICAgdHJ5 +OgogICAgICAgICAgICBmY250bC5mbG9jayhsb2NrLCBmY250bC5MT0NLX0VYIHwgZmNudGwuTE9DS19OQikKICAgICAgICBleGNlcHQgQmxvY2tpbmdJT0Vy +cm9yOgogICAgICAgICAgICByZXR1cm4KICAgICAgICBpZiBub3QgYWxsKHNodXRpbC53aGljaChuYW1lKSBmb3IgbmFtZSBpbiAoIndlYnNvY2tpZnkiLCAi +b3BlbmJveCIsICJkYnVzLWxhdW5jaCIpKToKICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJvcigiZGlzcGxheSBydW50aW1lIHVuYXZhaWxhYmxlIikKICAg +ICAgICBpZiBub3QgKHNodXRpbC53aGljaCgiWHZuYyIpIG9yIHNodXRpbC53aGljaCgiWHRpZ2Vydm5jIikpOgogICAgICAgICAgICByYWlzZSBWYWx1ZUVy +cm9yKCJkaXNwbGF5IHJ1bnRpbWUgdW5hdmFpbGFibGUiKQogICAgICAgIHNlcnZpY2UgPSBEaXNwbGF5U2VydmljZShEaXNwbGF5Q2F0YWxvZyhQYXRoLmhv +bWUoKSAvICIuY211eCIgLyAiZGlzcGxheXMiKSwgcnVudGltZSAvICJkaXNwbGF5cyIpCiAgICAgICAgZm9yIG51bWJlciBpbiBzZXJ2aWNlLmNhdGFsb2cu +bnVtYmVycygpOgogICAgICAgICAgICBzZXJ2aWNlLnN0YXJ0KG51bWJlcikKICAgICAgICBwYXRoID0gcnVudGltZSAvICJkaXNwbGF5LWNvbnRyb2wuc29j +ayIKICAgICAgICBpZiBwYXRoLmV4aXN0cygpOgogICAgICAgICAgICBwYXRoLnVubGluaygpCgogICAgICAgIGNsYXNzIEhhbmRsZXIoc29ja2V0c2VydmVy +LlN0cmVhbVJlcXVlc3RIYW5kbGVyKToKICAgICAgICAgICAgZGVmIGhhbmRsZShzZWxmKToKICAgICAgICAgICAgICAgIHNlbGYuY29ubmVjdGlvbi5zZXR0 +aW1lb3V0KDYwKQogICAgICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgIHJhdyA9IHNlbGYucmZpbGUucmVhZGxpbmUoNDA5NykKICAgICAg +ICAgICAgICAgICAgICBpZiBsZW4ocmF3KSA+IDQwOTY6CiAgICAgICAgICAgICAgICAgICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoImRpc3BsYXkgcmVxdWVz +dCB0b28gbGFyZ2UiKQogICAgICAgICAgICAgICAgICAgIHJlc3VsdCA9IHNlcnZpY2UuaGFuZGxlKGpzb24ubG9hZHMocmF3KSkKICAgICAgICAgICAgICAg +IGV4Y2VwdCAoVmFsdWVFcnJvciwgS2V5RXJyb3IsIFR5cGVFcnJvciwgT1NFcnJvcik6CiAgICAgICAgICAgICAgICAgICAgcmVzdWx0ID0geyJlcnJvciI6 +ICJkaXNwbGF5X3JlcXVlc3RfZmFpbGVkIn0KICAgICAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgICAgICBzZWxmLndmaWxlLndyaXRlKGpzb24u +ZHVtcHMocmVzdWx0KS5lbmNvZGUoKSArIGIiXG4iKQogICAgICAgICAgICAgICAgZXhjZXB0IE9TRXJyb3I6CiAgICAgICAgICAgICAgICAgICAgcGFzcyAg +IyBUaGUgcGVyc2lzdGVkIHJlY2VpcHQgcmVtYWlucyByZXBsYXlhYmxlIGFmdGVyIGRpc2Nvbm5lY3QuCgogICAgICAgIGNsYXNzIFNlcnZlcihzb2NrZXRz +ZXJ2ZXIuVGhyZWFkaW5nVW5peFN0cmVhbVNlcnZlcik6CiAgICAgICAgICAgIGRhZW1vbl90aHJlYWRzID0gVHJ1ZQoKICAgICAgICBwcmV2aW91c191bWFz +ayA9IG9zLnVtYXNrKDBvMDc3KQogICAgICAgIHRyeToKICAgICAgICAgICAgd2l0aCBTZXJ2ZXIoc3RyKHBhdGgpLCBIYW5kbGVyKSBhcyBzZXJ2ZXI6CiAg +ICAgICAgICAgICAgICBvcy51bWFzayhwcmV2aW91c191bWFzaykKICAgICAgICAgICAgICAgIHNlcnZlci5zZXJ2ZV9mb3JldmVyKCkKICAgICAgICBmaW5h +bGx5OgogICAgICAgICAgICBvcy51bWFzayhwcmV2aW91c191bWFzaykKICAgICAgICAgICAgc2VydmljZS5zaHV0ZG93bi5zZXQoKQogICAgICAgICAgICBw +YXRoLnVubGluayhtaXNzaW5nX29rPVRydWUpCgoKZGVmIG1haW4oKToKICAgIHBhcnNlciA9IGFyZ3BhcnNlLkFyZ3VtZW50UGFyc2VyKGRlc2NyaXB0aW9u +PV9fZG9jX18pCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50KCJhY3Rpb24iLCBjaG9pY2VzPVsibGlzdCIsICJjcmVhdGUiLCAic2VydmUiXSkKICAgIHBhcnNl +ci5hZGRfYXJndW1lbnQoIi0tcmVxdWVzdC1pZCIpCiAgICBhcmdzID0gcGFyc2VyLnBhcnNlX2FyZ3MoKQogICAgcnVudGltZSA9IG9zLmVudmlyb24uZ2V0 +KCJDTVVYX0RFU0tUT1BfUlVOVElNRV9ESVIiLCAiL3J1bi9jbXV4LWRlc2t0b3AiKQogICAgaWYgYXJncy5hY3Rpb24gPT0gInNlcnZlIjoKICAgICAgICBz +ZXJ2ZShydW50aW1lKQogICAgICAgIHJldHVybiAwCiAgICBpZiBhcmdzLmFjdGlvbiA9PSAiY3JlYXRlIiBhbmQgbm90IGFyZ3MucmVxdWVzdF9pZDoKICAg +ICAgICBwYXJzZXIuZXJyb3IoImNyZWF0ZSByZXF1aXJlcyAtLXJlcXVlc3QtaWQgKHJldGFpbiBpdCB3aGVuIHJldHJ5aW5nKSIpCiAgICB0cnk6CiAgICAg +ICAgd2l0aCBzb2NrZXQuc29ja2V0KHNvY2tldC5BRl9VTklYLCBzb2NrZXQuU09DS19TVFJFQU0pIGFzIGNsaWVudDoKICAgICAgICAgICAgY2xpZW50LnNl +dHRpbWVvdXQoNjApCiAgICAgICAgICAgIGNsaWVudC5jb25uZWN0KHN0cihQYXRoKHJ1bnRpbWUpIC8gImRpc3BsYXktY29udHJvbC5zb2NrIikpCiAgICAg +ICAgICAgIGNsaWVudC5zZW5kYWxsKGpzb24uZHVtcHMoeyJhY3Rpb24iOiBhcmdzLmFjdGlvbiwgInJlcXVlc3QiOiBhcmdzLnJlcXVlc3RfaWR9KS5lbmNv +ZGUoKSArIGIiXG4iKQogICAgICAgICAgICB3aXRoIGNsaWVudC5tYWtlZmlsZSgicmIiKSBhcyBzdHJlYW06CiAgICAgICAgICAgICAgICByZXN1bHQgPSBq +c29uLmxvYWRzKHN0cmVhbS5yZWFkbGluZSg2NTUzNikpCiAgICBleGNlcHQgKE9TRXJyb3IsIFZhbHVlRXJyb3IpOgogICAgICAgIHJlc3VsdCA9IHsiZXJy +b3IiOiAiZGlzcGxheV9ydW50aW1lX3VuYXZhaWxhYmxlIn0KICAgIHByaW50KGpzb24uZHVtcHMocmVzdWx0KSkKICAgIHJldHVybiAxIGlmICJlcnJvciIg +aW4gcmVzdWx0IGVsc2UgMAoKCmlmIF9fbmFtZV9fID09ICJfX21haW5fXyI6CiAgICByYWlzZSBTeXN0ZW1FeGl0KG1haW4oKSkK """ private static let path = "$HOME/.cmux/cmux-display" diff --git a/Sources/Cloud/PortForward/CloudBrowserAccessState.swift b/Sources/Cloud/PortForward/CloudBrowserAccessState.swift index 8e8e8d165d4f..e6ed323f71ff 100644 --- a/Sources/Cloud/PortForward/CloudBrowserAccessState.swift +++ b/Sources/Cloud/PortForward/CloudBrowserAccessState.swift @@ -185,6 +185,10 @@ final class CloudBrowserAccessState { navigationURL = url hasCommittedNavigation = false error = nil + desktopFailure = nil + dismissedFailure = nil + connectionDeadline.cancel() + startDeadline() loaded = false trace("route_ready") return url From d7a3aaf3a8a7bd7786e4c9c00670c85fd1a66beb Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 10:15:23 -0700 Subject: [PATCH 19/64] Finish guest display startup and restore routing --- Sources/Cloud/CloudGuestDisplayScript.swift | 154 +++++++++--------- .../CmuxTuiSurfaceProvider+PortForward.swift | 8 +- tests/fixtures/cmux-display | 4 +- 3 files changed, 88 insertions(+), 78 deletions(-) diff --git a/Sources/Cloud/CloudGuestDisplayScript.swift b/Sources/Cloud/CloudGuestDisplayScript.swift index 939929b88b31..470b68d47c90 100644 --- a/Sources/Cloud/CloudGuestDisplayScript.swift +++ b/Sources/Cloud/CloudGuestDisplayScript.swift @@ -83,82 +83,84 @@ ZW52aXJvbm1lbnQsIHN0ZGluPXN1YnByb2Nlc3MuREVWTlVMTCwKICAgICAgICAgICAgc3Rkb3V0PXN1 b2Nlc3MuREVWTlVMTCkpCiAgICAgICAgc2VsZi5wcm9jZXNzZXNbbnVtYmVyXSA9IHByb2Nlc3NlcwogICAgICAgIGlmIG5vdCBzZWxmLndhaXRfZm9yX3Bv cnQocmZiX3BvcnQpOgogICAgICAgICAgICByYWlzZSBSdW50aW1lRXJyb3IoIlh2bmMgZGlkIG5vdCBiZWNvbWUgcmVhZHkiKQogICAgICAgIGJ1cyA9IHN1 YnByb2Nlc3MucnVuKFsiZGJ1cy1sYXVuY2giLCAiLS1zaC1zeW50YXgiXSwgZW52PWVudmlyb25tZW50LAogICAgICAgICAgICAgICAgICAgICAgICAgICAg -IGNhcHR1cmVfb3V0cHV0PVRydWUsIHRleHQ9VHJ1ZSwgY2hlY2s9VHJ1ZSkuc3Rkb3V0CiAgICAgICAgZm9yIGxpbmUgaW4gYnVzLnNwbGl0bGluZXMoKToK -ICAgICAgICAgICAgaWYgIj0iIG5vdCBpbiBsaW5lOgogICAgICAgICAgICAgICAgY29udGludWUKICAgICAgICAgICAga2V5LCB2YWx1ZSA9IGxpbmUuc3Bs -aXQoIj0iLCAxKQogICAgICAgICAgICBlbnZpcm9ubWVudFtrZXldID0gdmFsdWUuc3RyaXAoKS5yc3RyaXAoIjsiKS5zdHJpcCgpLnN0cmlwKCInIikKICAg -ICAgICB0cnk6CiAgICAgICAgICAgIGRidXNfcGlkID0gaW50KGVudmlyb25tZW50LmdldCgiREJVU19TRVNTSU9OX0JVU19QSUQiLCAiMCIpKQogICAgICAg -IGV4Y2VwdCBWYWx1ZUVycm9yOgogICAgICAgICAgICBkYnVzX3BpZCA9IDAKICAgICAgICBpZiBkYnVzX3BpZCA+IDE6CiAgICAgICAgICAgIHByb2Nlc3Nl -cy5hcHBlbmQoT3duZWRQcm9jZXNzKGRidXNfcGlkKSkKICAgICAgICBmb3IgZXhlY3V0YWJsZSwgYXJncyBpbiBbCiAgICAgICAgICAgICgib3BlbmJveCIs -IFtdKSwgKCJ0aW50MiIsIFsiLWMiLCAiL2V0Yy9jbXV4L3RpbnQycmMiXSksCiAgICAgICAgICAgICgidm5jY29uZmlnIiwgWyItbm93aW4iXSksCiAgICAg -ICAgICAgICh3ZWJzb2NraWZ5LCBbIi0td2ViIiwgIi91c3Ivc2hhcmUvbm92bmMiLCAiLS1oZWFydGJlYXQiLCAiMzAiLAogICAgICAgICAgICAgICAgICAg -ICAgICAgIGYiMTI3LjAuMC4xOntub3ZuY19wb3J0fSIsIGYiMTI3LjAuMC4xOntyZmJfcG9ydH0iXSksCiAgICAgICAgXToKICAgICAgICAgICAgcGF0aCA9 -IGV4ZWN1dGFibGUgaWYgIi8iIGluIGV4ZWN1dGFibGUgZWxzZSBzaHV0aWwud2hpY2goZXhlY3V0YWJsZSkKICAgICAgICAgICAgaWYgbm90IHBhdGg6CiAg -ICAgICAgICAgICAgICBpZiBleGVjdXRhYmxlID09ICJ2bmNjb25maWciOgogICAgICAgICAgICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgICAgICAgICBy -YWlzZSBSdW50aW1lRXJyb3IoZiJtaXNzaW5nIGRpc3BsYXkgY29tcG9uZW50OiB7ZXhlY3V0YWJsZX0iKQogICAgICAgICAgICBwcm9jZXNzID0gc3VicHJv -Y2Vzcy5Qb3BlbihbcGF0aF0gKyBhcmdzLCBlbnY9ZW52aXJvbm1lbnQsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0ZGluPXN1 -YnByb2Nlc3MuREVWTlVMTCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3Rkb3V0PXN1YnByb2Nlc3MuREVWTlVMTCwgc3RkZXJy -PXN1YnByb2Nlc3MuREVWTlVMTCkKICAgICAgICAgICAgcHJvY2Vzc2VzLmFwcGVuZChwcm9jZXNzKQogICAgICAgIGlmIG5vdCBzZWxmLndhaXRfZm9yX3Bv -cnQobm92bmNfcG9ydCwgdGltZW91dD0xMCk6CiAgICAgICAgICAgIHJhaXNlIFJ1bnRpbWVFcnJvcigibm9WTkMgZGlkIG5vdCBiZWNvbWUgcmVhZHkiKQog -ICAgICAgIChydW50aW1lIC8gImVudiIpLndyaXRlX3RleHQoZiJleHBvcnQgRElTUExBWT06e251bWJlcn1cbiIpCgogICAgZGVmIHN1cGVydmlzZShzZWxm -LCBudW1iZXIsIGZpbmlzaGVkKToKICAgICAgICBydW50aW1lID0gc2VsZi5ydW50aW1lIC8gc3RyKG51bWJlcikKICAgICAgICBydW50aW1lLm1rZGlyKG1v -ZGU9MG83MDAsIHBhcmVudHM9VHJ1ZSwgZXhpc3Rfb2s9VHJ1ZSkKICAgICAgICBlbnZpcm9ubWVudCA9IGRpY3Qob3MuZW52aXJvbiwgRElTUExBWT1mIjp7 -bnVtYmVyfSIsIENNVVhfREVTS1RPUF9SVU5USU1FX0RJUj1zdHIocnVudGltZSkpCiAgICAgICAgZW52aXJvbm1lbnRbIkNIUk9NRV9DT05GSUdfSE9NRSJd -ID0gc3RyKHJ1bnRpbWUgLyAiY2hyb21lLWNvbmZpZyIpCiAgICAgICAgZW52aXJvbm1lbnRbIlhER19DT05GSUdfSE9NRSJdID0gc3RyKHJ1bnRpbWUgLyAi -eGRnLWNvbmZpZyIpCiAgICAgICAgZm9yIGtleSBpbiAoIk5PVElGWV9TT0NLRVQiLCAiREJVU19TRVNTSU9OX0JVU19BRERSRVNTIiwgIkRCVVNfU0VTU0lP -Tl9CVVNfUElEIiwKICAgICAgICAgICAgICAgICAgICAiQVRfU1BJX0JVU19BRERSRVNTIiwgIkFUX1NQSV9CVVMiKToKICAgICAgICAgICAgZW52aXJvbm1l -bnQucG9wKGtleSwgTm9uZSkKICAgICAgICB3aGlsZSBub3Qgc2VsZi5zaHV0ZG93bi5pc19zZXQoKToKICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAg -ICAgIyBUaGUgc3VwZXJ2aXNvciBpcyBhIGhlYWx0aCBjaGVjaywgbm90IGEgdGltZXItZHJpdmVuIHJlc3RhcnQuCiAgICAgICAgICAgICAgICAjIFByZXNl -cnZlIGFuIGFjdGl2ZSBSRkIvbm9WTkMgc2Vzc2lvbiB1bnRpbCBpdHMgb3duZWQgZW5kcG9pbnRzIGZhaWwuCiAgICAgICAgICAgICAgICBpZiBub3Qgc2Vs -Zi5wcm9jZXNzZXMuZ2V0KG51bWJlcikgb3Igbm90IHJlYWR5KG51bWJlcik6CiAgICAgICAgICAgICAgICAgICAgc2VsZi5zdGFydF9jb21wb25lbnRzKG51 -bWJlciwgZW52aXJvbm1lbnQsIHJ1bnRpbWUpCiAgICAgICAgICAgICAgICBzdGF0ZSA9ICJydW5uaW5nIiBpZiByZWFkeShudW1iZXIpIGVsc2UgInVuYXZh -aWxhYmxlIgogICAgICAgICAgICBleGNlcHQgKE9TRXJyb3IsIFJ1bnRpbWVFcnJvciwgc3VicHJvY2Vzcy5TdWJwcm9jZXNzRXJyb3IpOgogICAgICAgICAg -ICAgICAgc2VsZi50ZXJtaW5hdGUobnVtYmVyKQogICAgICAgICAgICAgICAgc3RhdGUgPSAidW5hdmFpbGFibGUiCiAgICAgICAgICAgIHdpdGggc2VsZi5s -b2NrOgogICAgICAgICAgICAgICAgc2VsZi5zdGF0ZXNbbnVtYmVyXSA9IHN0YXRlCiAgICAgICAgICAgIGZpbmlzaGVkLnNldCgpCiAgICAgICAgICAgIHNl -bGYuc2h1dGRvd24ud2FpdCgzMCkKICAgICAgICBzZWxmLnRlcm1pbmF0ZShudW1iZXIpCgogICAgZGVmIHNuYXBzaG90KHNlbGYpOgogICAgICAgIHdpdGgg -c2VsZi5sb2NrOgogICAgICAgICAgICByZXR1cm4geyJ2ZXJzaW9uIjogMSwgImNhbkNyZWF0ZSI6IGxlbihzZWxmLmpvYnMpIDwgTUFYX0RJU1BMQVlTIC0g -MSwKICAgICAgICAgICAgICAgICAgICAiZGlzcGxheXMiOiBbZGVzY3JpcHRvcigxLCAicnVubmluZyIgaWYgcmVhZHkoMSkgZWxzZSAidW5hdmFpbGFibGUi -KV0KICAgICAgICAgICAgICAgICAgICArIFtkZXNjcmlwdG9yKG4sIHNlbGYuc3RhdGVzLmdldChuLCAibGF1bmNoaW5nIikpIGZvciBuIGluIHNlbGYuY2F0 -YWxvZy5udW1iZXJzKCldfQoKICAgIGRlZiBoYW5kbGUoc2VsZiwgcmVxdWVzdCk6CiAgICAgICAgaWYgcmVxdWVzdC5nZXQoImFjdGlvbiIpID09ICJsaXN0 -IjoKICAgICAgICAgICAgcmV0dXJuIHNlbGYuc25hcHNob3QoKQogICAgICAgIGlmIHJlcXVlc3QuZ2V0KCJhY3Rpb24iKSAhPSAiY3JlYXRlIjoKICAgICAg -ICAgICAgcmFpc2UgVmFsdWVFcnJvcigidW5zdXBwb3J0ZWQgZGlzcGxheSBhY3Rpb24iKQogICAgICAgIG51bWJlciA9IHNlbGYuY2F0YWxvZy5hbGxvY2F0 -ZShyZXF1ZXN0LmdldCgicmVxdWVzdCIpKQogICAgICAgIHNlbGYuc3RhcnQobnVtYmVyKS53YWl0KDU1KQogICAgICAgIHJlc3VsdCA9IHNlbGYuc25hcHNo -b3QoKQogICAgICAgIHJlc3VsdFsiY3JlYXRlZCJdID0gZiJkaXNwbGF5OntudW1iZXJ9IgogICAgICAgIGlmIHNlbGYuc3RhdGVzLmdldChudW1iZXIpICE9 -ICJydW5uaW5nIjoKICAgICAgICAgICAgcmVzdWx0WyJlcnJvciJdID0gImRpc3BsYXlfc3RhcnRfZmFpbGVkIgogICAgICAgIHJldHVybiByZXN1bHQKCgpk -ZWYgc2VydmUocnVudGltZSk6CiAgICBydW50aW1lID0gUGF0aChydW50aW1lKQogICAgcnVudGltZS5ta2Rpcihtb2RlPTBvNzAwLCBwYXJlbnRzPVRydWUs -IGV4aXN0X29rPVRydWUpCiAgICB3aXRoIChydW50aW1lIC8gImRpc3BsYXktY29udHJvbC5sb2NrIikub3BlbigidyIpIGFzIGxvY2s6CiAgICAgICAgdHJ5 -OgogICAgICAgICAgICBmY250bC5mbG9jayhsb2NrLCBmY250bC5MT0NLX0VYIHwgZmNudGwuTE9DS19OQikKICAgICAgICBleGNlcHQgQmxvY2tpbmdJT0Vy -cm9yOgogICAgICAgICAgICByZXR1cm4KICAgICAgICBpZiBub3QgYWxsKHNodXRpbC53aGljaChuYW1lKSBmb3IgbmFtZSBpbiAoIndlYnNvY2tpZnkiLCAi -b3BlbmJveCIsICJkYnVzLWxhdW5jaCIpKToKICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJvcigiZGlzcGxheSBydW50aW1lIHVuYXZhaWxhYmxlIikKICAg -ICAgICBpZiBub3QgKHNodXRpbC53aGljaCgiWHZuYyIpIG9yIHNodXRpbC53aGljaCgiWHRpZ2Vydm5jIikpOgogICAgICAgICAgICByYWlzZSBWYWx1ZUVy -cm9yKCJkaXNwbGF5IHJ1bnRpbWUgdW5hdmFpbGFibGUiKQogICAgICAgIHNlcnZpY2UgPSBEaXNwbGF5U2VydmljZShEaXNwbGF5Q2F0YWxvZyhQYXRoLmhv -bWUoKSAvICIuY211eCIgLyAiZGlzcGxheXMiKSwgcnVudGltZSAvICJkaXNwbGF5cyIpCiAgICAgICAgZm9yIG51bWJlciBpbiBzZXJ2aWNlLmNhdGFsb2cu -bnVtYmVycygpOgogICAgICAgICAgICBzZXJ2aWNlLnN0YXJ0KG51bWJlcikKICAgICAgICBwYXRoID0gcnVudGltZSAvICJkaXNwbGF5LWNvbnRyb2wuc29j -ayIKICAgICAgICBpZiBwYXRoLmV4aXN0cygpOgogICAgICAgICAgICBwYXRoLnVubGluaygpCgogICAgICAgIGNsYXNzIEhhbmRsZXIoc29ja2V0c2VydmVy -LlN0cmVhbVJlcXVlc3RIYW5kbGVyKToKICAgICAgICAgICAgZGVmIGhhbmRsZShzZWxmKToKICAgICAgICAgICAgICAgIHNlbGYuY29ubmVjdGlvbi5zZXR0 -aW1lb3V0KDYwKQogICAgICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgIHJhdyA9IHNlbGYucmZpbGUucmVhZGxpbmUoNDA5NykKICAgICAg -ICAgICAgICAgICAgICBpZiBsZW4ocmF3KSA+IDQwOTY6CiAgICAgICAgICAgICAgICAgICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoImRpc3BsYXkgcmVxdWVz -dCB0b28gbGFyZ2UiKQogICAgICAgICAgICAgICAgICAgIHJlc3VsdCA9IHNlcnZpY2UuaGFuZGxlKGpzb24ubG9hZHMocmF3KSkKICAgICAgICAgICAgICAg -IGV4Y2VwdCAoVmFsdWVFcnJvciwgS2V5RXJyb3IsIFR5cGVFcnJvciwgT1NFcnJvcik6CiAgICAgICAgICAgICAgICAgICAgcmVzdWx0ID0geyJlcnJvciI6 -ICJkaXNwbGF5X3JlcXVlc3RfZmFpbGVkIn0KICAgICAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgICAgICBzZWxmLndmaWxlLndyaXRlKGpzb24u -ZHVtcHMocmVzdWx0KS5lbmNvZGUoKSArIGIiXG4iKQogICAgICAgICAgICAgICAgZXhjZXB0IE9TRXJyb3I6CiAgICAgICAgICAgICAgICAgICAgcGFzcyAg -IyBUaGUgcGVyc2lzdGVkIHJlY2VpcHQgcmVtYWlucyByZXBsYXlhYmxlIGFmdGVyIGRpc2Nvbm5lY3QuCgogICAgICAgIGNsYXNzIFNlcnZlcihzb2NrZXRz -ZXJ2ZXIuVGhyZWFkaW5nVW5peFN0cmVhbVNlcnZlcik6CiAgICAgICAgICAgIGRhZW1vbl90aHJlYWRzID0gVHJ1ZQoKICAgICAgICBwcmV2aW91c191bWFz -ayA9IG9zLnVtYXNrKDBvMDc3KQogICAgICAgIHRyeToKICAgICAgICAgICAgd2l0aCBTZXJ2ZXIoc3RyKHBhdGgpLCBIYW5kbGVyKSBhcyBzZXJ2ZXI6CiAg -ICAgICAgICAgICAgICBvcy51bWFzayhwcmV2aW91c191bWFzaykKICAgICAgICAgICAgICAgIHNlcnZlci5zZXJ2ZV9mb3JldmVyKCkKICAgICAgICBmaW5h -bGx5OgogICAgICAgICAgICBvcy51bWFzayhwcmV2aW91c191bWFzaykKICAgICAgICAgICAgc2VydmljZS5zaHV0ZG93bi5zZXQoKQogICAgICAgICAgICBw -YXRoLnVubGluayhtaXNzaW5nX29rPVRydWUpCgoKZGVmIG1haW4oKToKICAgIHBhcnNlciA9IGFyZ3BhcnNlLkFyZ3VtZW50UGFyc2VyKGRlc2NyaXB0aW9u -PV9fZG9jX18pCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50KCJhY3Rpb24iLCBjaG9pY2VzPVsibGlzdCIsICJjcmVhdGUiLCAic2VydmUiXSkKICAgIHBhcnNl -ci5hZGRfYXJndW1lbnQoIi0tcmVxdWVzdC1pZCIpCiAgICBhcmdzID0gcGFyc2VyLnBhcnNlX2FyZ3MoKQogICAgcnVudGltZSA9IG9zLmVudmlyb24uZ2V0 -KCJDTVVYX0RFU0tUT1BfUlVOVElNRV9ESVIiLCAiL3J1bi9jbXV4LWRlc2t0b3AiKQogICAgaWYgYXJncy5hY3Rpb24gPT0gInNlcnZlIjoKICAgICAgICBz -ZXJ2ZShydW50aW1lKQogICAgICAgIHJldHVybiAwCiAgICBpZiBhcmdzLmFjdGlvbiA9PSAiY3JlYXRlIiBhbmQgbm90IGFyZ3MucmVxdWVzdF9pZDoKICAg -ICAgICBwYXJzZXIuZXJyb3IoImNyZWF0ZSByZXF1aXJlcyAtLXJlcXVlc3QtaWQgKHJldGFpbiBpdCB3aGVuIHJldHJ5aW5nKSIpCiAgICB0cnk6CiAgICAg -ICAgd2l0aCBzb2NrZXQuc29ja2V0KHNvY2tldC5BRl9VTklYLCBzb2NrZXQuU09DS19TVFJFQU0pIGFzIGNsaWVudDoKICAgICAgICAgICAgY2xpZW50LnNl -dHRpbWVvdXQoNjApCiAgICAgICAgICAgIGNsaWVudC5jb25uZWN0KHN0cihQYXRoKHJ1bnRpbWUpIC8gImRpc3BsYXktY29udHJvbC5zb2NrIikpCiAgICAg -ICAgICAgIGNsaWVudC5zZW5kYWxsKGpzb24uZHVtcHMoeyJhY3Rpb24iOiBhcmdzLmFjdGlvbiwgInJlcXVlc3QiOiBhcmdzLnJlcXVlc3RfaWR9KS5lbmNv -ZGUoKSArIGIiXG4iKQogICAgICAgICAgICB3aXRoIGNsaWVudC5tYWtlZmlsZSgicmIiKSBhcyBzdHJlYW06CiAgICAgICAgICAgICAgICByZXN1bHQgPSBq -c29uLmxvYWRzKHN0cmVhbS5yZWFkbGluZSg2NTUzNikpCiAgICBleGNlcHQgKE9TRXJyb3IsIFZhbHVlRXJyb3IpOgogICAgICAgIHJlc3VsdCA9IHsiZXJy -b3IiOiAiZGlzcGxheV9ydW50aW1lX3VuYXZhaWxhYmxlIn0KICAgIHByaW50KGpzb24uZHVtcHMocmVzdWx0KSkKICAgIHJldHVybiAxIGlmICJlcnJvciIg -aW4gcmVzdWx0IGVsc2UgMAoKCmlmIF9fbmFtZV9fID09ICJfX21haW5fXyI6CiAgICByYWlzZSBTeXN0ZW1FeGl0KG1haW4oKSkK +IHN0ZGluPXN1YnByb2Nlc3MuREVWTlVMTCwgc3Rkb3V0PXN1YnByb2Nlc3MuUElQRSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdGRlcnI9c3Vi +cHJvY2Vzcy5ERVZOVUxMLCB0ZXh0PVRydWUsIHRpbWVvdXQ9MTAsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY2hlY2s9VHJ1ZSkuc3Rkb3V0CiAg +ICAgICAgZm9yIGxpbmUgaW4gYnVzLnNwbGl0bGluZXMoKToKICAgICAgICAgICAgaWYgIj0iIG5vdCBpbiBsaW5lOgogICAgICAgICAgICAgICAgY29udGlu +dWUKICAgICAgICAgICAga2V5LCB2YWx1ZSA9IGxpbmUuc3BsaXQoIj0iLCAxKQogICAgICAgICAgICBlbnZpcm9ubWVudFtrZXldID0gdmFsdWUuc3RyaXAo +KS5yc3RyaXAoIjsiKS5zdHJpcCgpLnN0cmlwKCInIikKICAgICAgICB0cnk6CiAgICAgICAgICAgIGRidXNfcGlkID0gaW50KGVudmlyb25tZW50LmdldCgi +REJVU19TRVNTSU9OX0JVU19QSUQiLCAiMCIpKQogICAgICAgIGV4Y2VwdCBWYWx1ZUVycm9yOgogICAgICAgICAgICBkYnVzX3BpZCA9IDAKICAgICAgICBp +ZiBkYnVzX3BpZCA+IDE6CiAgICAgICAgICAgIHByb2Nlc3Nlcy5hcHBlbmQoT3duZWRQcm9jZXNzKGRidXNfcGlkKSkKICAgICAgICBmb3IgZXhlY3V0YWJs +ZSwgYXJncyBpbiBbCiAgICAgICAgICAgICgib3BlbmJveCIsIFtdKSwgKCJ0aW50MiIsIFsiLWMiLCAiL2V0Yy9jbXV4L3RpbnQycmMiXSksCiAgICAgICAg +ICAgICgidm5jY29uZmlnIiwgWyItbm93aW4iXSksCiAgICAgICAgICAgICh3ZWJzb2NraWZ5LCBbIi0td2ViIiwgIi91c3Ivc2hhcmUvbm92bmMiLCAiLS1o +ZWFydGJlYXQiLCAiMzAiLAogICAgICAgICAgICAgICAgICAgICAgICAgIGYiMTI3LjAuMC4xOntub3ZuY19wb3J0fSIsIGYiMTI3LjAuMC4xOntyZmJfcG9y +dH0iXSksCiAgICAgICAgXToKICAgICAgICAgICAgcGF0aCA9IGV4ZWN1dGFibGUgaWYgIi8iIGluIGV4ZWN1dGFibGUgZWxzZSBzaHV0aWwud2hpY2goZXhl +Y3V0YWJsZSkKICAgICAgICAgICAgaWYgbm90IHBhdGg6CiAgICAgICAgICAgICAgICBpZiBleGVjdXRhYmxlID09ICJ2bmNjb25maWciOgogICAgICAgICAg +ICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgICAgICAgICByYWlzZSBSdW50aW1lRXJyb3IoZiJtaXNzaW5nIGRpc3BsYXkgY29tcG9uZW50OiB7ZXhlY3V0 +YWJsZX0iKQogICAgICAgICAgICBwcm9jZXNzID0gc3VicHJvY2Vzcy5Qb3BlbihbcGF0aF0gKyBhcmdzLCBlbnY9ZW52aXJvbm1lbnQsCiAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0ZGluPXN1YnByb2Nlc3MuREVWTlVMTCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgc3Rkb3V0PXN1YnByb2Nlc3MuREVWTlVMTCwgc3RkZXJyPXN1YnByb2Nlc3MuREVWTlVMTCkKICAgICAgICAgICAgcHJvY2Vzc2VzLmFwcGVuZChwcm9j +ZXNzKQogICAgICAgIGlmIG5vdCBzZWxmLndhaXRfZm9yX3BvcnQobm92bmNfcG9ydCwgdGltZW91dD0xMCk6CiAgICAgICAgICAgIHJhaXNlIFJ1bnRpbWVF +cnJvcigibm9WTkMgZGlkIG5vdCBiZWNvbWUgcmVhZHkiKQogICAgICAgIChydW50aW1lIC8gImVudiIpLndyaXRlX3RleHQoZiJleHBvcnQgRElTUExBWT06 +e251bWJlcn1cbiIpCgogICAgZGVmIHN1cGVydmlzZShzZWxmLCBudW1iZXIsIGZpbmlzaGVkKToKICAgICAgICBydW50aW1lID0gc2VsZi5ydW50aW1lIC8g +c3RyKG51bWJlcikKICAgICAgICBydW50aW1lLm1rZGlyKG1vZGU9MG83MDAsIHBhcmVudHM9VHJ1ZSwgZXhpc3Rfb2s9VHJ1ZSkKICAgICAgICBlbnZpcm9u +bWVudCA9IGRpY3Qob3MuZW52aXJvbiwgRElTUExBWT1mIjp7bnVtYmVyfSIsIENNVVhfREVTS1RPUF9SVU5USU1FX0RJUj1zdHIocnVudGltZSkpCiAgICAg +ICAgZW52aXJvbm1lbnRbIkNIUk9NRV9DT05GSUdfSE9NRSJdID0gc3RyKHJ1bnRpbWUgLyAiY2hyb21lLWNvbmZpZyIpCiAgICAgICAgZW52aXJvbm1lbnRb +IlhER19DT05GSUdfSE9NRSJdID0gc3RyKHJ1bnRpbWUgLyAieGRnLWNvbmZpZyIpCiAgICAgICAgZm9yIGtleSBpbiAoIk5PVElGWV9TT0NLRVQiLCAiREJV +U19TRVNTSU9OX0JVU19BRERSRVNTIiwgIkRCVVNfU0VTU0lPTl9CVVNfUElEIiwKICAgICAgICAgICAgICAgICAgICAiQVRfU1BJX0JVU19BRERSRVNTIiwg +IkFUX1NQSV9CVVMiKToKICAgICAgICAgICAgZW52aXJvbm1lbnQucG9wKGtleSwgTm9uZSkKICAgICAgICB3aGlsZSBub3Qgc2VsZi5zaHV0ZG93bi5pc19z +ZXQoKToKICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgIyBUaGUgc3VwZXJ2aXNvciBpcyBhIGhlYWx0aCBjaGVjaywgbm90IGEgdGltZXItZHJp +dmVuIHJlc3RhcnQuCiAgICAgICAgICAgICAgICAjIFByZXNlcnZlIGFuIGFjdGl2ZSBSRkIvbm9WTkMgc2Vzc2lvbiB1bnRpbCBpdHMgb3duZWQgZW5kcG9p +bnRzIGZhaWwuCiAgICAgICAgICAgICAgICBpZiBub3Qgc2VsZi5wcm9jZXNzZXMuZ2V0KG51bWJlcikgb3Igbm90IHJlYWR5KG51bWJlcik6CiAgICAgICAg +ICAgICAgICAgICAgc2VsZi5zdGFydF9jb21wb25lbnRzKG51bWJlciwgZW52aXJvbm1lbnQsIHJ1bnRpbWUpCiAgICAgICAgICAgICAgICBzdGF0ZSA9ICJy +dW5uaW5nIiBpZiByZWFkeShudW1iZXIpIGVsc2UgInVuYXZhaWxhYmxlIgogICAgICAgICAgICBleGNlcHQgKE9TRXJyb3IsIFJ1bnRpbWVFcnJvciwgc3Vi +cHJvY2Vzcy5TdWJwcm9jZXNzRXJyb3IpOgogICAgICAgICAgICAgICAgc2VsZi50ZXJtaW5hdGUobnVtYmVyKQogICAgICAgICAgICAgICAgc3RhdGUgPSAi +dW5hdmFpbGFibGUiCiAgICAgICAgICAgIHdpdGggc2VsZi5sb2NrOgogICAgICAgICAgICAgICAgc2VsZi5zdGF0ZXNbbnVtYmVyXSA9IHN0YXRlCiAgICAg +ICAgICAgIGZpbmlzaGVkLnNldCgpCiAgICAgICAgICAgIHNlbGYuc2h1dGRvd24ud2FpdCgzMCkKICAgICAgICBzZWxmLnRlcm1pbmF0ZShudW1iZXIpCgog +ICAgZGVmIHNuYXBzaG90KHNlbGYpOgogICAgICAgIHdpdGggc2VsZi5sb2NrOgogICAgICAgICAgICByZXR1cm4geyJ2ZXJzaW9uIjogMSwgImNhbkNyZWF0 +ZSI6IGxlbihzZWxmLmpvYnMpIDwgTUFYX0RJU1BMQVlTIC0gMSwKICAgICAgICAgICAgICAgICAgICAiZGlzcGxheXMiOiBbZGVzY3JpcHRvcigxLCAicnVu +bmluZyIgaWYgcmVhZHkoMSkgZWxzZSAidW5hdmFpbGFibGUiKV0KICAgICAgICAgICAgICAgICAgICArIFtkZXNjcmlwdG9yKG4sIHNlbGYuc3RhdGVzLmdl +dChuLCAibGF1bmNoaW5nIikpIGZvciBuIGluIHNlbGYuY2F0YWxvZy5udW1iZXJzKCldfQoKICAgIGRlZiBoYW5kbGUoc2VsZiwgcmVxdWVzdCk6CiAgICAg +ICAgaWYgcmVxdWVzdC5nZXQoImFjdGlvbiIpID09ICJsaXN0IjoKICAgICAgICAgICAgcmV0dXJuIHNlbGYuc25hcHNob3QoKQogICAgICAgIGlmIHJlcXVl +c3QuZ2V0KCJhY3Rpb24iKSAhPSAiY3JlYXRlIjoKICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJvcigidW5zdXBwb3J0ZWQgZGlzcGxheSBhY3Rpb24iKQog +ICAgICAgIG51bWJlciA9IHNlbGYuY2F0YWxvZy5hbGxvY2F0ZShyZXF1ZXN0LmdldCgicmVxdWVzdCIpKQogICAgICAgIHNlbGYuc3RhcnQobnVtYmVyKS53 +YWl0KDU1KQogICAgICAgIHJlc3VsdCA9IHNlbGYuc25hcHNob3QoKQogICAgICAgIHJlc3VsdFsiY3JlYXRlZCJdID0gZiJkaXNwbGF5OntudW1iZXJ9Igog +ICAgICAgIGlmIHNlbGYuc3RhdGVzLmdldChudW1iZXIpICE9ICJydW5uaW5nIjoKICAgICAgICAgICAgcmVzdWx0WyJlcnJvciJdID0gImRpc3BsYXlfc3Rh +cnRfZmFpbGVkIgogICAgICAgIHJldHVybiByZXN1bHQKCgpkZWYgc2VydmUocnVudGltZSk6CiAgICBydW50aW1lID0gUGF0aChydW50aW1lKQogICAgcnVu +dGltZS5ta2Rpcihtb2RlPTBvNzAwLCBwYXJlbnRzPVRydWUsIGV4aXN0X29rPVRydWUpCiAgICB3aXRoIChydW50aW1lIC8gImRpc3BsYXktY29udHJvbC5s +b2NrIikub3BlbigidyIpIGFzIGxvY2s6CiAgICAgICAgdHJ5OgogICAgICAgICAgICBmY250bC5mbG9jayhsb2NrLCBmY250bC5MT0NLX0VYIHwgZmNudGwu +TE9DS19OQikKICAgICAgICBleGNlcHQgQmxvY2tpbmdJT0Vycm9yOgogICAgICAgICAgICByZXR1cm4KICAgICAgICBpZiBub3QgYWxsKHNodXRpbC53aGlj +aChuYW1lKSBmb3IgbmFtZSBpbiAoIndlYnNvY2tpZnkiLCAib3BlbmJveCIsICJkYnVzLWxhdW5jaCIpKToKICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJv +cigiZGlzcGxheSBydW50aW1lIHVuYXZhaWxhYmxlIikKICAgICAgICBpZiBub3QgKHNodXRpbC53aGljaCgiWHZuYyIpIG9yIHNodXRpbC53aGljaCgiWHRp +Z2Vydm5jIikpOgogICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJkaXNwbGF5IHJ1bnRpbWUgdW5hdmFpbGFibGUiKQogICAgICAgIHNlcnZpY2UgPSBE +aXNwbGF5U2VydmljZShEaXNwbGF5Q2F0YWxvZyhQYXRoLmhvbWUoKSAvICIuY211eCIgLyAiZGlzcGxheXMiKSwgcnVudGltZSAvICJkaXNwbGF5cyIpCiAg +ICAgICAgZm9yIG51bWJlciBpbiBzZXJ2aWNlLmNhdGFsb2cubnVtYmVycygpOgogICAgICAgICAgICBzZXJ2aWNlLnN0YXJ0KG51bWJlcikKICAgICAgICBw +YXRoID0gcnVudGltZSAvICJkaXNwbGF5LWNvbnRyb2wuc29jayIKICAgICAgICBpZiBwYXRoLmV4aXN0cygpOgogICAgICAgICAgICBwYXRoLnVubGluaygp +CgogICAgICAgIGNsYXNzIEhhbmRsZXIoc29ja2V0c2VydmVyLlN0cmVhbVJlcXVlc3RIYW5kbGVyKToKICAgICAgICAgICAgZGVmIGhhbmRsZShzZWxmKToK +ICAgICAgICAgICAgICAgIHNlbGYuY29ubmVjdGlvbi5zZXR0aW1lb3V0KDYwKQogICAgICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgIHJh +dyA9IHNlbGYucmZpbGUucmVhZGxpbmUoNDA5NykKICAgICAgICAgICAgICAgICAgICBpZiBsZW4ocmF3KSA+IDQwOTY6CiAgICAgICAgICAgICAgICAgICAg +ICAgIHJhaXNlIFZhbHVlRXJyb3IoImRpc3BsYXkgcmVxdWVzdCB0b28gbGFyZ2UiKQogICAgICAgICAgICAgICAgICAgIHJlc3VsdCA9IHNlcnZpY2UuaGFu +ZGxlKGpzb24ubG9hZHMocmF3KSkKICAgICAgICAgICAgICAgIGV4Y2VwdCAoVmFsdWVFcnJvciwgS2V5RXJyb3IsIFR5cGVFcnJvciwgT1NFcnJvcik6CiAg +ICAgICAgICAgICAgICAgICAgcmVzdWx0ID0geyJlcnJvciI6ICJkaXNwbGF5X3JlcXVlc3RfZmFpbGVkIn0KICAgICAgICAgICAgICAgIHRyeToKICAgICAg +ICAgICAgICAgICAgICBzZWxmLndmaWxlLndyaXRlKGpzb24uZHVtcHMocmVzdWx0KS5lbmNvZGUoKSArIGIiXG4iKQogICAgICAgICAgICAgICAgZXhjZXB0 +IE9TRXJyb3I6CiAgICAgICAgICAgICAgICAgICAgcGFzcyAgIyBUaGUgcGVyc2lzdGVkIHJlY2VpcHQgcmVtYWlucyByZXBsYXlhYmxlIGFmdGVyIGRpc2Nv +bm5lY3QuCgogICAgICAgIGNsYXNzIFNlcnZlcihzb2NrZXRzZXJ2ZXIuVGhyZWFkaW5nVW5peFN0cmVhbVNlcnZlcik6CiAgICAgICAgICAgIGRhZW1vbl90 +aHJlYWRzID0gVHJ1ZQoKICAgICAgICBwcmV2aW91c191bWFzayA9IG9zLnVtYXNrKDBvMDc3KQogICAgICAgIHRyeToKICAgICAgICAgICAgd2l0aCBTZXJ2 +ZXIoc3RyKHBhdGgpLCBIYW5kbGVyKSBhcyBzZXJ2ZXI6CiAgICAgICAgICAgICAgICBvcy51bWFzayhwcmV2aW91c191bWFzaykKICAgICAgICAgICAgICAg +IHNlcnZlci5zZXJ2ZV9mb3JldmVyKCkKICAgICAgICBmaW5hbGx5OgogICAgICAgICAgICBvcy51bWFzayhwcmV2aW91c191bWFzaykKICAgICAgICAgICAg +c2VydmljZS5zaHV0ZG93bi5zZXQoKQogICAgICAgICAgICBwYXRoLnVubGluayhtaXNzaW5nX29rPVRydWUpCgoKZGVmIG1haW4oKToKICAgIHBhcnNlciA9 +IGFyZ3BhcnNlLkFyZ3VtZW50UGFyc2VyKGRlc2NyaXB0aW9uPV9fZG9jX18pCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50KCJhY3Rpb24iLCBjaG9pY2VzPVsi +bGlzdCIsICJjcmVhdGUiLCAic2VydmUiXSkKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoIi0tcmVxdWVzdC1pZCIpCiAgICBhcmdzID0gcGFyc2VyLnBhcnNl +X2FyZ3MoKQogICAgcnVudGltZSA9IG9zLmVudmlyb24uZ2V0KCJDTVVYX0RFU0tUT1BfUlVOVElNRV9ESVIiLCAiL3J1bi9jbXV4LWRlc2t0b3AiKQogICAg +aWYgYXJncy5hY3Rpb24gPT0gInNlcnZlIjoKICAgICAgICBzZXJ2ZShydW50aW1lKQogICAgICAgIHJldHVybiAwCiAgICBpZiBhcmdzLmFjdGlvbiA9PSAi +Y3JlYXRlIiBhbmQgbm90IGFyZ3MucmVxdWVzdF9pZDoKICAgICAgICBwYXJzZXIuZXJyb3IoImNyZWF0ZSByZXF1aXJlcyAtLXJlcXVlc3QtaWQgKHJldGFp +biBpdCB3aGVuIHJldHJ5aW5nKSIpCiAgICB0cnk6CiAgICAgICAgd2l0aCBzb2NrZXQuc29ja2V0KHNvY2tldC5BRl9VTklYLCBzb2NrZXQuU09DS19TVFJF +QU0pIGFzIGNsaWVudDoKICAgICAgICAgICAgY2xpZW50LnNldHRpbWVvdXQoNjApCiAgICAgICAgICAgIGNsaWVudC5jb25uZWN0KHN0cihQYXRoKHJ1bnRp +bWUpIC8gImRpc3BsYXktY29udHJvbC5zb2NrIikpCiAgICAgICAgICAgIGNsaWVudC5zZW5kYWxsKGpzb24uZHVtcHMoeyJhY3Rpb24iOiBhcmdzLmFjdGlv +biwgInJlcXVlc3QiOiBhcmdzLnJlcXVlc3RfaWR9KS5lbmNvZGUoKSArIGIiXG4iKQogICAgICAgICAgICB3aXRoIGNsaWVudC5tYWtlZmlsZSgicmIiKSBh +cyBzdHJlYW06CiAgICAgICAgICAgICAgICByZXN1bHQgPSBqc29uLmxvYWRzKHN0cmVhbS5yZWFkbGluZSg2NTUzNikpCiAgICBleGNlcHQgKE9TRXJyb3Is +IFZhbHVlRXJyb3IpOgogICAgICAgIHJlc3VsdCA9IHsiZXJyb3IiOiAiZGlzcGxheV9ydW50aW1lX3VuYXZhaWxhYmxlIn0KICAgIHByaW50KGpzb24uZHVt +cHMocmVzdWx0KSkKICAgIHJldHVybiAxIGlmICJlcnJvciIgaW4gcmVzdWx0IGVsc2UgMAoKCmlmIF9fbmFtZV9fID09ICJfX21haW5fXyI6CiAgICByYWlz +ZSBTeXN0ZW1FeGl0KG1haW4oKSkK """ private static let path = "$HOME/.cmux/cmux-display" diff --git a/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift b/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift index f553d3594ca7..99b29c50584c 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift @@ -43,9 +43,15 @@ extension CmuxTuiSurfaceProvider { /// Bind the page to its machine proxy without activating a system VPN. func configureBrowser(_ browser: BrowserPanel, url: URL, resourceID: SurfaceResourceID? = nil) { + let requestedPort = url.port ?? (url.scheme?.lowercased() == "https" ? 443 : 80) + let fallbackID: SurfaceResourceID = if info.hasDesktop, (CmuxTuiSnapshotParser.desktopPort...6916).contains(requestedPort) { + SurfaceResourceID(machine: machine, kind: .display, key: "display:\(requestedPort - 6900)") + } else { + SurfaceResourceID(machine: machine, kind: .browser, key: "port:\(requestedPort)") + } let resourceID = resourceID ?? browser.cloudAccess.resourceID ?? catalog.projectionRecord(forPanel: browser.id).flatMap { $0.resource.machine.isLocal ? nil : $0.resource } - ?? SurfaceResourceID(machine: machine, kind: .browser, key: "port:\(url.port ?? 80)") + ?? fallbackID guard resourceID.machine == machine, browser.cloudAccess.resourceID?.machine == nil || browser.cloudAccess.resourceID?.machine == machine, (try? catalog.validateOwnership(of: [resourceID], at: .workspace(id: browser.workspaceId, placement: .tab))) != nil else { diff --git a/tests/fixtures/cmux-display b/tests/fixtures/cmux-display index c5408b5a539f..2e0df79a8959 100755 --- a/tests/fixtures/cmux-display +++ b/tests/fixtures/cmux-display @@ -193,7 +193,9 @@ class DisplayService: if not self.wait_for_port(rfb_port): raise RuntimeError("Xvnc did not become ready") bus = subprocess.run(["dbus-launch", "--sh-syntax"], env=environment, - capture_output=True, text=True, check=True).stdout + stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, text=True, timeout=10, + check=True).stdout for line in bus.splitlines(): if "=" not in line: continue From 49e5899d576d58ee15f0b5e52b1caf048a1f7762 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 10:30:33 -0700 Subject: [PATCH 20/64] Keep unresolved display restores retryable --- Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift b/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift index 99b29c50584c..cd2766bb7298 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift @@ -137,7 +137,6 @@ extension CmuxTuiSurfaceProvider { for projection in catalog.projections(of: resource.id) where !materializedPanels.contains(projection.panelID) { guard let browser = SurfacePaneFactory.browserPanel(panelID: projection.panelID, in: projection.workspaceID), isCurrentLifecycleGeneration(generation), catalog.canRestoreProjection(projection) else { continue } - materializedPanels.insert(projection.panelID) switch CloudPortRoutePlan.plan(resource: resource, privateAddress: info.privateAddress) { case .privateDirect(let raw): guard let url = URL(string: raw) else { continue } From 26e71ed0fb4ed59975f991d1153b0b2bebf69e26 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 11:07:09 -0700 Subject: [PATCH 21/64] Fence guest discovery to provider lifetime --- Sources/Cloud/CloudDisplayCoordinator.swift | 27 ++++++++++++------- .../Surfaces/CmuxTuiSurfaceProviders.swift | 5 ++-- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/Sources/Cloud/CloudDisplayCoordinator.swift b/Sources/Cloud/CloudDisplayCoordinator.swift index 0201a1c3193d..f31f4ee027bf 100644 --- a/Sources/Cloud/CloudDisplayCoordinator.swift +++ b/Sources/Cloud/CloudDisplayCoordinator.swift @@ -11,6 +11,7 @@ final class CloudDisplayCoordinator { private(set) var isAvailable = false private var generation: UInt64 = 0 private var requestID: UUID? + private var refreshTask: Task? private var creation: Task? init(execute: @escaping @MainActor (String, Int) async throws -> VMExecResult) { @@ -21,18 +22,24 @@ final class CloudDisplayCoordinator { func refresh() async { guard creation == nil else { return } + refreshTask?.cancel() generation &+= 1 let token = generation - do { - let response = try await execute(CloudGuestDisplayScript.command(action: "list"), 10_000) - let snapshot = try CloudGuestDisplaySnapshot(data: Data(response.stdout.utf8)) - guard token == generation, !Task.isCancelled else { return } - self.snapshot = snapshot - isAvailable = response.exitCode == 0 - } catch { - guard token == generation else { return } - isAvailable = false + let task = Task { [weak self, execute] in + do { + let response = try await execute(CloudGuestDisplayScript.command(action: "list"), 10_000) + let snapshot = try CloudGuestDisplaySnapshot(data: Data(response.stdout.utf8)) + guard let self, token == self.generation, !Task.isCancelled else { return } + self.snapshot = snapshot + self.isAvailable = response.exitCode == 0 + } catch { + guard let self, token == self.generation else { return } + self.isAvailable = false + } } + refreshTask = task + await task.value + if refreshTask != nil, token == generation { refreshTask = nil } } func create() async throws -> CloudGuestDisplaySnapshot { @@ -71,6 +78,8 @@ final class CloudDisplayCoordinator { /// Drops guest state when the provider identity or account scope changes. func invalidate() { generation &+= 1 + refreshTask?.cancel() + refreshTask = nil creation?.cancel() creation = nil snapshot = nil diff --git a/Sources/Surfaces/CmuxTuiSurfaceProviders.swift b/Sources/Surfaces/CmuxTuiSurfaceProviders.swift index fa630da426f1..a95baad4c7b5 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProviders.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProviders.swift @@ -130,9 +130,10 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { self.portForwards = portForwards self.portAccessStore = portAccessStore ?? CloudPortAccessStore() self.browserPolicy = browserPolicy + let displayClient = VMClient.shared self.displayCoordinator = displayCoordinator ?? CloudDisplayCoordinator { command, timeout in - guard let client = VMClient.shared else { throw ProviderError.notSignedIn } - return try await client.exec(id: summary.id, command: command, timeoutMs: timeout) + guard let displayClient else { throw ProviderError.notSignedIn } + return try await displayClient.exec(id: summary.id, command: command, timeoutMs: timeout) } info = Self.info(from: summary, linkState: summary.status == "running" ? .connecting : .asleep, linkError: nil, stats: nil) installNotificationSync() From c0f7bcd2442bedf3d889bbdaffb3ef28a210eebe Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 11:49:57 -0700 Subject: [PATCH 22/64] test: cover display transport recovery and duplication state --- cmuxTests/CloudDesktopAccessTests.swift | 15 +++++++++++ tests/test_cloud_display_catalog.py | 33 +++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/cmuxTests/CloudDesktopAccessTests.swift b/cmuxTests/CloudDesktopAccessTests.swift index feecb7b25a9d..33e653a0fbc8 100644 --- a/cmuxTests/CloudDesktopAccessTests.swift +++ b/cmuxTests/CloudDesktopAccessTests.swift @@ -265,6 +265,21 @@ struct CloudDesktopAccessTests { #expect(state.resourceID == display) state.leave() #expect(state.resourceID == nil) + #expect(!state.retainsCloudResourceForDuplication) + } + + @Test("An unavailable Cloud placeholder retains its resource for duplication") + func unavailableCloudRetainsResourceIdentity() { + let state = CloudBrowserAccessState() + let model = CloudPortAccessModel(target: .init(host: "10.0.0.7", port: 6901), coordinator: nil, + wake: {}, startForward: { _ in 46901 }, stopForward: {}, route: .loopback) + let display = SurfaceResourceID(machine: .cloud("a"), kind: .display, key: "display:1") + state.configure(model: model, url: URL(string: "http://10.0.0.7:6901/vnc.html")!, resourceID: display) + state.showUnavailable("display unavailable") + #expect(state.resourceID == display) + #expect(state.retainsCloudResourceForDuplication) + state.leave() + #expect(state.resourceID == nil) } @Test("A forwarded /vnc.html URL is not a display when its resource is a browser") diff --git a/tests/test_cloud_display_catalog.py b/tests/test_cloud_display_catalog.py index 4cb54f9823de..1b6b2d7e76c1 100644 --- a/tests/test_cloud_display_catalog.py +++ b/tests/test_cloud_display_catalog.py @@ -132,6 +132,39 @@ def readiness(number): finally: service.shutdown.set() + def test_novnc_recovery_does_not_terminate_guest_desktop(self): + service = display.DisplayService(self.catalog(), self.root / "runtime") + number = 2 + + class Process: + def __init__(self): + self.terminated = False + + def terminate(self): + self.terminated = True + + x_server = Process() + old_websockify = Process() + new_websockify = Process() + service.processes[number] = [x_server] + service.websockify_processes[number] = old_websockify + environment = {"DISPLAY": ":2"} + runtime = self.root / "runtime" / "2" + runtime.mkdir(parents=True) + + with mock.patch.object(display, "ready", return_value=False), \ + mock.patch.object(display, "rfb_ready", return_value=True), \ + mock.patch.object(display, "novnc_ready", return_value=False), \ + mock.patch.object(display.shutil, "which", return_value="/usr/bin/websockify"), \ + mock.patch.object(display.subprocess, "Popen", return_value=new_websockify), \ + mock.patch.object(service, "wait_for_port", return_value=True): + service.start_components(number, environment, runtime) + + self.assertFalse(x_server.terminated) + self.assertTrue(old_websockify.terminated) + self.assertIs(service.websockify_processes[number], new_websockify) + service.shutdown.set() + def test_start_failure_retains_resource_and_replay_receipt(self): service = display.DisplayService(self.catalog(), self.root / "runtime") request = str(uuid.uuid4()) From 81bd61b81aac709b5bee8e4f16180fa3802507fc Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 11:50:02 -0700 Subject: [PATCH 23/64] fix: preserve Cloud displays during transport recovery --- Sources/Cloud/CloudGuestDisplayScript.swift | 315 ++++++++++-------- .../PortForward/CloudBrowserAccessState.swift | 10 + Sources/DockSplitStore+BrowserActions.swift | 7 +- .../Workspace+BrowserDuplication.swift | 7 +- tests/fixtures/cmux-display | 141 +++++--- 5 files changed, 284 insertions(+), 196 deletions(-) diff --git a/Sources/Cloud/CloudGuestDisplayScript.swift b/Sources/Cloud/CloudGuestDisplayScript.swift index 470b68d47c90..942a3089b6f5 100644 --- a/Sources/Cloud/CloudGuestDisplayScript.swift +++ b/Sources/Cloud/CloudGuestDisplayScript.swift @@ -14,153 +14,174 @@ b3J0IHV1aWQKCk1BWF9ESVNQTEFZUyA9IDE2ClNUQVJURVIgPSAiL3Vzci9sb2NhbC9iaW4vc3RhcnQt ciwgc3RhdGU9InJ1bm5pbmciKToKICAgIHJldHVybiB7ImlkIjogZiJkaXNwbGF5OntudW1iZXJ9IiwgIm51bWJlciI6IG51bWJlciwKICAgICAgICAgICAg InBvcnQiOiA2OTAwICsgbnVtYmVyLCAic3RhdGUiOiBzdGF0ZX0KCgpkZWYgbGlzdGVuaW5nKHBvcnQpOgogICAgdHJ5OgogICAgICAgIHdpdGggc29ja2V0 LmNyZWF0ZV9jb25uZWN0aW9uKCgiMTI3LjAuMC4xIiwgcG9ydCksIHRpbWVvdXQ9MC4zKToKICAgICAgICAgICAgcmV0dXJuIFRydWUKICAgIGV4Y2VwdCBP -U0Vycm9yOgogICAgICAgIHJldHVybiBGYWxzZQoKCmRlZiByZWFkeShudW1iZXIpOgogICAgIiIiQSBib3VuZCBIVFRQIHBvcnQgYWxvbmUgZG9lcyBub3Qg -cHJvdmUgdGhhdCBhbiBYL1JGQiBzZXJ2ZXIgZXhpc3RzLiIiIgogICAgdHJ5OgogICAgICAgIHdpdGggc29ja2V0LmNyZWF0ZV9jb25uZWN0aW9uKCgiMTI3 -LjAuMC4xIiwgNTkwMCArIG51bWJlciksIHRpbWVvdXQ9MC41KSBhcyByZmI6CiAgICAgICAgICAgIHZlcnNpb24gPSBiIiIKICAgICAgICAgICAgd2hpbGUg -bGVuKHZlcnNpb24pIDwgMTI6CiAgICAgICAgICAgICAgICBwYXJ0ID0gcmZiLnJlY3YoMTIgLSBsZW4odmVyc2lvbikpCiAgICAgICAgICAgICAgICBpZiBu -b3QgcGFydDoKICAgICAgICAgICAgICAgICAgICByZXR1cm4gRmFsc2UKICAgICAgICAgICAgICAgIHZlcnNpb24gKz0gcGFydAogICAgICAgICAgICBpZiBu -b3QgdmVyc2lvbi5zdGFydHN3aXRoKGIiUkZCICIpOgogICAgICAgICAgICAgICAgcmV0dXJuIEZhbHNlCiAgICAgICAgY29ubmVjdGlvbiA9IGh0dHAuY2xp -ZW50LkhUVFBDb25uZWN0aW9uKCIxMjcuMC4wLjEiLCA2OTAwICsgbnVtYmVyLCB0aW1lb3V0PTAuNSkKICAgICAgICB0cnk6CiAgICAgICAgICAgIGNvbm5l -Y3Rpb24ucmVxdWVzdCgiSEVBRCIsICIvdm5jLmh0bWwiKQogICAgICAgICAgICByZXR1cm4gY29ubmVjdGlvbi5nZXRyZXNwb25zZSgpLnN0YXR1cyA9PSAy -MDAKICAgICAgICBmaW5hbGx5OgogICAgICAgICAgICBjb25uZWN0aW9uLmNsb3NlKCkKICAgIGV4Y2VwdCAoT1NFcnJvciwgaHR0cC5jbGllbnQuSFRUUEV4 -Y2VwdGlvbik6CiAgICAgICAgcmV0dXJuIEZhbHNlCgoKY2xhc3MgRGlzcGxheUNhdGFsb2c6CiAgICAiIiJTZXJpYWxpemVzIGFsbG9jYXRpb24gYW5kIHBl -cnNpc3RzIHJlcXVlc3QgcmVjZWlwdHMgYmVmb3JlIHN0YXJ0aW5nIHdvcmsuIiIiCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGRpcmVjdG9yeSwgb2NjdXBp -ZWQ9Tm9uZSk6CiAgICAgICAgc2VsZi5kaXJlY3RvcnkgPSBQYXRoKGRpcmVjdG9yeSkKICAgICAgICBzZWxmLmRpcmVjdG9yeS5ta2Rpcihtb2RlPTBvNzAw -LCBwYXJlbnRzPVRydWUsIGV4aXN0X29rPVRydWUpCiAgICAgICAgc2VsZi5wYXRoID0gc2VsZi5kaXJlY3RvcnkgLyAiY2F0YWxvZy5qc29uIgogICAgICAg -IHNlbGYubG9jayA9IHRocmVhZGluZy5STG9jaygpCiAgICAgICAgc2VsZi5vY2N1cGllZCA9IG9jY3VwaWVkIG9yIChsYW1iZGEgbjogUGF0aChmIi90bXAv -LlgxMS11bml4L1h7bn0iKS5leGlzdHMoKQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgb3IgbGlzdGVuaW5nKDU5MDAgKyBuKSBvciBs -aXN0ZW5pbmcoNjkwMCArIG4pKQogICAgICAgIHNlbGYucmVjb3JkcyA9IFtdCiAgICAgICAgaWYgc2VsZi5wYXRoLmV4aXN0cygpOgogICAgICAgICAgICBk -YXRhID0ganNvbi5sb2FkcyhzZWxmLnBhdGgucmVhZF90ZXh0KCkpCiAgICAgICAgICAgIGlmIGRhdGEuZ2V0KCJ2ZXJzaW9uIikgIT0gMToKICAgICAgICAg -ICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoInVuc3VwcG9ydGVkIGRpc3BsYXkgY2F0YWxvZyB2ZXJzaW9uIikKICAgICAgICAgICAgc2Vlbl9udW1iZXJzLCBz -ZWVuX3JlcXVlc3RzID0gc2V0KCksIHNldCgpCiAgICAgICAgICAgIGZvciByZWNvcmQgaW4gZGF0YVsiZGlzcGxheXMiXToKICAgICAgICAgICAgICAgIG51 -bWJlciwgcmVxdWVzdCA9IHJlY29yZFsibnVtYmVyIl0sIHJlY29yZFsicmVxdWVzdCJdCiAgICAgICAgICAgICAgICBpZiB0eXBlKG51bWJlcikgaXMgbm90 -IGludCBvciBub3QgMiA8PSBudW1iZXIgPD0gTUFYX0RJU1BMQVlTOgogICAgICAgICAgICAgICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoImludmFsaWQgZGlz -cGxheSBudW1iZXIiKQogICAgICAgICAgICAgICAgaWYgc3RyKHV1aWQuVVVJRChyZXF1ZXN0KSkgIT0gcmVxdWVzdCBvciBudW1iZXIgaW4gc2Vlbl9udW1i -ZXJzIG9yIHJlcXVlc3QgaW4gc2Vlbl9yZXF1ZXN0czoKICAgICAgICAgICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJpbnZhbGlkIGRpc3BsYXkgaWRl -bnRpdHkiKQogICAgICAgICAgICAgICAgc2Vlbl9udW1iZXJzLmFkZChudW1iZXIpCiAgICAgICAgICAgICAgICBzZWVuX3JlcXVlc3RzLmFkZChyZXF1ZXN0 -KQogICAgICAgICAgICAgICAgc2VsZi5yZWNvcmRzLmFwcGVuZCh7Im51bWJlciI6IG51bWJlciwgInJlcXVlc3QiOiByZXF1ZXN0fSkKCiAgICBkZWYgbnVt -YmVycyhzZWxmKToKICAgICAgICB3aXRoIHNlbGYubG9jazoKICAgICAgICAgICAgcmV0dXJuIHNvcnRlZChyZWNvcmRbIm51bWJlciJdIGZvciByZWNvcmQg -aW4gc2VsZi5yZWNvcmRzKQoKICAgIGRlZiBhbGxvY2F0ZShzZWxmLCByZXF1ZXN0KToKICAgICAgICBpZiBub3QgaXNpbnN0YW5jZShyZXF1ZXN0LCBzdHIp -IG9yIHN0cih1dWlkLlVVSUQocmVxdWVzdCkpICE9IHJlcXVlc3Q6CiAgICAgICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoImludmFsaWQgcmVxdWVzdCBpZGVu -dGl0eSIpCiAgICAgICAgd2l0aCBzZWxmLmxvY2s6CiAgICAgICAgICAgIGZvciByZWNvcmQgaW4gc2VsZi5yZWNvcmRzOgogICAgICAgICAgICAgICAgaWYg -cmVjb3JkWyJyZXF1ZXN0Il0gPT0gcmVxdWVzdDoKICAgICAgICAgICAgICAgICAgICByZXR1cm4gcmVjb3JkWyJudW1iZXIiXQogICAgICAgICAgICB1c2Vk -ID0gc2V0KHNlbGYubnVtYmVycygpKQogICAgICAgICAgICBudW1iZXIgPSBuZXh0KChuIGZvciBuIGluIHJhbmdlKDIsIE1BWF9ESVNQTEFZUyArIDEpCiAg -ICAgICAgICAgICAgICAgICAgICAgICAgIGlmIG4gbm90IGluIHVzZWQgYW5kIG5vdCBzZWxmLm9jY3VwaWVkKG4pKSwgTm9uZSkKICAgICAgICAgICAgaWYg -bnVtYmVyIGlzIE5vbmU6CiAgICAgICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJkaXNwbGF5IGNhcGFjaXR5IGV4aGF1c3RlZCIpCiAgICAgICAgICAg -IHJlY29yZHMgPSBzZWxmLnJlY29yZHMgKyBbeyJudW1iZXIiOiBudW1iZXIsICJyZXF1ZXN0IjogcmVxdWVzdH1dCiAgICAgICAgICAgIGZkLCB0ZW1wb3Jh -cnkgPSB0ZW1wZmlsZS5ta3N0ZW1wKHByZWZpeD0iY2F0YWxvZy0iLCBkaXI9c2VsZi5kaXJlY3RvcnkpCiAgICAgICAgICAgIHRyeToKICAgICAgICAgICAg -ICAgIHdpdGggb3MuZmRvcGVuKGZkLCAidyIpIGFzIG91dHB1dDoKICAgICAgICAgICAgICAgICAgICBqc29uLmR1bXAoeyJ2ZXJzaW9uIjogMSwgImRpc3Bs -YXlzIjogcmVjb3Jkc30sIG91dHB1dCkKICAgICAgICAgICAgICAgICAgICBvdXRwdXQuZmx1c2goKQogICAgICAgICAgICAgICAgICAgIG9zLmZzeW5jKG91 -dHB1dC5maWxlbm8oKSkKICAgICAgICAgICAgICAgIG9zLnJlcGxhY2UodGVtcG9yYXJ5LCBzZWxmLnBhdGgpCiAgICAgICAgICAgIGZpbmFsbHk6CiAgICAg -ICAgICAgICAgICBpZiBvcy5wYXRoLmV4aXN0cyh0ZW1wb3JhcnkpOgogICAgICAgICAgICAgICAgICAgIG9zLnVubGluayh0ZW1wb3JhcnkpCiAgICAgICAg -ICAgIHNlbGYucmVjb3JkcyA9IHJlY29yZHMKICAgICAgICAgICAgcmV0dXJuIG51bWJlcgoKCmNsYXNzIE93bmVkUHJvY2VzczoKICAgICIiIkEgaGFuZGxl -IGZvciBhIGRhZW1vbiBzdGFydGVkIGJ5IGRidXMtbGF1bmNoIHJhdGhlciB0aGFuIGJ5IFBvcGVuLiIiIgoKICAgIGRlZiBfX2luaXRfXyhzZWxmLCBwaWQp -OgogICAgICAgIHNlbGYucGlkID0gcGlkCgogICAgZGVmIHRlcm1pbmF0ZShzZWxmKToKICAgICAgICB0cnk6CiAgICAgICAgICAgIG9zLmtpbGwoc2VsZi5w -aWQsIDE1KQogICAgICAgIGV4Y2VwdCBPU0Vycm9yOgogICAgICAgICAgICBwYXNzCgogICAgZGVmIHBvbGwoc2VsZik6CiAgICAgICAgdHJ5OgogICAgICAg -ICAgICBvcy5raWxsKHNlbGYucGlkLCAwKQogICAgICAgICAgICByZXR1cm4gTm9uZQogICAgICAgIGV4Y2VwdCBPU0Vycm9yOgogICAgICAgICAgICByZXR1 -cm4gMAoKCmNsYXNzIERpc3BsYXlTZXJ2aWNlOgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGNhdGFsb2csIHJ1bnRpbWUsIHN0YXJ0ZXI9U1RBUlRFUik6CiAg -ICAgICAgc2VsZi5jYXRhbG9nID0gY2F0YWxvZwogICAgICAgIHNlbGYucnVudGltZSA9IFBhdGgocnVudGltZSkKICAgICAgICBzZWxmLnN0YXJ0ZXIgPSBz -dGFydGVyCiAgICAgICAgc2VsZi5sb2NrID0gdGhyZWFkaW5nLkxvY2soKQogICAgICAgIHNlbGYuam9icyA9IHt9CiAgICAgICAgc2VsZi5zdGF0ZXMgPSB7 -fQogICAgICAgIHNlbGYucHJvY2Vzc2VzID0ge30KICAgICAgICBzZWxmLnNodXRkb3duID0gdGhyZWFkaW5nLkV2ZW50KCkKCiAgICBkZWYgc3RhcnQoc2Vs -ZiwgbnVtYmVyKToKICAgICAgICB3aXRoIHNlbGYubG9jazoKICAgICAgICAgICAgaWYgbnVtYmVyIGluIHNlbGYuam9iczoKICAgICAgICAgICAgICAgIHJl -dHVybiBzZWxmLmpvYnNbbnVtYmVyXQogICAgICAgICAgICBmaW5pc2hlZCA9IHRocmVhZGluZy5FdmVudCgpCiAgICAgICAgICAgIHNlbGYuam9ic1tudW1i -ZXJdID0gZmluaXNoZWQKICAgICAgICAgICAgc2VsZi5zdGF0ZXNbbnVtYmVyXSA9ICJsYXVuY2hpbmciCiAgICAgICAgICAgIHRocmVhZGluZy5UaHJlYWQo -dGFyZ2V0PXNlbGYuc3VwZXJ2aXNlLCBhcmdzPShudW1iZXIsIGZpbmlzaGVkKSwgZGFlbW9uPVRydWUpLnN0YXJ0KCkKICAgICAgICAgICAgcmV0dXJuIGZp -bmlzaGVkCgogICAgZGVmIHRlcm1pbmF0ZShzZWxmLCBudW1iZXIpOgogICAgICAgIGZvciBwcm9jZXNzIGluIHNlbGYucHJvY2Vzc2VzLnBvcChudW1iZXIs -IFtdKToKICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgcHJvY2Vzcy50ZXJtaW5hdGUoKQogICAgICAgICAgICBleGNlcHQgT1NFcnJvcjoKICAg -ICAgICAgICAgICAgIHBhc3MKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgd2FpdF9mb3JfcG9ydChwb3J0LCB0aW1lb3V0PTIwKToKICAgICAgICBkZWFk -bGluZSA9IHRpbWUubW9ub3RvbmljKCkgKyB0aW1lb3V0CiAgICAgICAgd2hpbGUgdGltZS5tb25vdG9uaWMoKSA8IGRlYWRsaW5lOgogICAgICAgICAgICBp -ZiBsaXN0ZW5pbmcocG9ydCk6CiAgICAgICAgICAgICAgICByZXR1cm4gVHJ1ZQogICAgICAgICAgICB0aW1lLnNsZWVwKDAuMSkKICAgICAgICByZXR1cm4g -RmFsc2UKCiAgICBkZWYgc3RhcnRfY29tcG9uZW50cyhzZWxmLCBudW1iZXIsIGVudmlyb25tZW50LCBydW50aW1lKToKICAgICAgICBpZiByZWFkeShudW1i -ZXIpOgogICAgICAgICAgICByZXR1cm4KICAgICAgICBzZWxmLnRlcm1pbmF0ZShudW1iZXIpCiAgICAgICAgdm5jID0gc2h1dGlsLndoaWNoKCJYdm5jIikg -b3Igc2h1dGlsLndoaWNoKCJYdGlnZXJ2bmMiKQogICAgICAgIHdlYnNvY2tpZnkgPSBzaHV0aWwud2hpY2goIndlYnNvY2tpZnkiKQogICAgICAgIGlmIG5v -dCB2bmMgb3Igbm90IHdlYnNvY2tpZnk6CiAgICAgICAgICAgIHJhaXNlIFJ1bnRpbWVFcnJvcigiZGlzcGxheSBydW50aW1lIHVuYXZhaWxhYmxlIikKICAg -ICAgICByZmJfcG9ydCwgbm92bmNfcG9ydCA9IDU5MDAgKyBudW1iZXIsIDY5MDAgKyBudW1iZXIKICAgICAgICBwcm9jZXNzZXMgPSBbXQogICAgICAgIHBy -b2Nlc3Nlcy5hcHBlbmQoc3VicHJvY2Vzcy5Qb3BlbigKICAgICAgICAgICAgW3ZuYywgZiI6e251bWJlcn0iLCAiLWdlb21ldHJ5Iiwgb3MuZW52aXJvbi5n -ZXQoIkNNVVhfVk5DX0dFT01FVFJZIiwgIjE0NDB4OTAwIiksCiAgICAgICAgICAgICAiLWRlcHRoIiwgIjI0IiwgIi1yZmJwb3J0Iiwgc3RyKHJmYl9wb3J0 -KSwgIi1sb2NhbGhvc3QiLAogICAgICAgICAgICAgIi1TZWN1cml0eVR5cGVzIiwgIk5vbmUiLCAiLUFsd2F5c1NoYXJlZCJdLAogICAgICAgICAgICBlbnY9 -ZW52aXJvbm1lbnQsIHN0ZGluPXN1YnByb2Nlc3MuREVWTlVMTCwKICAgICAgICAgICAgc3Rkb3V0PXN1YnByb2Nlc3MuREVWTlVMTCwgc3RkZXJyPXN1YnBy -b2Nlc3MuREVWTlVMTCkpCiAgICAgICAgc2VsZi5wcm9jZXNzZXNbbnVtYmVyXSA9IHByb2Nlc3NlcwogICAgICAgIGlmIG5vdCBzZWxmLndhaXRfZm9yX3Bv -cnQocmZiX3BvcnQpOgogICAgICAgICAgICByYWlzZSBSdW50aW1lRXJyb3IoIlh2bmMgZGlkIG5vdCBiZWNvbWUgcmVhZHkiKQogICAgICAgIGJ1cyA9IHN1 -YnByb2Nlc3MucnVuKFsiZGJ1cy1sYXVuY2giLCAiLS1zaC1zeW50YXgiXSwgZW52PWVudmlyb25tZW50LAogICAgICAgICAgICAgICAgICAgICAgICAgICAg -IHN0ZGluPXN1YnByb2Nlc3MuREVWTlVMTCwgc3Rkb3V0PXN1YnByb2Nlc3MuUElQRSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdGRlcnI9c3Vi -cHJvY2Vzcy5ERVZOVUxMLCB0ZXh0PVRydWUsIHRpbWVvdXQ9MTAsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY2hlY2s9VHJ1ZSkuc3Rkb3V0CiAg -ICAgICAgZm9yIGxpbmUgaW4gYnVzLnNwbGl0bGluZXMoKToKICAgICAgICAgICAgaWYgIj0iIG5vdCBpbiBsaW5lOgogICAgICAgICAgICAgICAgY29udGlu -dWUKICAgICAgICAgICAga2V5LCB2YWx1ZSA9IGxpbmUuc3BsaXQoIj0iLCAxKQogICAgICAgICAgICBlbnZpcm9ubWVudFtrZXldID0gdmFsdWUuc3RyaXAo -KS5yc3RyaXAoIjsiKS5zdHJpcCgpLnN0cmlwKCInIikKICAgICAgICB0cnk6CiAgICAgICAgICAgIGRidXNfcGlkID0gaW50KGVudmlyb25tZW50LmdldCgi -REJVU19TRVNTSU9OX0JVU19QSUQiLCAiMCIpKQogICAgICAgIGV4Y2VwdCBWYWx1ZUVycm9yOgogICAgICAgICAgICBkYnVzX3BpZCA9IDAKICAgICAgICBp -ZiBkYnVzX3BpZCA+IDE6CiAgICAgICAgICAgIHByb2Nlc3Nlcy5hcHBlbmQoT3duZWRQcm9jZXNzKGRidXNfcGlkKSkKICAgICAgICBmb3IgZXhlY3V0YWJs -ZSwgYXJncyBpbiBbCiAgICAgICAgICAgICgib3BlbmJveCIsIFtdKSwgKCJ0aW50MiIsIFsiLWMiLCAiL2V0Yy9jbXV4L3RpbnQycmMiXSksCiAgICAgICAg -ICAgICgidm5jY29uZmlnIiwgWyItbm93aW4iXSksCiAgICAgICAgICAgICh3ZWJzb2NraWZ5LCBbIi0td2ViIiwgIi91c3Ivc2hhcmUvbm92bmMiLCAiLS1o -ZWFydGJlYXQiLCAiMzAiLAogICAgICAgICAgICAgICAgICAgICAgICAgIGYiMTI3LjAuMC4xOntub3ZuY19wb3J0fSIsIGYiMTI3LjAuMC4xOntyZmJfcG9y -dH0iXSksCiAgICAgICAgXToKICAgICAgICAgICAgcGF0aCA9IGV4ZWN1dGFibGUgaWYgIi8iIGluIGV4ZWN1dGFibGUgZWxzZSBzaHV0aWwud2hpY2goZXhl -Y3V0YWJsZSkKICAgICAgICAgICAgaWYgbm90IHBhdGg6CiAgICAgICAgICAgICAgICBpZiBleGVjdXRhYmxlID09ICJ2bmNjb25maWciOgogICAgICAgICAg -ICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgICAgICAgICByYWlzZSBSdW50aW1lRXJyb3IoZiJtaXNzaW5nIGRpc3BsYXkgY29tcG9uZW50OiB7ZXhlY3V0 -YWJsZX0iKQogICAgICAgICAgICBwcm9jZXNzID0gc3VicHJvY2Vzcy5Qb3BlbihbcGF0aF0gKyBhcmdzLCBlbnY9ZW52aXJvbm1lbnQsCiAgICAgICAgICAg -ICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0ZGluPXN1YnByb2Nlc3MuREVWTlVMTCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg -ICAgc3Rkb3V0PXN1YnByb2Nlc3MuREVWTlVMTCwgc3RkZXJyPXN1YnByb2Nlc3MuREVWTlVMTCkKICAgICAgICAgICAgcHJvY2Vzc2VzLmFwcGVuZChwcm9j -ZXNzKQogICAgICAgIGlmIG5vdCBzZWxmLndhaXRfZm9yX3BvcnQobm92bmNfcG9ydCwgdGltZW91dD0xMCk6CiAgICAgICAgICAgIHJhaXNlIFJ1bnRpbWVF -cnJvcigibm9WTkMgZGlkIG5vdCBiZWNvbWUgcmVhZHkiKQogICAgICAgIChydW50aW1lIC8gImVudiIpLndyaXRlX3RleHQoZiJleHBvcnQgRElTUExBWT06 -e251bWJlcn1cbiIpCgogICAgZGVmIHN1cGVydmlzZShzZWxmLCBudW1iZXIsIGZpbmlzaGVkKToKICAgICAgICBydW50aW1lID0gc2VsZi5ydW50aW1lIC8g -c3RyKG51bWJlcikKICAgICAgICBydW50aW1lLm1rZGlyKG1vZGU9MG83MDAsIHBhcmVudHM9VHJ1ZSwgZXhpc3Rfb2s9VHJ1ZSkKICAgICAgICBlbnZpcm9u -bWVudCA9IGRpY3Qob3MuZW52aXJvbiwgRElTUExBWT1mIjp7bnVtYmVyfSIsIENNVVhfREVTS1RPUF9SVU5USU1FX0RJUj1zdHIocnVudGltZSkpCiAgICAg -ICAgZW52aXJvbm1lbnRbIkNIUk9NRV9DT05GSUdfSE9NRSJdID0gc3RyKHJ1bnRpbWUgLyAiY2hyb21lLWNvbmZpZyIpCiAgICAgICAgZW52aXJvbm1lbnRb -IlhER19DT05GSUdfSE9NRSJdID0gc3RyKHJ1bnRpbWUgLyAieGRnLWNvbmZpZyIpCiAgICAgICAgZm9yIGtleSBpbiAoIk5PVElGWV9TT0NLRVQiLCAiREJV -U19TRVNTSU9OX0JVU19BRERSRVNTIiwgIkRCVVNfU0VTU0lPTl9CVVNfUElEIiwKICAgICAgICAgICAgICAgICAgICAiQVRfU1BJX0JVU19BRERSRVNTIiwg -IkFUX1NQSV9CVVMiKToKICAgICAgICAgICAgZW52aXJvbm1lbnQucG9wKGtleSwgTm9uZSkKICAgICAgICB3aGlsZSBub3Qgc2VsZi5zaHV0ZG93bi5pc19z -ZXQoKToKICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgIyBUaGUgc3VwZXJ2aXNvciBpcyBhIGhlYWx0aCBjaGVjaywgbm90IGEgdGltZXItZHJp -dmVuIHJlc3RhcnQuCiAgICAgICAgICAgICAgICAjIFByZXNlcnZlIGFuIGFjdGl2ZSBSRkIvbm9WTkMgc2Vzc2lvbiB1bnRpbCBpdHMgb3duZWQgZW5kcG9p -bnRzIGZhaWwuCiAgICAgICAgICAgICAgICBpZiBub3Qgc2VsZi5wcm9jZXNzZXMuZ2V0KG51bWJlcikgb3Igbm90IHJlYWR5KG51bWJlcik6CiAgICAgICAg -ICAgICAgICAgICAgc2VsZi5zdGFydF9jb21wb25lbnRzKG51bWJlciwgZW52aXJvbm1lbnQsIHJ1bnRpbWUpCiAgICAgICAgICAgICAgICBzdGF0ZSA9ICJy -dW5uaW5nIiBpZiByZWFkeShudW1iZXIpIGVsc2UgInVuYXZhaWxhYmxlIgogICAgICAgICAgICBleGNlcHQgKE9TRXJyb3IsIFJ1bnRpbWVFcnJvciwgc3Vi -cHJvY2Vzcy5TdWJwcm9jZXNzRXJyb3IpOgogICAgICAgICAgICAgICAgc2VsZi50ZXJtaW5hdGUobnVtYmVyKQogICAgICAgICAgICAgICAgc3RhdGUgPSAi -dW5hdmFpbGFibGUiCiAgICAgICAgICAgIHdpdGggc2VsZi5sb2NrOgogICAgICAgICAgICAgICAgc2VsZi5zdGF0ZXNbbnVtYmVyXSA9IHN0YXRlCiAgICAg -ICAgICAgIGZpbmlzaGVkLnNldCgpCiAgICAgICAgICAgIHNlbGYuc2h1dGRvd24ud2FpdCgzMCkKICAgICAgICBzZWxmLnRlcm1pbmF0ZShudW1iZXIpCgog -ICAgZGVmIHNuYXBzaG90KHNlbGYpOgogICAgICAgIHdpdGggc2VsZi5sb2NrOgogICAgICAgICAgICByZXR1cm4geyJ2ZXJzaW9uIjogMSwgImNhbkNyZWF0 -ZSI6IGxlbihzZWxmLmpvYnMpIDwgTUFYX0RJU1BMQVlTIC0gMSwKICAgICAgICAgICAgICAgICAgICAiZGlzcGxheXMiOiBbZGVzY3JpcHRvcigxLCAicnVu -bmluZyIgaWYgcmVhZHkoMSkgZWxzZSAidW5hdmFpbGFibGUiKV0KICAgICAgICAgICAgICAgICAgICArIFtkZXNjcmlwdG9yKG4sIHNlbGYuc3RhdGVzLmdl -dChuLCAibGF1bmNoaW5nIikpIGZvciBuIGluIHNlbGYuY2F0YWxvZy5udW1iZXJzKCldfQoKICAgIGRlZiBoYW5kbGUoc2VsZiwgcmVxdWVzdCk6CiAgICAg -ICAgaWYgcmVxdWVzdC5nZXQoImFjdGlvbiIpID09ICJsaXN0IjoKICAgICAgICAgICAgcmV0dXJuIHNlbGYuc25hcHNob3QoKQogICAgICAgIGlmIHJlcXVl -c3QuZ2V0KCJhY3Rpb24iKSAhPSAiY3JlYXRlIjoKICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJvcigidW5zdXBwb3J0ZWQgZGlzcGxheSBhY3Rpb24iKQog -ICAgICAgIG51bWJlciA9IHNlbGYuY2F0YWxvZy5hbGxvY2F0ZShyZXF1ZXN0LmdldCgicmVxdWVzdCIpKQogICAgICAgIHNlbGYuc3RhcnQobnVtYmVyKS53 -YWl0KDU1KQogICAgICAgIHJlc3VsdCA9IHNlbGYuc25hcHNob3QoKQogICAgICAgIHJlc3VsdFsiY3JlYXRlZCJdID0gZiJkaXNwbGF5OntudW1iZXJ9Igog -ICAgICAgIGlmIHNlbGYuc3RhdGVzLmdldChudW1iZXIpICE9ICJydW5uaW5nIjoKICAgICAgICAgICAgcmVzdWx0WyJlcnJvciJdID0gImRpc3BsYXlfc3Rh -cnRfZmFpbGVkIgogICAgICAgIHJldHVybiByZXN1bHQKCgpkZWYgc2VydmUocnVudGltZSk6CiAgICBydW50aW1lID0gUGF0aChydW50aW1lKQogICAgcnVu -dGltZS5ta2Rpcihtb2RlPTBvNzAwLCBwYXJlbnRzPVRydWUsIGV4aXN0X29rPVRydWUpCiAgICB3aXRoIChydW50aW1lIC8gImRpc3BsYXktY29udHJvbC5s -b2NrIikub3BlbigidyIpIGFzIGxvY2s6CiAgICAgICAgdHJ5OgogICAgICAgICAgICBmY250bC5mbG9jayhsb2NrLCBmY250bC5MT0NLX0VYIHwgZmNudGwu -TE9DS19OQikKICAgICAgICBleGNlcHQgQmxvY2tpbmdJT0Vycm9yOgogICAgICAgICAgICByZXR1cm4KICAgICAgICBpZiBub3QgYWxsKHNodXRpbC53aGlj -aChuYW1lKSBmb3IgbmFtZSBpbiAoIndlYnNvY2tpZnkiLCAib3BlbmJveCIsICJkYnVzLWxhdW5jaCIpKToKICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJv -cigiZGlzcGxheSBydW50aW1lIHVuYXZhaWxhYmxlIikKICAgICAgICBpZiBub3QgKHNodXRpbC53aGljaCgiWHZuYyIpIG9yIHNodXRpbC53aGljaCgiWHRp -Z2Vydm5jIikpOgogICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJkaXNwbGF5IHJ1bnRpbWUgdW5hdmFpbGFibGUiKQogICAgICAgIHNlcnZpY2UgPSBE -aXNwbGF5U2VydmljZShEaXNwbGF5Q2F0YWxvZyhQYXRoLmhvbWUoKSAvICIuY211eCIgLyAiZGlzcGxheXMiKSwgcnVudGltZSAvICJkaXNwbGF5cyIpCiAg -ICAgICAgZm9yIG51bWJlciBpbiBzZXJ2aWNlLmNhdGFsb2cubnVtYmVycygpOgogICAgICAgICAgICBzZXJ2aWNlLnN0YXJ0KG51bWJlcikKICAgICAgICBw -YXRoID0gcnVudGltZSAvICJkaXNwbGF5LWNvbnRyb2wuc29jayIKICAgICAgICBpZiBwYXRoLmV4aXN0cygpOgogICAgICAgICAgICBwYXRoLnVubGluaygp -CgogICAgICAgIGNsYXNzIEhhbmRsZXIoc29ja2V0c2VydmVyLlN0cmVhbVJlcXVlc3RIYW5kbGVyKToKICAgICAgICAgICAgZGVmIGhhbmRsZShzZWxmKToK -ICAgICAgICAgICAgICAgIHNlbGYuY29ubmVjdGlvbi5zZXR0aW1lb3V0KDYwKQogICAgICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgIHJh -dyA9IHNlbGYucmZpbGUucmVhZGxpbmUoNDA5NykKICAgICAgICAgICAgICAgICAgICBpZiBsZW4ocmF3KSA+IDQwOTY6CiAgICAgICAgICAgICAgICAgICAg -ICAgIHJhaXNlIFZhbHVlRXJyb3IoImRpc3BsYXkgcmVxdWVzdCB0b28gbGFyZ2UiKQogICAgICAgICAgICAgICAgICAgIHJlc3VsdCA9IHNlcnZpY2UuaGFu -ZGxlKGpzb24ubG9hZHMocmF3KSkKICAgICAgICAgICAgICAgIGV4Y2VwdCAoVmFsdWVFcnJvciwgS2V5RXJyb3IsIFR5cGVFcnJvciwgT1NFcnJvcik6CiAg -ICAgICAgICAgICAgICAgICAgcmVzdWx0ID0geyJlcnJvciI6ICJkaXNwbGF5X3JlcXVlc3RfZmFpbGVkIn0KICAgICAgICAgICAgICAgIHRyeToKICAgICAg -ICAgICAgICAgICAgICBzZWxmLndmaWxlLndyaXRlKGpzb24uZHVtcHMocmVzdWx0KS5lbmNvZGUoKSArIGIiXG4iKQogICAgICAgICAgICAgICAgZXhjZXB0 -IE9TRXJyb3I6CiAgICAgICAgICAgICAgICAgICAgcGFzcyAgIyBUaGUgcGVyc2lzdGVkIHJlY2VpcHQgcmVtYWlucyByZXBsYXlhYmxlIGFmdGVyIGRpc2Nv -bm5lY3QuCgogICAgICAgIGNsYXNzIFNlcnZlcihzb2NrZXRzZXJ2ZXIuVGhyZWFkaW5nVW5peFN0cmVhbVNlcnZlcik6CiAgICAgICAgICAgIGRhZW1vbl90 -aHJlYWRzID0gVHJ1ZQoKICAgICAgICBwcmV2aW91c191bWFzayA9IG9zLnVtYXNrKDBvMDc3KQogICAgICAgIHRyeToKICAgICAgICAgICAgd2l0aCBTZXJ2 -ZXIoc3RyKHBhdGgpLCBIYW5kbGVyKSBhcyBzZXJ2ZXI6CiAgICAgICAgICAgICAgICBvcy51bWFzayhwcmV2aW91c191bWFzaykKICAgICAgICAgICAgICAg -IHNlcnZlci5zZXJ2ZV9mb3JldmVyKCkKICAgICAgICBmaW5hbGx5OgogICAgICAgICAgICBvcy51bWFzayhwcmV2aW91c191bWFzaykKICAgICAgICAgICAg -c2VydmljZS5zaHV0ZG93bi5zZXQoKQogICAgICAgICAgICBwYXRoLnVubGluayhtaXNzaW5nX29rPVRydWUpCgoKZGVmIG1haW4oKToKICAgIHBhcnNlciA9 -IGFyZ3BhcnNlLkFyZ3VtZW50UGFyc2VyKGRlc2NyaXB0aW9uPV9fZG9jX18pCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50KCJhY3Rpb24iLCBjaG9pY2VzPVsi -bGlzdCIsICJjcmVhdGUiLCAic2VydmUiXSkKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoIi0tcmVxdWVzdC1pZCIpCiAgICBhcmdzID0gcGFyc2VyLnBhcnNl -X2FyZ3MoKQogICAgcnVudGltZSA9IG9zLmVudmlyb24uZ2V0KCJDTVVYX0RFU0tUT1BfUlVOVElNRV9ESVIiLCAiL3J1bi9jbXV4LWRlc2t0b3AiKQogICAg -aWYgYXJncy5hY3Rpb24gPT0gInNlcnZlIjoKICAgICAgICBzZXJ2ZShydW50aW1lKQogICAgICAgIHJldHVybiAwCiAgICBpZiBhcmdzLmFjdGlvbiA9PSAi -Y3JlYXRlIiBhbmQgbm90IGFyZ3MucmVxdWVzdF9pZDoKICAgICAgICBwYXJzZXIuZXJyb3IoImNyZWF0ZSByZXF1aXJlcyAtLXJlcXVlc3QtaWQgKHJldGFp -biBpdCB3aGVuIHJldHJ5aW5nKSIpCiAgICB0cnk6CiAgICAgICAgd2l0aCBzb2NrZXQuc29ja2V0KHNvY2tldC5BRl9VTklYLCBzb2NrZXQuU09DS19TVFJF -QU0pIGFzIGNsaWVudDoKICAgICAgICAgICAgY2xpZW50LnNldHRpbWVvdXQoNjApCiAgICAgICAgICAgIGNsaWVudC5jb25uZWN0KHN0cihQYXRoKHJ1bnRp -bWUpIC8gImRpc3BsYXktY29udHJvbC5zb2NrIikpCiAgICAgICAgICAgIGNsaWVudC5zZW5kYWxsKGpzb24uZHVtcHMoeyJhY3Rpb24iOiBhcmdzLmFjdGlv -biwgInJlcXVlc3QiOiBhcmdzLnJlcXVlc3RfaWR9KS5lbmNvZGUoKSArIGIiXG4iKQogICAgICAgICAgICB3aXRoIGNsaWVudC5tYWtlZmlsZSgicmIiKSBh -cyBzdHJlYW06CiAgICAgICAgICAgICAgICByZXN1bHQgPSBqc29uLmxvYWRzKHN0cmVhbS5yZWFkbGluZSg2NTUzNikpCiAgICBleGNlcHQgKE9TRXJyb3Is -IFZhbHVlRXJyb3IpOgogICAgICAgIHJlc3VsdCA9IHsiZXJyb3IiOiAiZGlzcGxheV9ydW50aW1lX3VuYXZhaWxhYmxlIn0KICAgIHByaW50KGpzb24uZHVt -cHMocmVzdWx0KSkKICAgIHJldHVybiAxIGlmICJlcnJvciIgaW4gcmVzdWx0IGVsc2UgMAoKCmlmIF9fbmFtZV9fID09ICJfX21haW5fXyI6CiAgICByYWlz -ZSBTeXN0ZW1FeGl0KG1haW4oKSkK +U0Vycm9yOgogICAgICAgIHJldHVybiBGYWxzZQoKCmRlZiByZmJfcmVhZHkobnVtYmVyKToKICAgICIiIkEgYm91bmQgSFRUUCBwb3J0IGFsb25lIGRvZXMg +bm90IHByb3ZlIHRoYXQgYW4gWC9SRkIgc2VydmVyIGV4aXN0cy4iIiIKICAgIHRyeToKICAgICAgICB3aXRoIHNvY2tldC5jcmVhdGVfY29ubmVjdGlvbigo +IjEyNy4wLjAuMSIsIDU5MDAgKyBudW1iZXIpLCB0aW1lb3V0PTAuNSkgYXMgcmZiOgogICAgICAgICAgICB2ZXJzaW9uID0gYiIiCiAgICAgICAgICAgIHdo +aWxlIGxlbih2ZXJzaW9uKSA8IDEyOgogICAgICAgICAgICAgICAgcGFydCA9IHJmYi5yZWN2KDEyIC0gbGVuKHZlcnNpb24pKQogICAgICAgICAgICAgICAg +aWYgbm90IHBhcnQ6CiAgICAgICAgICAgICAgICAgICAgcmV0dXJuIEZhbHNlCiAgICAgICAgICAgICAgICB2ZXJzaW9uICs9IHBhcnQKICAgICAgICAgICAg +aWYgbm90IHZlcnNpb24uc3RhcnRzd2l0aChiIlJGQiAiKToKICAgICAgICAgICAgICAgIHJldHVybiBGYWxzZQogICAgZXhjZXB0IChPU0Vycm9yLCBodHRw +LmNsaWVudC5IVFRQRXhjZXB0aW9uKToKICAgICAgICByZXR1cm4gRmFsc2UKICAgIHJldHVybiBUcnVlCgoKZGVmIG5vdm5jX3JlYWR5KG51bWJlcik6CiAg +ICB0cnk6CiAgICAgICAgY29ubmVjdGlvbiA9IGh0dHAuY2xpZW50LkhUVFBDb25uZWN0aW9uKCIxMjcuMC4wLjEiLCA2OTAwICsgbnVtYmVyLCB0aW1lb3V0 +PTAuNSkKICAgICAgICB0cnk6CiAgICAgICAgICAgIGNvbm5lY3Rpb24ucmVxdWVzdCgiSEVBRCIsICIvdm5jLmh0bWwiKQogICAgICAgICAgICByZXR1cm4g +Y29ubmVjdGlvbi5nZXRyZXNwb25zZSgpLnN0YXR1cyA9PSAyMDAKICAgICAgICBmaW5hbGx5OgogICAgICAgICAgICBjb25uZWN0aW9uLmNsb3NlKCkKICAg +IGV4Y2VwdCAoT1NFcnJvciwgaHR0cC5jbGllbnQuSFRUUEV4Y2VwdGlvbik6CiAgICAgICAgcmV0dXJuIEZhbHNlCgoKZGVmIHJlYWR5KG51bWJlcik6CiAg +ICAiIiJBIGJvdW5kIGVuZHBvaW50IGFsb25lIGRvZXMgbm90IHByb3ZlIHRoYXQgdGhlIGNvbXBsZXRlIGRlc2t0b3AgZXhpc3RzLiIiIgogICAgcmV0dXJu +IHJmYl9yZWFkeShudW1iZXIpIGFuZCBub3ZuY19yZWFkeShudW1iZXIpCgoKY2xhc3MgRGlzcGxheUNhdGFsb2c6CiAgICAiIiJTZXJpYWxpemVzIGFsbG9j +YXRpb24gYW5kIHBlcnNpc3RzIHJlcXVlc3QgcmVjZWlwdHMgYmVmb3JlIHN0YXJ0aW5nIHdvcmsuIiIiCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGRpcmVj +dG9yeSwgb2NjdXBpZWQ9Tm9uZSk6CiAgICAgICAgc2VsZi5kaXJlY3RvcnkgPSBQYXRoKGRpcmVjdG9yeSkKICAgICAgICBzZWxmLmRpcmVjdG9yeS5ta2Rp +cihtb2RlPTBvNzAwLCBwYXJlbnRzPVRydWUsIGV4aXN0X29rPVRydWUpCiAgICAgICAgc2VsZi5wYXRoID0gc2VsZi5kaXJlY3RvcnkgLyAiY2F0YWxvZy5q +c29uIgogICAgICAgIHNlbGYubG9jayA9IHRocmVhZGluZy5STG9jaygpCiAgICAgICAgc2VsZi5vY2N1cGllZCA9IG9jY3VwaWVkIG9yIChsYW1iZGEgbjog +UGF0aChmIi90bXAvLlgxMS11bml4L1h7bn0iKS5leGlzdHMoKQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgb3IgbGlzdGVuaW5nKDU5 +MDAgKyBuKSBvciBsaXN0ZW5pbmcoNjkwMCArIG4pKQogICAgICAgIHNlbGYucmVjb3JkcyA9IFtdCiAgICAgICAgaWYgc2VsZi5wYXRoLmV4aXN0cygpOgog +ICAgICAgICAgICBkYXRhID0ganNvbi5sb2FkcyhzZWxmLnBhdGgucmVhZF90ZXh0KCkpCiAgICAgICAgICAgIGlmIGRhdGEuZ2V0KCJ2ZXJzaW9uIikgIT0g +MToKICAgICAgICAgICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoInVuc3VwcG9ydGVkIGRpc3BsYXkgY2F0YWxvZyB2ZXJzaW9uIikKICAgICAgICAgICAgc2Vl +bl9udW1iZXJzLCBzZWVuX3JlcXVlc3RzID0gc2V0KCksIHNldCgpCiAgICAgICAgICAgIGZvciByZWNvcmQgaW4gZGF0YVsiZGlzcGxheXMiXToKICAgICAg +ICAgICAgICAgIG51bWJlciwgcmVxdWVzdCA9IHJlY29yZFsibnVtYmVyIl0sIHJlY29yZFsicmVxdWVzdCJdCiAgICAgICAgICAgICAgICBpZiB0eXBlKG51 +bWJlcikgaXMgbm90IGludCBvciBub3QgMiA8PSBudW1iZXIgPD0gTUFYX0RJU1BMQVlTOgogICAgICAgICAgICAgICAgICAgIHJhaXNlIFZhbHVlRXJyb3Io +ImludmFsaWQgZGlzcGxheSBudW1iZXIiKQogICAgICAgICAgICAgICAgaWYgc3RyKHV1aWQuVVVJRChyZXF1ZXN0KSkgIT0gcmVxdWVzdCBvciBudW1iZXIg +aW4gc2Vlbl9udW1iZXJzIG9yIHJlcXVlc3QgaW4gc2Vlbl9yZXF1ZXN0czoKICAgICAgICAgICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJpbnZhbGlk +IGRpc3BsYXkgaWRlbnRpdHkiKQogICAgICAgICAgICAgICAgc2Vlbl9udW1iZXJzLmFkZChudW1iZXIpCiAgICAgICAgICAgICAgICBzZWVuX3JlcXVlc3Rz +LmFkZChyZXF1ZXN0KQogICAgICAgICAgICAgICAgc2VsZi5yZWNvcmRzLmFwcGVuZCh7Im51bWJlciI6IG51bWJlciwgInJlcXVlc3QiOiByZXF1ZXN0fSkK +CiAgICBkZWYgbnVtYmVycyhzZWxmKToKICAgICAgICB3aXRoIHNlbGYubG9jazoKICAgICAgICAgICAgcmV0dXJuIHNvcnRlZChyZWNvcmRbIm51bWJlciJd +IGZvciByZWNvcmQgaW4gc2VsZi5yZWNvcmRzKQoKICAgIGRlZiBhbGxvY2F0ZShzZWxmLCByZXF1ZXN0KToKICAgICAgICBpZiBub3QgaXNpbnN0YW5jZShy +ZXF1ZXN0LCBzdHIpIG9yIHN0cih1dWlkLlVVSUQocmVxdWVzdCkpICE9IHJlcXVlc3Q6CiAgICAgICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoImludmFsaWQg +cmVxdWVzdCBpZGVudGl0eSIpCiAgICAgICAgd2l0aCBzZWxmLmxvY2s6CiAgICAgICAgICAgIGZvciByZWNvcmQgaW4gc2VsZi5yZWNvcmRzOgogICAgICAg +ICAgICAgICAgaWYgcmVjb3JkWyJyZXF1ZXN0Il0gPT0gcmVxdWVzdDoKICAgICAgICAgICAgICAgICAgICByZXR1cm4gcmVjb3JkWyJudW1iZXIiXQogICAg +ICAgICAgICB1c2VkID0gc2V0KHNlbGYubnVtYmVycygpKQogICAgICAgICAgICBudW1iZXIgPSBuZXh0KChuIGZvciBuIGluIHJhbmdlKDIsIE1BWF9ESVNQ +TEFZUyArIDEpCiAgICAgICAgICAgICAgICAgICAgICAgICAgIGlmIG4gbm90IGluIHVzZWQgYW5kIG5vdCBzZWxmLm9jY3VwaWVkKG4pKSwgTm9uZSkKICAg +ICAgICAgICAgaWYgbnVtYmVyIGlzIE5vbmU6CiAgICAgICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJkaXNwbGF5IGNhcGFjaXR5IGV4aGF1c3RlZCIp +CiAgICAgICAgICAgIHJlY29yZHMgPSBzZWxmLnJlY29yZHMgKyBbeyJudW1iZXIiOiBudW1iZXIsICJyZXF1ZXN0IjogcmVxdWVzdH1dCiAgICAgICAgICAg +IGZkLCB0ZW1wb3JhcnkgPSB0ZW1wZmlsZS5ta3N0ZW1wKHByZWZpeD0iY2F0YWxvZy0iLCBkaXI9c2VsZi5kaXJlY3RvcnkpCiAgICAgICAgICAgIHRyeToK +ICAgICAgICAgICAgICAgIHdpdGggb3MuZmRvcGVuKGZkLCAidyIpIGFzIG91dHB1dDoKICAgICAgICAgICAgICAgICAgICBqc29uLmR1bXAoeyJ2ZXJzaW9u +IjogMSwgImRpc3BsYXlzIjogcmVjb3Jkc30sIG91dHB1dCkKICAgICAgICAgICAgICAgICAgICBvdXRwdXQuZmx1c2goKQogICAgICAgICAgICAgICAgICAg +IG9zLmZzeW5jKG91dHB1dC5maWxlbm8oKSkKICAgICAgICAgICAgICAgIG9zLnJlcGxhY2UodGVtcG9yYXJ5LCBzZWxmLnBhdGgpCiAgICAgICAgICAgIGZp +bmFsbHk6CiAgICAgICAgICAgICAgICBpZiBvcy5wYXRoLmV4aXN0cyh0ZW1wb3JhcnkpOgogICAgICAgICAgICAgICAgICAgIG9zLnVubGluayh0ZW1wb3Jh +cnkpCiAgICAgICAgICAgIHNlbGYucmVjb3JkcyA9IHJlY29yZHMKICAgICAgICAgICAgcmV0dXJuIG51bWJlcgoKCmNsYXNzIE93bmVkUHJvY2VzczoKICAg +ICIiIkEgaGFuZGxlIGZvciBhIGRhZW1vbiBzdGFydGVkIGJ5IGRidXMtbGF1bmNoIHJhdGhlciB0aGFuIGJ5IFBvcGVuLiIiIgoKICAgIGRlZiBfX2luaXRf +XyhzZWxmLCBwaWQpOgogICAgICAgIHNlbGYucGlkID0gcGlkCgogICAgZGVmIHRlcm1pbmF0ZShzZWxmKToKICAgICAgICB0cnk6CiAgICAgICAgICAgIG9z +LmtpbGwoc2VsZi5waWQsIDE1KQogICAgICAgIGV4Y2VwdCBPU0Vycm9yOgogICAgICAgICAgICBwYXNzCgogICAgZGVmIHBvbGwoc2VsZik6CiAgICAgICAg +dHJ5OgogICAgICAgICAgICBvcy5raWxsKHNlbGYucGlkLCAwKQogICAgICAgICAgICByZXR1cm4gTm9uZQogICAgICAgIGV4Y2VwdCBPU0Vycm9yOgogICAg +ICAgICAgICByZXR1cm4gMAoKCmNsYXNzIE5vVk5DVW5hdmFpbGFibGUoUnVudGltZUVycm9yKToKICAgICIiIlRoZSB2aWV3aW5nIHRyYW5zcG9ydCBmYWls +ZWQgd2hpbGUgdGhlIGd1ZXN0IGRlc2t0b3AgbWF5IGJlIGhlYWx0aHkuIiIiCgoKY2xhc3MgRGlzcGxheVNlcnZpY2U6CiAgICBkZWYgX19pbml0X18oc2Vs +ZiwgY2F0YWxvZywgcnVudGltZSwgc3RhcnRlcj1TVEFSVEVSKToKICAgICAgICBzZWxmLmNhdGFsb2cgPSBjYXRhbG9nCiAgICAgICAgc2VsZi5ydW50aW1l +ID0gUGF0aChydW50aW1lKQogICAgICAgIHNlbGYuc3RhcnRlciA9IHN0YXJ0ZXIKICAgICAgICBzZWxmLmxvY2sgPSB0aHJlYWRpbmcuTG9jaygpCiAgICAg +ICAgc2VsZi5qb2JzID0ge30KICAgICAgICBzZWxmLnN0YXRlcyA9IHt9CiAgICAgICAgc2VsZi5wcm9jZXNzZXMgPSB7fQogICAgICAgIHNlbGYud2Vic29j +a2lmeV9wcm9jZXNzZXMgPSB7fQogICAgICAgIHNlbGYuc2h1dGRvd24gPSB0aHJlYWRpbmcuRXZlbnQoKQoKICAgIGRlZiBzdGFydChzZWxmLCBudW1iZXIp +OgogICAgICAgIHdpdGggc2VsZi5sb2NrOgogICAgICAgICAgICBpZiBudW1iZXIgaW4gc2VsZi5qb2JzOgogICAgICAgICAgICAgICAgcmV0dXJuIHNlbGYu +am9ic1tudW1iZXJdCiAgICAgICAgICAgIGZpbmlzaGVkID0gdGhyZWFkaW5nLkV2ZW50KCkKICAgICAgICAgICAgc2VsZi5qb2JzW251bWJlcl0gPSBmaW5p +c2hlZAogICAgICAgICAgICBzZWxmLnN0YXRlc1tudW1iZXJdID0gImxhdW5jaGluZyIKICAgICAgICAgICAgdGhyZWFkaW5nLlRocmVhZCh0YXJnZXQ9c2Vs +Zi5zdXBlcnZpc2UsIGFyZ3M9KG51bWJlciwgZmluaXNoZWQpLCBkYWVtb249VHJ1ZSkuc3RhcnQoKQogICAgICAgICAgICByZXR1cm4gZmluaXNoZWQKCiAg +ICBkZWYgdGVybWluYXRlKHNlbGYsIG51bWJlcik6CiAgICAgICAgZm9yIHByb2Nlc3MgaW4gc2VsZi5wcm9jZXNzZXMucG9wKG51bWJlciwgW10pOgogICAg +ICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICBwcm9jZXNzLnRlcm1pbmF0ZSgpCiAgICAgICAgICAgIGV4Y2VwdCBPU0Vycm9yOgogICAgICAgICAgICAg +ICAgcGFzcwogICAgICAgIHNlbGYudGVybWluYXRlX3dlYnNvY2tpZnkobnVtYmVyKQoKICAgIGRlZiB0ZXJtaW5hdGVfd2Vic29ja2lmeShzZWxmLCBudW1i +ZXIpOgogICAgICAgIHByb2Nlc3MgPSBzZWxmLndlYnNvY2tpZnlfcHJvY2Vzc2VzLnBvcChudW1iZXIsIE5vbmUpCiAgICAgICAgaWYgcHJvY2VzcyBpcyBO +b25lOgogICAgICAgICAgICByZXR1cm4KICAgICAgICB0cnk6CiAgICAgICAgICAgIHByb2Nlc3MudGVybWluYXRlKCkKICAgICAgICBleGNlcHQgT1NFcnJv +cjoKICAgICAgICAgICAgcGFzcwoKICAgIEBzdGF0aWNtZXRob2QKICAgIGRlZiB3YWl0X2Zvcl9wb3J0KHBvcnQsIHRpbWVvdXQ9MjApOgogICAgICAgIGRl +YWRsaW5lID0gdGltZS5tb25vdG9uaWMoKSArIHRpbWVvdXQKICAgICAgICB3aGlsZSB0aW1lLm1vbm90b25pYygpIDwgZGVhZGxpbmU6CiAgICAgICAgICAg +IGlmIGxpc3RlbmluZyhwb3J0KToKICAgICAgICAgICAgICAgIHJldHVybiBUcnVlCiAgICAgICAgICAgIHRpbWUuc2xlZXAoMC4xKQogICAgICAgIHJldHVy +biBGYWxzZQoKICAgIGRlZiBzdGFydF93ZWJzb2NraWZ5KHNlbGYsIG51bWJlciwgZW52aXJvbm1lbnQsIGV4ZWN1dGFibGUpOgogICAgICAgIHBhdGggPSBl +eGVjdXRhYmxlIGlmICIvIiBpbiBleGVjdXRhYmxlIGVsc2Ugc2h1dGlsLndoaWNoKGV4ZWN1dGFibGUpCiAgICAgICAgaWYgbm90IHBhdGg6CiAgICAgICAg +ICAgIHJhaXNlIE5vVk5DVW5hdmFpbGFibGUoIm1pc3NpbmcgZGlzcGxheSBjb21wb25lbnQ6IHdlYnNvY2tpZnkiKQogICAgICAgIHNlbGYudGVybWluYXRl +X3dlYnNvY2tpZnkobnVtYmVyKQogICAgICAgIHJmYl9wb3J0LCBub3ZuY19wb3J0ID0gNTkwMCArIG51bWJlciwgNjkwMCArIG51bWJlcgogICAgICAgIHBy +b2Nlc3MgPSBzdWJwcm9jZXNzLlBvcGVuKAogICAgICAgICAgICBbcGF0aCwgIi0td2ViIiwgIi91c3Ivc2hhcmUvbm92bmMiLCAiLS1oZWFydGJlYXQiLCAi +MzAiLAogICAgICAgICAgICAgZiIxMjcuMC4wLjE6e25vdm5jX3BvcnR9IiwgZiIxMjcuMC4wLjE6e3JmYl9wb3J0fSJdLAogICAgICAgICAgICBlbnY9ZW52 +aXJvbm1lbnQsIHN0ZGluPXN1YnByb2Nlc3MuREVWTlVMTCwKICAgICAgICAgICAgc3Rkb3V0PXN1YnByb2Nlc3MuREVWTlVMTCwgc3RkZXJyPXN1YnByb2Nl +c3MuREVWTlVMTCkKICAgICAgICBzZWxmLndlYnNvY2tpZnlfcHJvY2Vzc2VzW251bWJlcl0gPSBwcm9jZXNzCiAgICAgICAgaWYgbm90IHNlbGYud2FpdF9m +b3JfcG9ydChub3ZuY19wb3J0LCB0aW1lb3V0PTEwKToKICAgICAgICAgICAgc2VsZi50ZXJtaW5hdGVfd2Vic29ja2lmeShudW1iZXIpCiAgICAgICAgICAg +IHJhaXNlIE5vVk5DVW5hdmFpbGFibGUoIm5vVk5DIGRpZCBub3QgYmVjb21lIHJlYWR5IikKCiAgICBkZWYgc3RhcnRfY29tcG9uZW50cyhzZWxmLCBudW1i +ZXIsIGVudmlyb25tZW50LCBydW50aW1lKToKICAgICAgICBpZiByZWFkeShudW1iZXIpOgogICAgICAgICAgICByZXR1cm4KICAgICAgICByZmJfaXNfcmVh +ZHkgPSByZmJfcmVhZHkobnVtYmVyKQogICAgICAgIHN0YXJ0ZWRfZGVza3RvcCA9IEZhbHNlCiAgICAgICAgaWYgbm90IHJmYl9pc19yZWFkeToKICAgICAg +ICAgICAgc2VsZi50ZXJtaW5hdGUobnVtYmVyKQogICAgICAgICAgICBzdGFydGVkX2Rlc2t0b3AgPSBUcnVlCiAgICAgICAgdm5jID0gc2h1dGlsLndoaWNo +KCJYdm5jIikgb3Igc2h1dGlsLndoaWNoKCJYdGlnZXJ2bmMiKQogICAgICAgIHdlYnNvY2tpZnkgPSBzaHV0aWwud2hpY2goIndlYnNvY2tpZnkiKQogICAg +ICAgIGlmIG5vdCB3ZWJzb2NraWZ5OgogICAgICAgICAgICByYWlzZSBOb1ZOQ1VuYXZhaWxhYmxlKCJkaXNwbGF5IHJ1bnRpbWUgdW5hdmFpbGFibGUiKQog +ICAgICAgIGlmIG5vdCByZmJfaXNfcmVhZHkgYW5kIG5vdCB2bmM6CiAgICAgICAgICAgIHJhaXNlIFJ1bnRpbWVFcnJvcigiZGlzcGxheSBydW50aW1lIHVu +YXZhaWxhYmxlIikKICAgICAgICBpZiBzdGFydGVkX2Rlc2t0b3A6CiAgICAgICAgICAgIHJmYl9wb3J0ID0gNTkwMCArIG51bWJlcgogICAgICAgICAgICBw +cm9jZXNzZXMgPSBbXQogICAgICAgICAgICBwcm9jZXNzZXMuYXBwZW5kKHN1YnByb2Nlc3MuUG9wZW4oCiAgICAgICAgICAgICAgICBbdm5jLCBmIjp7bnVt +YmVyfSIsICItZ2VvbWV0cnkiLCBvcy5lbnZpcm9uLmdldCgiQ01VWF9WTkNfR0VPTUVUUlkiLCAiMTQ0MHg5MDAiKSwKICAgICAgICAgICAgICAgICAiLWRl +cHRoIiwgIjI0IiwgIi1yZmJwb3J0Iiwgc3RyKHJmYl9wb3J0KSwgIi1sb2NhbGhvc3QiLAogICAgICAgICAgICAgICAgICItU2VjdXJpdHlUeXBlcyIsICJO +b25lIiwgIi1BbHdheXNTaGFyZWQiXSwKICAgICAgICAgICAgICAgIGVudj1lbnZpcm9ubWVudCwgc3RkaW49c3VicHJvY2Vzcy5ERVZOVUxMLAogICAgICAg +ICAgICAgICAgc3Rkb3V0PXN1YnByb2Nlc3MuREVWTlVMTCwgc3RkZXJyPXN1YnByb2Nlc3MuREVWTlVMTCkpCiAgICAgICAgICAgIHNlbGYucHJvY2Vzc2Vz +W251bWJlcl0gPSBwcm9jZXNzZXMKICAgICAgICAgICAgaWYgbm90IHNlbGYud2FpdF9mb3JfcG9ydChyZmJfcG9ydCk6CiAgICAgICAgICAgICAgICByYWlz +ZSBSdW50aW1lRXJyb3IoIlh2bmMgZGlkIG5vdCBiZWNvbWUgcmVhZHkiKQogICAgICAgICAgICBidXMgPSBzdWJwcm9jZXNzLnJ1bihbImRidXMtbGF1bmNo +IiwgIi0tc2gtc3ludGF4Il0sIGVudj1lbnZpcm9ubWVudCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3RkaW49c3VicHJvY2Vzcy5ERVZO +VUxMLCBzdGRvdXQ9c3VicHJvY2Vzcy5QSVBFLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdGRlcnI9c3VicHJvY2Vzcy5ERVZOVUxMLCB0 +ZXh0PVRydWUsIHRpbWVvdXQ9MTAsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNoZWNrPVRydWUpLnN0ZG91dAogICAgICAgICAgICBmb3Ig +bGluZSBpbiBidXMuc3BsaXRsaW5lcygpOgogICAgICAgICAgICAgICAgaWYgIj0iIG5vdCBpbiBsaW5lOgogICAgICAgICAgICAgICAgICAgIGNvbnRpbnVl +CiAgICAgICAgICAgICAgICBrZXksIHZhbHVlID0gbGluZS5zcGxpdCgiPSIsIDEpCiAgICAgICAgICAgICAgICBlbnZpcm9ubWVudFtrZXldID0gdmFsdWUu +c3RyaXAoKS5yc3RyaXAoIjsiKS5zdHJpcCgpLnN0cmlwKCInIikKICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgZGJ1c19waWQgPSBpbnQoZW52 +aXJvbm1lbnQuZ2V0KCJEQlVTX1NFU1NJT05fQlVTX1BJRCIsICIwIikpCiAgICAgICAgICAgIGV4Y2VwdCBWYWx1ZUVycm9yOgogICAgICAgICAgICAgICAg +ZGJ1c19waWQgPSAwCiAgICAgICAgICAgIGlmIGRidXNfcGlkID4gMToKICAgICAgICAgICAgICAgIHByb2Nlc3Nlcy5hcHBlbmQoT3duZWRQcm9jZXNzKGRi +dXNfcGlkKSkKICAgICAgICAgICAgZm9yIGV4ZWN1dGFibGUsIGFyZ3MgaW4gWwogICAgICAgICAgICAgICAgKCJvcGVuYm94IiwgW10pLCAoInRpbnQyIiwg +WyItYyIsICIvZXRjL2NtdXgvdGludDJyYyJdKSwKICAgICAgICAgICAgICAgICgidm5jY29uZmlnIiwgWyItbm93aW4iXSksCiAgICAgICAgICAgIF06CiAg +ICAgICAgICAgICAgICBwYXRoID0gZXhlY3V0YWJsZSBpZiAiLyIgaW4gZXhlY3V0YWJsZSBlbHNlIHNodXRpbC53aGljaChleGVjdXRhYmxlKQogICAgICAg +ICAgICAgICAgaWYgbm90IHBhdGg6CiAgICAgICAgICAgICAgICAgICAgaWYgZXhlY3V0YWJsZSA9PSAidm5jY29uZmlnIjoKICAgICAgICAgICAgICAgICAg +ICAgICAgY29udGludWUKICAgICAgICAgICAgICAgICAgICByYWlzZSBSdW50aW1lRXJyb3IoZiJtaXNzaW5nIGRpc3BsYXkgY29tcG9uZW50OiB7ZXhlY3V0 +YWJsZX0iKQogICAgICAgICAgICAgICAgcHJvY2VzcyA9IHN1YnByb2Nlc3MuUG9wZW4oW3BhdGhdICsgYXJncywgZW52PWVudmlyb25tZW50LAogICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3RkaW49c3VicHJvY2Vzcy5ERVZOVUxMLAogICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgc3Rkb3V0PXN1YnByb2Nlc3MuREVWTlVMTCwgc3RkZXJyPXN1YnByb2Nlc3MuREVWTlVMTCkKICAgICAgICAgICAgICAgIHByb2Nl +c3Nlcy5hcHBlbmQocHJvY2VzcykKICAgICAgICBpZiBzdGFydGVkX2Rlc2t0b3Agb3Igbm90IG5vdm5jX3JlYWR5KG51bWJlcik6CiAgICAgICAgICAgIHNl +bGYuc3RhcnRfd2Vic29ja2lmeShudW1iZXIsIGVudmlyb25tZW50LCB3ZWJzb2NraWZ5KQogICAgICAgIChydW50aW1lIC8gImVudiIpLndyaXRlX3RleHQo +ZiJleHBvcnQgRElTUExBWT06e251bWJlcn1cbiIpCgogICAgZGVmIHN1cGVydmlzZShzZWxmLCBudW1iZXIsIGZpbmlzaGVkKToKICAgICAgICBydW50aW1l +ID0gc2VsZi5ydW50aW1lIC8gc3RyKG51bWJlcikKICAgICAgICBydW50aW1lLm1rZGlyKG1vZGU9MG83MDAsIHBhcmVudHM9VHJ1ZSwgZXhpc3Rfb2s9VHJ1 +ZSkKICAgICAgICBlbnZpcm9ubWVudCA9IGRpY3Qob3MuZW52aXJvbiwgRElTUExBWT1mIjp7bnVtYmVyfSIsIENNVVhfREVTS1RPUF9SVU5USU1FX0RJUj1z +dHIocnVudGltZSkpCiAgICAgICAgZW52aXJvbm1lbnRbIkNIUk9NRV9DT05GSUdfSE9NRSJdID0gc3RyKHJ1bnRpbWUgLyAiY2hyb21lLWNvbmZpZyIpCiAg +ICAgICAgZW52aXJvbm1lbnRbIlhER19DT05GSUdfSE9NRSJdID0gc3RyKHJ1bnRpbWUgLyAieGRnLWNvbmZpZyIpCiAgICAgICAgZm9yIGtleSBpbiAoIk5P +VElGWV9TT0NLRVQiLCAiREJVU19TRVNTSU9OX0JVU19BRERSRVNTIiwgIkRCVVNfU0VTU0lPTl9CVVNfUElEIiwKICAgICAgICAgICAgICAgICAgICAiQVRf +U1BJX0JVU19BRERSRVNTIiwgIkFUX1NQSV9CVVMiKToKICAgICAgICAgICAgZW52aXJvbm1lbnQucG9wKGtleSwgTm9uZSkKICAgICAgICB3aGlsZSBub3Qg +c2VsZi5zaHV0ZG93bi5pc19zZXQoKToKICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgIyBUaGUgc3VwZXJ2aXNvciBpcyBhIGhlYWx0aCBjaGVj +aywgbm90IGEgdGltZXItZHJpdmVuIHJlc3RhcnQuCiAgICAgICAgICAgICAgICAjIFByZXNlcnZlIGFuIGFjdGl2ZSBSRkIvbm9WTkMgc2Vzc2lvbiB1bnRp +bCBpdHMgb3duZWQgZW5kcG9pbnRzIGZhaWwuCiAgICAgICAgICAgICAgICBpZiBub3Qgc2VsZi5wcm9jZXNzZXMuZ2V0KG51bWJlcikgb3Igbm90IHJlYWR5 +KG51bWJlcik6CiAgICAgICAgICAgICAgICAgICAgc2VsZi5zdGFydF9jb21wb25lbnRzKG51bWJlciwgZW52aXJvbm1lbnQsIHJ1bnRpbWUpCiAgICAgICAg +ICAgICAgICBzdGF0ZSA9ICJydW5uaW5nIiBpZiByZWFkeShudW1iZXIpIGVsc2UgInVuYXZhaWxhYmxlIgogICAgICAgICAgICBleGNlcHQgTm9WTkNVbmF2 +YWlsYWJsZToKICAgICAgICAgICAgICAgIHNlbGYudGVybWluYXRlX3dlYnNvY2tpZnkobnVtYmVyKQogICAgICAgICAgICAgICAgc3RhdGUgPSAidW5hdmFp +bGFibGUiCiAgICAgICAgICAgIGV4Y2VwdCAoT1NFcnJvciwgUnVudGltZUVycm9yLCBzdWJwcm9jZXNzLlN1YnByb2Nlc3NFcnJvcik6CiAgICAgICAgICAg +ICAgICBzZWxmLnRlcm1pbmF0ZShudW1iZXIpCiAgICAgICAgICAgICAgICBzdGF0ZSA9ICJ1bmF2YWlsYWJsZSIKICAgICAgICAgICAgd2l0aCBzZWxmLmxv +Y2s6CiAgICAgICAgICAgICAgICBzZWxmLnN0YXRlc1tudW1iZXJdID0gc3RhdGUKICAgICAgICAgICAgZmluaXNoZWQuc2V0KCkKICAgICAgICAgICAgc2Vs +Zi5zaHV0ZG93bi53YWl0KDMwKQogICAgICAgIHNlbGYudGVybWluYXRlKG51bWJlcikKCiAgICBkZWYgc25hcHNob3Qoc2VsZik6CiAgICAgICAgd2l0aCBz +ZWxmLmxvY2s6CiAgICAgICAgICAgIHJldHVybiB7InZlcnNpb24iOiAxLCAiY2FuQ3JlYXRlIjogbGVuKHNlbGYuam9icykgPCBNQVhfRElTUExBWVMgLSAx +LAogICAgICAgICAgICAgICAgICAgICJkaXNwbGF5cyI6IFtkZXNjcmlwdG9yKDEsICJydW5uaW5nIiBpZiByZWFkeSgxKSBlbHNlICJ1bmF2YWlsYWJsZSIp +XQogICAgICAgICAgICAgICAgICAgICsgW2Rlc2NyaXB0b3Iobiwgc2VsZi5zdGF0ZXMuZ2V0KG4sICJsYXVuY2hpbmciKSkgZm9yIG4gaW4gc2VsZi5jYXRh +bG9nLm51bWJlcnMoKV19CgogICAgZGVmIGhhbmRsZShzZWxmLCByZXF1ZXN0KToKICAgICAgICBpZiByZXF1ZXN0LmdldCgiYWN0aW9uIikgPT0gImxpc3Qi +OgogICAgICAgICAgICByZXR1cm4gc2VsZi5zbmFwc2hvdCgpCiAgICAgICAgaWYgcmVxdWVzdC5nZXQoImFjdGlvbiIpICE9ICJjcmVhdGUiOgogICAgICAg +ICAgICByYWlzZSBWYWx1ZUVycm9yKCJ1bnN1cHBvcnRlZCBkaXNwbGF5IGFjdGlvbiIpCiAgICAgICAgbnVtYmVyID0gc2VsZi5jYXRhbG9nLmFsbG9jYXRl +KHJlcXVlc3QuZ2V0KCJyZXF1ZXN0IikpCiAgICAgICAgc2VsZi5zdGFydChudW1iZXIpLndhaXQoNTUpCiAgICAgICAgcmVzdWx0ID0gc2VsZi5zbmFwc2hv +dCgpCiAgICAgICAgcmVzdWx0WyJjcmVhdGVkIl0gPSBmImRpc3BsYXk6e251bWJlcn0iCiAgICAgICAgaWYgc2VsZi5zdGF0ZXMuZ2V0KG51bWJlcikgIT0g +InJ1bm5pbmciOgogICAgICAgICAgICByZXN1bHRbImVycm9yIl0gPSAiZGlzcGxheV9zdGFydF9mYWlsZWQiCiAgICAgICAgcmV0dXJuIHJlc3VsdAoKCmRl +ZiBzZXJ2ZShydW50aW1lKToKICAgIHJ1bnRpbWUgPSBQYXRoKHJ1bnRpbWUpCiAgICBydW50aW1lLm1rZGlyKG1vZGU9MG83MDAsIHBhcmVudHM9VHJ1ZSwg +ZXhpc3Rfb2s9VHJ1ZSkKICAgIHdpdGggKHJ1bnRpbWUgLyAiZGlzcGxheS1jb250cm9sLmxvY2siKS5vcGVuKCJ3IikgYXMgbG9jazoKICAgICAgICB0cnk6 +CiAgICAgICAgICAgIGZjbnRsLmZsb2NrKGxvY2ssIGZjbnRsLkxPQ0tfRVggfCBmY250bC5MT0NLX05CKQogICAgICAgIGV4Y2VwdCBCbG9ja2luZ0lPRXJy +b3I6CiAgICAgICAgICAgIHJldHVybgogICAgICAgIGlmIG5vdCBhbGwoc2h1dGlsLndoaWNoKG5hbWUpIGZvciBuYW1lIGluICgid2Vic29ja2lmeSIsICJv +cGVuYm94IiwgImRidXMtbGF1bmNoIikpOgogICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJkaXNwbGF5IHJ1bnRpbWUgdW5hdmFpbGFibGUiKQogICAg +ICAgIGlmIG5vdCAoc2h1dGlsLndoaWNoKCJYdm5jIikgb3Igc2h1dGlsLndoaWNoKCJYdGlnZXJ2bmMiKSk6CiAgICAgICAgICAgIHJhaXNlIFZhbHVlRXJy +b3IoImRpc3BsYXkgcnVudGltZSB1bmF2YWlsYWJsZSIpCiAgICAgICAgc2VydmljZSA9IERpc3BsYXlTZXJ2aWNlKERpc3BsYXlDYXRhbG9nKFBhdGguaG9t +ZSgpIC8gIi5jbXV4IiAvICJkaXNwbGF5cyIpLCBydW50aW1lIC8gImRpc3BsYXlzIikKICAgICAgICBmb3IgbnVtYmVyIGluIHNlcnZpY2UuY2F0YWxvZy5u +dW1iZXJzKCk6CiAgICAgICAgICAgIHNlcnZpY2Uuc3RhcnQobnVtYmVyKQogICAgICAgIHBhdGggPSBydW50aW1lIC8gImRpc3BsYXktY29udHJvbC5zb2Nr +IgogICAgICAgIGlmIHBhdGguZXhpc3RzKCk6CiAgICAgICAgICAgIHBhdGgudW5saW5rKCkKCiAgICAgICAgY2xhc3MgSGFuZGxlcihzb2NrZXRzZXJ2ZXIu +U3RyZWFtUmVxdWVzdEhhbmRsZXIpOgogICAgICAgICAgICBkZWYgaGFuZGxlKHNlbGYpOgogICAgICAgICAgICAgICAgc2VsZi5jb25uZWN0aW9uLnNldHRp +bWVvdXQoNjApCiAgICAgICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICAgICAgcmF3ID0gc2VsZi5yZmlsZS5yZWFkbGluZSg0MDk3KQogICAgICAg +ICAgICAgICAgICAgIGlmIGxlbihyYXcpID4gNDA5NjoKICAgICAgICAgICAgICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJvcigiZGlzcGxheSByZXF1ZXN0 +IHRvbyBsYXJnZSIpCiAgICAgICAgICAgICAgICAgICAgcmVzdWx0ID0gc2VydmljZS5oYW5kbGUoanNvbi5sb2FkcyhyYXcpKQogICAgICAgICAgICAgICAg +ZXhjZXB0IChWYWx1ZUVycm9yLCBLZXlFcnJvciwgVHlwZUVycm9yLCBPU0Vycm9yKToKICAgICAgICAgICAgICAgICAgICByZXN1bHQgPSB7ImVycm9yIjog +ImRpc3BsYXlfcmVxdWVzdF9mYWlsZWQifQogICAgICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgIHNlbGYud2ZpbGUud3JpdGUoanNvbi5k +dW1wcyhyZXN1bHQpLmVuY29kZSgpICsgYiJcbiIpCiAgICAgICAgICAgICAgICBleGNlcHQgT1NFcnJvcjoKICAgICAgICAgICAgICAgICAgICBwYXNzICAj +IFRoZSBwZXJzaXN0ZWQgcmVjZWlwdCByZW1haW5zIHJlcGxheWFibGUgYWZ0ZXIgZGlzY29ubmVjdC4KCiAgICAgICAgY2xhc3MgU2VydmVyKHNvY2tldHNl +cnZlci5UaHJlYWRpbmdVbml4U3RyZWFtU2VydmVyKToKICAgICAgICAgICAgZGFlbW9uX3RocmVhZHMgPSBUcnVlCgogICAgICAgIHByZXZpb3VzX3VtYXNr +ID0gb3MudW1hc2soMG8wNzcpCiAgICAgICAgdHJ5OgogICAgICAgICAgICB3aXRoIFNlcnZlcihzdHIocGF0aCksIEhhbmRsZXIpIGFzIHNlcnZlcjoKICAg +ICAgICAgICAgICAgIG9zLnVtYXNrKHByZXZpb3VzX3VtYXNrKQogICAgICAgICAgICAgICAgc2VydmVyLnNlcnZlX2ZvcmV2ZXIoKQogICAgICAgIGZpbmFs +bHk6CiAgICAgICAgICAgIG9zLnVtYXNrKHByZXZpb3VzX3VtYXNrKQogICAgICAgICAgICBzZXJ2aWNlLnNodXRkb3duLnNldCgpCiAgICAgICAgICAgIHBh +dGgudW5saW5rKG1pc3Npbmdfb2s9VHJ1ZSkKCgpkZWYgbWFpbigpOgogICAgcGFyc2VyID0gYXJncGFyc2UuQXJndW1lbnRQYXJzZXIoZGVzY3JpcHRpb249 +X19kb2NfXykKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoImFjdGlvbiIsIGNob2ljZXM9WyJsaXN0IiwgImNyZWF0ZSIsICJzZXJ2ZSJdKQogICAgcGFyc2Vy +LmFkZF9hcmd1bWVudCgiLS1yZXF1ZXN0LWlkIikKICAgIGFyZ3MgPSBwYXJzZXIucGFyc2VfYXJncygpCiAgICBydW50aW1lID0gb3MuZW52aXJvbi5nZXQo +IkNNVVhfREVTS1RPUF9SVU5USU1FX0RJUiIsICIvcnVuL2NtdXgtZGVza3RvcCIpCiAgICBpZiBhcmdzLmFjdGlvbiA9PSAic2VydmUiOgogICAgICAgIHNl +cnZlKHJ1bnRpbWUpCiAgICAgICAgcmV0dXJuIDAKICAgIGlmIGFyZ3MuYWN0aW9uID09ICJjcmVhdGUiIGFuZCBub3QgYXJncy5yZXF1ZXN0X2lkOgogICAg +ICAgIHBhcnNlci5lcnJvcigiY3JlYXRlIHJlcXVpcmVzIC0tcmVxdWVzdC1pZCAocmV0YWluIGl0IHdoZW4gcmV0cnlpbmcpIikKICAgIHRyeToKICAgICAg +ICB3aXRoIHNvY2tldC5zb2NrZXQoc29ja2V0LkFGX1VOSVgsIHNvY2tldC5TT0NLX1NUUkVBTSkgYXMgY2xpZW50OgogICAgICAgICAgICBjbGllbnQuc2V0 +dGltZW91dCg2MCkKICAgICAgICAgICAgY2xpZW50LmNvbm5lY3Qoc3RyKFBhdGgocnVudGltZSkgLyAiZGlzcGxheS1jb250cm9sLnNvY2siKSkKICAgICAg +ICAgICAgY2xpZW50LnNlbmRhbGwoanNvbi5kdW1wcyh7ImFjdGlvbiI6IGFyZ3MuYWN0aW9uLCAicmVxdWVzdCI6IGFyZ3MucmVxdWVzdF9pZH0pLmVuY29k +ZSgpICsgYiJcbiIpCiAgICAgICAgICAgIHdpdGggY2xpZW50Lm1ha2VmaWxlKCJyYiIpIGFzIHN0cmVhbToKICAgICAgICAgICAgICAgIHJlc3VsdCA9IGpz +b24ubG9hZHMoc3RyZWFtLnJlYWRsaW5lKDY1NTM2KSkKICAgIGV4Y2VwdCAoT1NFcnJvciwgVmFsdWVFcnJvcik6CiAgICAgICAgcmVzdWx0ID0geyJlcnJv +ciI6ICJkaXNwbGF5X3J1bnRpbWVfdW5hdmFpbGFibGUifQogICAgcHJpbnQoanNvbi5kdW1wcyhyZXN1bHQpKQogICAgcmV0dXJuIDEgaWYgImVycm9yIiBp +biByZXN1bHQgZWxzZSAwCgoKaWYgX19uYW1lX18gPT0gIl9fbWFpbl9fIjoKICAgIHJhaXNlIFN5c3RlbUV4aXQobWFpbigpKQo= """ private static let path = "$HOME/.cmux/cmux-display" diff --git a/Sources/Cloud/PortForward/CloudBrowserAccessState.swift b/Sources/Cloud/PortForward/CloudBrowserAccessState.swift index e6ed323f71ff..c8d5b12cbdea 100644 --- a/Sources/Cloud/PortForward/CloudBrowserAccessState.swift +++ b/Sources/Cloud/PortForward/CloudBrowserAccessState.swift @@ -74,7 +74,9 @@ final class CloudBrowserAccessState { } func showUnavailable(_ message: String) { + let retainedResource = resourceID leave() + resourceID = retainedResource unavailable = message } @@ -97,6 +99,14 @@ final class CloudBrowserAccessState { return nil } + /// A failed Cloud placeholder still owns its resource. A browser that has + /// deliberately navigated away has called `leave()` and must duplicate its + /// current page as an ordinary browser instead of resurrecting that stale + /// Cloud projection. + var retainsCloudResourceForDuplication: Bool { + model != nil || resourceID?.machine.isLocal == false + } + var showsFailureAlert: Bool { failureMessage.map { $0 != dismissedFailure } ?? false } diff --git a/Sources/DockSplitStore+BrowserActions.swift b/Sources/DockSplitStore+BrowserActions.swift index 5e16039ab189..bbee5241a640 100644 --- a/Sources/DockSplitStore+BrowserActions.swift +++ b/Sources/DockSplitStore+BrowserActions.swift @@ -16,8 +16,11 @@ extension DockSplitStore { } let tabs = bonsplitController.tabs(inPane: paneId) let catalog = SurfaceCatalog.shared - let record = catalog.projectionRecord(forPanel: panelId).flatMap { $0.resource.machine.isLocal ? nil : $0 } - let resource = record?.resource ?? browser.cloudAccess.resourceID + let keepsCloudRoute = browser.cloudAccess.retainsCloudResourceForDuplication + let record = keepsCloudRoute + ? catalog.projectionRecord(forPanel: panelId).flatMap { $0.resource.machine.isLocal ? nil : $0 } + : nil + let resource = keepsCloudRoute ? (record?.resource ?? browser.cloudAccess.resourceID) : nil let isCloud = resource?.machine.isLocal == false guard surfaceOwnershipPolicy.rejection(for: machineOwningSurface(panelId)) == nil else { return nil } guard let anchorIndex = tabs.firstIndex(where: { diff --git a/Sources/Surfaces/Workspace+BrowserDuplication.swift b/Sources/Surfaces/Workspace+BrowserDuplication.swift index 5fd9eed297a9..9fe0a8d1949d 100644 --- a/Sources/Surfaces/Workspace+BrowserDuplication.swift +++ b/Sources/Surfaces/Workspace+BrowserDuplication.swift @@ -8,8 +8,11 @@ extension Workspace { let paneId = paneId(forPanelId: panelId), let browser = browserPanel(for: panelId) else { return nil } let catalog = SurfaceCatalog.shared - let record = catalog.projectionRecord(forPanel: panelId).flatMap { $0.resource.machine.isLocal ? nil : $0 } - let resource = record?.resource ?? browser.cloudAccess.resourceID + let keepsCloudRoute = browser.cloudAccess.retainsCloudResourceForDuplication + let record = keepsCloudRoute + ? catalog.projectionRecord(forPanel: panelId).flatMap { $0.resource.machine.isLocal ? nil : $0 } + : nil + let resource = keepsCloudRoute ? (record?.resource ?? browser.cloudAccess.resourceID) : nil guard surfaceOwnershipPolicy.rejection(for: machineOwningSurface(panelId)) == nil else { return nil } let isCloud = resource?.machine.isLocal == false let targetIndex = insertionIndexToRight(of: anchorTabId, inPane: paneId) diff --git a/tests/fixtures/cmux-display b/tests/fixtures/cmux-display index 2e0df79a8959..b8a087d4e348 100755 --- a/tests/fixtures/cmux-display +++ b/tests/fixtures/cmux-display @@ -37,7 +37,7 @@ def listening(port): return False -def ready(number): +def rfb_ready(number): """A bound HTTP port alone does not prove that an X/RFB server exists.""" try: with socket.create_connection(("127.0.0.1", 5900 + number), timeout=0.5) as rfb: @@ -49,6 +49,13 @@ def ready(number): version += part if not version.startswith(b"RFB "): return False + except (OSError, http.client.HTTPException): + return False + return True + + +def novnc_ready(number): + try: connection = http.client.HTTPConnection("127.0.0.1", 6900 + number, timeout=0.5) try: connection.request("HEAD", "/vnc.html") @@ -59,6 +66,11 @@ def ready(number): return False +def ready(number): + """A bound endpoint alone does not prove that the complete desktop exists.""" + return rfb_ready(number) and novnc_ready(number) + + class DisplayCatalog: """Serializes allocation and persists request receipts before starting work.""" @@ -136,6 +148,10 @@ class OwnedProcess: return 0 +class NoVNCUnavailable(RuntimeError): + """The viewing transport failed while the guest desktop may be healthy.""" + + class DisplayService: def __init__(self, catalog, runtime, starter=STARTER): self.catalog = catalog @@ -145,6 +161,7 @@ class DisplayService: self.jobs = {} self.states = {} self.processes = {} + self.websockify_processes = {} self.shutdown = threading.Event() def start(self, number): @@ -163,6 +180,16 @@ class DisplayService: process.terminate() except OSError: pass + self.terminate_websockify(number) + + def terminate_websockify(self, number): + process = self.websockify_processes.pop(number, None) + if process is None: + return + try: + process.terminate() + except OSError: + pass @staticmethod def wait_for_port(port, timeout=20): @@ -173,57 +200,78 @@ class DisplayService: time.sleep(0.1) return False + def start_websockify(self, number, environment, executable): + path = executable if "/" in executable else shutil.which(executable) + if not path: + raise NoVNCUnavailable("missing display component: websockify") + self.terminate_websockify(number) + rfb_port, novnc_port = 5900 + number, 6900 + number + process = subprocess.Popen( + [path, "--web", "/usr/share/novnc", "--heartbeat", "30", + f"127.0.0.1:{novnc_port}", f"127.0.0.1:{rfb_port}"], + env=environment, stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + self.websockify_processes[number] = process + if not self.wait_for_port(novnc_port, timeout=10): + self.terminate_websockify(number) + raise NoVNCUnavailable("noVNC did not become ready") + def start_components(self, number, environment, runtime): if ready(number): return - self.terminate(number) + rfb_is_ready = rfb_ready(number) + started_desktop = False + if not rfb_is_ready: + self.terminate(number) + started_desktop = True vnc = shutil.which("Xvnc") or shutil.which("Xtigervnc") websockify = shutil.which("websockify") - if not vnc or not websockify: + if not websockify: + raise NoVNCUnavailable("display runtime unavailable") + if not rfb_is_ready and not vnc: raise RuntimeError("display runtime unavailable") - rfb_port, novnc_port = 5900 + number, 6900 + number - processes = [] - processes.append(subprocess.Popen( - [vnc, f":{number}", "-geometry", os.environ.get("CMUX_VNC_GEOMETRY", "1440x900"), - "-depth", "24", "-rfbport", str(rfb_port), "-localhost", - "-SecurityTypes", "None", "-AlwaysShared"], - env=environment, stdin=subprocess.DEVNULL, - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)) - self.processes[number] = processes - if not self.wait_for_port(rfb_port): - raise RuntimeError("Xvnc did not become ready") - bus = subprocess.run(["dbus-launch", "--sh-syntax"], env=environment, - stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, text=True, timeout=10, - check=True).stdout - for line in bus.splitlines(): - if "=" not in line: - continue - key, value = line.split("=", 1) - environment[key] = value.strip().rstrip(";").strip().strip("'") - try: - dbus_pid = int(environment.get("DBUS_SESSION_BUS_PID", "0")) - except ValueError: - dbus_pid = 0 - if dbus_pid > 1: - processes.append(OwnedProcess(dbus_pid)) - for executable, args in [ - ("openbox", []), ("tint2", ["-c", "/etc/cmux/tint2rc"]), - ("vncconfig", ["-nowin"]), - (websockify, ["--web", "/usr/share/novnc", "--heartbeat", "30", - f"127.0.0.1:{novnc_port}", f"127.0.0.1:{rfb_port}"]), - ]: - path = executable if "/" in executable else shutil.which(executable) - if not path: - if executable == "vncconfig": + if started_desktop: + rfb_port = 5900 + number + processes = [] + processes.append(subprocess.Popen( + [vnc, f":{number}", "-geometry", os.environ.get("CMUX_VNC_GEOMETRY", "1440x900"), + "-depth", "24", "-rfbport", str(rfb_port), "-localhost", + "-SecurityTypes", "None", "-AlwaysShared"], + env=environment, stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)) + self.processes[number] = processes + if not self.wait_for_port(rfb_port): + raise RuntimeError("Xvnc did not become ready") + bus = subprocess.run(["dbus-launch", "--sh-syntax"], env=environment, + stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, text=True, timeout=10, + check=True).stdout + for line in bus.splitlines(): + if "=" not in line: continue - raise RuntimeError(f"missing display component: {executable}") - process = subprocess.Popen([path] + args, env=environment, - stdin=subprocess.DEVNULL, - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - processes.append(process) - if not self.wait_for_port(novnc_port, timeout=10): - raise RuntimeError("noVNC did not become ready") + key, value = line.split("=", 1) + environment[key] = value.strip().rstrip(";").strip().strip("'") + try: + dbus_pid = int(environment.get("DBUS_SESSION_BUS_PID", "0")) + except ValueError: + dbus_pid = 0 + if dbus_pid > 1: + processes.append(OwnedProcess(dbus_pid)) + for executable, args in [ + ("openbox", []), ("tint2", ["-c", "/etc/cmux/tint2rc"]), + ("vncconfig", ["-nowin"]), + ]: + path = executable if "/" in executable else shutil.which(executable) + if not path: + if executable == "vncconfig": + continue + raise RuntimeError(f"missing display component: {executable}") + process = subprocess.Popen([path] + args, env=environment, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + processes.append(process) + if started_desktop or not novnc_ready(number): + self.start_websockify(number, environment, websockify) (runtime / "env").write_text(f"export DISPLAY=:{number}\n") def supervise(self, number, finished): @@ -242,6 +290,9 @@ class DisplayService: if not self.processes.get(number) or not ready(number): self.start_components(number, environment, runtime) state = "running" if ready(number) else "unavailable" + except NoVNCUnavailable: + self.terminate_websockify(number) + state = "unavailable" except (OSError, RuntimeError, subprocess.SubprocessError): self.terminate(number) state = "unavailable" From 7ebdd4f06682b7e111a9e7a41f30da530dfda88a Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 12:08:04 -0700 Subject: [PATCH 24/64] test: preserve display identity across browser reconfiguration --- cmuxTests/CloudDesktopAccessTests.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmuxTests/CloudDesktopAccessTests.swift b/cmuxTests/CloudDesktopAccessTests.swift index 33e653a0fbc8..d6d14f8a5fd9 100644 --- a/cmuxTests/CloudDesktopAccessTests.swift +++ b/cmuxTests/CloudDesktopAccessTests.swift @@ -123,6 +123,11 @@ struct CloudDesktopAccessTests { state.didCommit(url: url) state.desktopConnectionDidChange(url: url, isConnected: true) #expect(state.showsPage, "A live RFB connection is stronger evidence than document finish") + state.configure(model: model, url: remote) + #expect(state.resourceID?.key == "display:\(port == 6901 ? 1 : 2)", + "Rebinding an existing WebView preserves the display identity") + let reboundURL = try #require(state.nextURL()) + state.didCommit(url: reboundURL) state.desktopConnectionDidChange(url: url, isConnected: false) #expect(state.showsFailureAlert) state.desktopConnectionDidChange(url: url, isConnected: true) From 292fd36fc1b0e3ca6f250e3820058023d792ea85 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 12:08:11 -0700 Subject: [PATCH 25/64] fix: retain display identity across route reconfiguration --- Sources/Cloud/PortForward/CloudBrowserAccessState.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Sources/Cloud/PortForward/CloudBrowserAccessState.swift b/Sources/Cloud/PortForward/CloudBrowserAccessState.swift index c8d5b12cbdea..15fc87d37689 100644 --- a/Sources/Cloud/PortForward/CloudBrowserAccessState.swift +++ b/Sources/Cloud/PortForward/CloudBrowserAccessState.swift @@ -163,7 +163,13 @@ final class CloudBrowserAccessState { func configure(model: CloudPortAccessModel, url: URL, resourceID: SurfaceResourceID? = nil) { observationGeneration &+= 1 unavailable = nil - self.resourceID = resourceID + // WebView/profile replacement reconfigures the existing route without + // passing the identity again. Keep the stable display ID until an + // explicit replacement supplies a new one; callers that leave Cloud + // first still clear it deliberately. + if let resourceID { + self.resourceID = resourceID + } self.model = model remoteURL = url navigationURL = nil From b1f53ae54a681568cbb28dcf46e5508f97826a8f Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 12:27:13 -0700 Subject: [PATCH 26/64] test: cover route observation after display reconfiguration --- cmuxTests/CloudDesktopAccessTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/cmuxTests/CloudDesktopAccessTests.swift b/cmuxTests/CloudDesktopAccessTests.swift index d6d14f8a5fd9..41481e1cf097 100644 --- a/cmuxTests/CloudDesktopAccessTests.swift +++ b/cmuxTests/CloudDesktopAccessTests.swift @@ -29,6 +29,7 @@ struct CloudDesktopAccessTests { var navigations: [URL] = [] state.automaticallyNavigate { navigations.append($0) } model.connect() + state.configure(model: model, url: URL(string: "http://10.0.0.7:6901/vnc.html")!) #expect(navigations.isEmpty, "A slow route cannot invent a completed navigation") ready.resolve(true) #expect(await wait { navigations.count == 1 }) From 54fcdfb56b8a95c663a3939b80138b507e43278a Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 12:27:19 -0700 Subject: [PATCH 27/64] fix: retain Cloud restore lifecycle state --- Sources/Cloud/PortForward/CloudBrowserAccessState.swift | 4 ++++ Sources/Panels/BrowserPanel+CloudConnection.swift | 6 +++++- Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Sources/Cloud/PortForward/CloudBrowserAccessState.swift b/Sources/Cloud/PortForward/CloudBrowserAccessState.swift index 15fc87d37689..fbd4589a6ace 100644 --- a/Sources/Cloud/PortForward/CloudBrowserAccessState.swift +++ b/Sources/Cloud/PortForward/CloudBrowserAccessState.swift @@ -184,6 +184,10 @@ final class CloudBrowserAccessState { startDeadline() attempt += 1 trace("configured") + // Reconfiguration invalidates the previous observation generation. + // Re-arm it even when the same access model is reused by a WebView + // replacement that is still waiting for its route to become ready. + observeRoute() } func nextURL() -> URL? { diff --git a/Sources/Panels/BrowserPanel+CloudConnection.swift b/Sources/Panels/BrowserPanel+CloudConnection.swift index 9836e10b6da2..0c7ded46d056 100644 --- a/Sources/Panels/BrowserPanel+CloudConnection.swift +++ b/Sources/Panels/BrowserPanel+CloudConnection.swift @@ -3,8 +3,12 @@ import WebKit extension BrowserPanel { var cloudResourceForSession: SurfaceResourceID? { + guard cloudAccess.retainsCloudResourceForDuplication else { return nil } + if let resource = cloudAccess.resourceID, !resource.machine.isLocal { + return resource + } let resource = SurfaceCatalog.shared.projectionRecord(forPanel: id)?.resource - return resource?.machine.isLocal == false ? resource : cloudAccess.resourceID + return resource?.machine.isLocal == false ? resource : nil } /// Restore by stable resource identity before loading any saved address. diff --git a/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift b/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift index cd2766bb7298..f3c1c7226e1c 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift @@ -78,6 +78,7 @@ extension CmuxTuiSurfaceProvider { browser.showCloudAddress(privateURL) model.connect() browser.cloudAccess.routeDidConfigure() + materializedPanels.insert(browser.id) } func accessModel(port: Int, address: String, scheme: String = "http") -> CloudPortAccessModel { From 1c64ed84cd17cdb7b6e387ec8f3a054428590852 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 12:29:52 -0700 Subject: [PATCH 28/64] test: drop Cloud provenance after external navigation --- cmuxTests/CloudDesktopAccessTests.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/cmuxTests/CloudDesktopAccessTests.swift b/cmuxTests/CloudDesktopAccessTests.swift index 41481e1cf097..cf6afe621228 100644 --- a/cmuxTests/CloudDesktopAccessTests.swift +++ b/cmuxTests/CloudDesktopAccessTests.swift @@ -288,6 +288,19 @@ struct CloudDesktopAccessTests { #expect(state.resourceID == nil) } + @Test("Leaving Cloud for an external page drops stale session provenance") + func externalNavigationDropsCloudSessionResource() { + let browser = BrowserPanel(workspaceId: UUID(), websiteDataStore: .nonPersistent()) + defer { browser.close() } + let model = CloudPortAccessModel(target: .init(host: "10.0.0.7", port: 6902), coordinator: nil, + wake: {}, startForward: { _ in 46902 }, stopForward: {}, route: .loopback) + let display = SurfaceResourceID(machine: .cloud("a"), kind: .display, key: "display:2") + browser.cloudAccess.configure(model: model, url: URL(string: "http://10.0.0.7:6902/vnc.html")!, resourceID: display) + #expect(browser.cloudResourceForSession == display) + browser.leaveCloudResourceForLocalNavigation() + #expect(browser.cloudResourceForSession == nil) + } + @Test("A forwarded /vnc.html URL is not a display when its resource is a browser") func nonDisplayVNCPathDoesNotUseDesktopReadiness() { let state = CloudBrowserAccessState() From 7cda189e4eb352f812a8b9b284682bd38c1d3cb9 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 12:30:01 -0700 Subject: [PATCH 29/64] fix: clear Cloud provenance on external browser navigation --- Sources/Panels/BrowserPanel+CloudConnection.swift | 11 +++++++++++ Sources/Panels/BrowserPanel.swift | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Sources/Panels/BrowserPanel+CloudConnection.swift b/Sources/Panels/BrowserPanel+CloudConnection.swift index 0c7ded46d056..e751f21048f5 100644 --- a/Sources/Panels/BrowserPanel+CloudConnection.swift +++ b/Sources/Panels/BrowserPanel+CloudConnection.swift @@ -2,6 +2,17 @@ import Foundation import WebKit extension BrowserPanel { + /// Leaving a Cloud resource for a user-owned external page ends only this + /// local projection. The `.replaced` reason keeps a navigation from + /// editing the remote workspace layout while removing stale restore + /// provenance from this panel. + func leaveCloudResourceForLocalNavigation() { + if cloudAccess.retainsCloudResourceForDuplication { + SurfaceCatalog.shared.endProjections(panelID: id, reason: .replaced) + } + cloudAccess.leave() + } + var cloudResourceForSession: SurfaceResourceID? { guard cloudAccess.retainsCloudResourceForDuplication else { return nil } if let resource = cloudAccess.resourceID, !resource.machine.isLocal { diff --git a/Sources/Panels/BrowserPanel.swift b/Sources/Panels/BrowserPanel.swift index fed568668362..ac2ec6631d11 100644 --- a/Sources/Panels/BrowserPanel.swift +++ b/Sources/Panels/BrowserPanel.swift @@ -5406,7 +5406,7 @@ final class BrowserPanel: Panel, ObservableObject { provider.configureBrowser(self, url: url) return nil } else { - cloudAccess.leave() + leaveCloudResourceForLocalNavigation() } let request = URLRequest(url: url) let policy = BrowserURLAllowlistPolicy(defaults: .standard) From 1d185892234012cc8f3654a4c59bdea765fd38e6 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 12:42:54 -0700 Subject: [PATCH 30/64] test: cover display catalog and readiness cancellation --- cmuxTests/CloudDesktopAccessTests.swift | 2 +- cmuxTests/CloudDisplayCatalogTests.swift | 38 ++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/cmuxTests/CloudDesktopAccessTests.swift b/cmuxTests/CloudDesktopAccessTests.swift index cf6afe621228..b7fbd57e0f47 100644 --- a/cmuxTests/CloudDesktopAccessTests.swift +++ b/cmuxTests/CloudDesktopAccessTests.swift @@ -54,12 +54,12 @@ struct CloudDesktopAccessTests { wake: {}, startForward: { _ in 46901 }, stopForward: {}, route: .loopback) let state = CloudBrowserAccessState(clock: clock) state.configure(model: model, url: URL(string: "http://10.0.0.7:6901/vnc.html")!) - await clock.waitUntilSleeping() model.connect() #expect(await wait { model.isReady }) let url = try #require(state.nextURL()) state.didCommit(url: url) state.didFinish(url: url) + await clock.waitUntilSleeping() clock.advance(by: .seconds(46)) #expect(await wait { state.showsFailureAlert }) #expect(!state.desktopConnected) diff --git a/cmuxTests/CloudDisplayCatalogTests.swift b/cmuxTests/CloudDisplayCatalogTests.swift index 624e075be2a9..c82ea9491e2f 100644 --- a/cmuxTests/CloudDisplayCatalogTests.swift +++ b/cmuxTests/CloudDisplayCatalogTests.swift @@ -65,6 +65,44 @@ struct CloudDisplayCatalogTests { #expect(!service.canCreate && service.snapshot == nil) } + @Test("A failed refresh invalidates the cached guest catalog") + func failedRefreshClearsSnapshot() async { + var shouldFail = false + let service = CloudDisplayCoordinator { command, _ in + guard command.contains(" list") else { return .init(exitCode: 0, stdout: initial, stderr: "") } + if shouldFail { throw URLError(.networkConnectionLost) } + return .init(exitCode: 0, stdout: initial, stderr: "") + } + await service.refresh() + #expect(service.snapshot != nil && service.canCreate) + shouldFail = true + await service.refresh() + #expect(service.snapshot == nil && !service.canCreate) + } + + @Test("Cancelling display creation cancels the guest exec") + func cancelledCreationCancelsGuestExec() async { + let started = CloudLinkFirstValue() + let cancelled = CloudLinkFirstValue() + let service = CloudDisplayCoordinator { command, _ in + if command.contains(" list") { return .init(exitCode: 0, stdout: initial, stderr: "") } + started.resolve(true) + do { + try await Task.sleep(for: .seconds(60)) + } catch { + cancelled.resolve(true) + throw error + } + return .init(exitCode: 0, stdout: created, stderr: "") + } + await service.refresh() + let operation = Task { try await service.create() } + _ = await started.result + operation.cancel() + #expect(await cancelled.result == true) + do { _ = try await operation.value } catch {} + } + @Test("The existing Desktop cannot be reported as a newly created display") func rejectsDesktopCreationReceipt() { let raw = #"{"version":1,"canCreate":true,"displays":[{"id":"display:1","number":1,"port":6901,"state":"running"}],"created":"display:1"}"# From fbef08cb0010fd30a0b339b953a07c61e0d9aba1 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 12:43:03 -0700 Subject: [PATCH 31/64] fix: fence display catalog and readiness lifecycles --- Sources/Cloud/CloudDisplayCoordinator.swift | 15 +++++++++++++-- .../PortForward/CloudBrowserAccessState.swift | 10 +++------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/Sources/Cloud/CloudDisplayCoordinator.swift b/Sources/Cloud/CloudDisplayCoordinator.swift index f31f4ee027bf..d8b5775730c8 100644 --- a/Sources/Cloud/CloudDisplayCoordinator.swift +++ b/Sources/Cloud/CloudDisplayCoordinator.swift @@ -34,6 +34,7 @@ final class CloudDisplayCoordinator { self.isAvailable = response.exitCode == 0 } catch { guard let self, token == self.generation else { return } + self.snapshot = nil self.isAvailable = false } } @@ -43,7 +44,13 @@ final class CloudDisplayCoordinator { } func create() async throws -> CloudGuestDisplaySnapshot { - if let creation { return try await creation.value } + if let creation { + return try await withTaskCancellationHandler { + try await creation.value + } onCancel: { + creation.cancel() + } + } guard isAvailable, snapshot?.canCreate == true || requestID != nil else { throw SurfaceCatalogError.unsupported(CloudGuestDisplaySnapshot.unavailableMessage) } @@ -68,7 +75,11 @@ final class CloudDisplayCoordinator { } creation = task defer { if generation == token { creation = nil } } - return try await task.value + return try await withTaskCancellationHandler { + try await task.value + } onCancel: { + task.cancel() + } } func stop() { diff --git a/Sources/Cloud/PortForward/CloudBrowserAccessState.swift b/Sources/Cloud/PortForward/CloudBrowserAccessState.swift index fbd4589a6ace..1e04d1b24ec9 100644 --- a/Sources/Cloud/PortForward/CloudBrowserAccessState.swift +++ b/Sources/Cloud/PortForward/CloudBrowserAccessState.swift @@ -181,7 +181,6 @@ final class CloudBrowserAccessState { desktopConnected = false activeNavigationID = nil connectionDeadline.cancel() - startDeadline() attempt += 1 trace("configured") // Reconfiguration invalidates the previous observation generation. @@ -192,11 +191,9 @@ final class CloudBrowserAccessState { func nextURL() -> URL? { guard let remoteURL, let url = model?.url(for: remoteURL) else { - if navigationURL != nil { - hasCommittedNavigation = false - desktopConnected = false - startDeadline() - } + connectionDeadline.cancel() + hasCommittedNavigation = false + desktopConnected = false navigationURL = nil loaded = false return nil @@ -284,7 +281,6 @@ final class CloudBrowserAccessState { desktopConnected = false activeNavigationID = nil connectionDeadline.cancel() - startDeadline() model?.retry() observeRoute() } From 3a5ea9879e17a35541c0340a1f29c7fad1912107 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 13:26:59 -0700 Subject: [PATCH 32/64] test: cover delayed display restore and scoped helpers --- cmuxTests/CloudDesktopAccessTests.swift | 9 +++++++++ tests/test_cloud_display_catalog.py | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/cmuxTests/CloudDesktopAccessTests.swift b/cmuxTests/CloudDesktopAccessTests.swift index b7fbd57e0f47..4e1e163bf658 100644 --- a/cmuxTests/CloudDesktopAccessTests.swift +++ b/cmuxTests/CloudDesktopAccessTests.swift @@ -301,6 +301,15 @@ struct CloudDesktopAccessTests { #expect(browser.cloudResourceForSession == nil) } + @Test("A delayed Cloud restore keeps the saved path and query") + func delayedCloudRestoreKeepsSavedURL() throws { + let browser = BrowserPanel(workspaceId: UUID(), websiteDataStore: .nonPersistent()) + defer { browser.close() } + browser.pendingCloudRestoreURL = try #require(URL(string: "http://10.0.0.7:8000/projects/123?tab=logs#tail")) + let target = try #require(URL(string: "http://10.0.0.7:8000/")) + #expect(browser.cloudRestoreURL(on: target).absoluteString == "http://10.0.0.7:8000/projects/123?tab=logs#tail") + } + @Test("A forwarded /vnc.html URL is not a display when its resource is a browser") func nonDisplayVNCPathDoesNotUseDesktopReadiness() { let state = CloudBrowserAccessState() diff --git a/tests/test_cloud_display_catalog.py b/tests/test_cloud_display_catalog.py index 1b6b2d7e76c1..3c2d54b60698 100644 --- a/tests/test_cloud_display_catalog.py +++ b/tests/test_cloud_display_catalog.py @@ -165,6 +165,15 @@ def terminate(self): self.assertIs(service.websockify_processes[number], new_websockify) service.shutdown.set() + def test_additional_desktop_clients_use_display_scoped_process_names(self): + service = display.DisplayService(self.catalog(), self.root / "runtime") + source = self.root / "openbox" + source.write_text("#!/bin/sh\n") + runtime = self.root / "runtime" / "2" + scoped = service.scoped_component_path("openbox", str(source), 2, runtime) + self.assertEqual(Path(scoped).name, "cmux-display-2-openbox") + self.assertTrue(Path(scoped).exists()) + def test_start_failure_retains_resource_and_replay_receipt(self): service = display.DisplayService(self.catalog(), self.root / "runtime") request = str(uuid.uuid4()) From c3c5c8ce6e7d2eda2cfb67ed4d228d07747b6c90 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 13:27:09 -0700 Subject: [PATCH 33/64] fix: complete display restore and supervisor isolation --- Sources/Cloud/CloudGuestDisplayScript.swift | 220 +++++++++--------- .../PortForward/CloudBrowserAccessState.swift | 4 +- .../Panels/BrowserPanel+CloudConnection.swift | 13 +- Sources/Panels/BrowserPanel.swift | 3 + .../CmuxTuiSurfaceProvider+PortForward.swift | 26 ++- tests/fixtures/cmux-display | 15 ++ 6 files changed, 165 insertions(+), 116 deletions(-) diff --git a/Sources/Cloud/CloudGuestDisplayScript.swift b/Sources/Cloud/CloudGuestDisplayScript.swift index 942a3089b6f5..01204dd18930 100644 --- a/Sources/Cloud/CloudGuestDisplayScript.swift +++ b/Sources/Cloud/CloudGuestDisplayScript.swift @@ -76,112 +76,120 @@ ICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICBwcm9jZXNzLnRlcm1pbmF0ZSgpCiAgICAgICAgICAg ICAgcGFzcwogICAgICAgIHNlbGYudGVybWluYXRlX3dlYnNvY2tpZnkobnVtYmVyKQoKICAgIGRlZiB0ZXJtaW5hdGVfd2Vic29ja2lmeShzZWxmLCBudW1i ZXIpOgogICAgICAgIHByb2Nlc3MgPSBzZWxmLndlYnNvY2tpZnlfcHJvY2Vzc2VzLnBvcChudW1iZXIsIE5vbmUpCiAgICAgICAgaWYgcHJvY2VzcyBpcyBO b25lOgogICAgICAgICAgICByZXR1cm4KICAgICAgICB0cnk6CiAgICAgICAgICAgIHByb2Nlc3MudGVybWluYXRlKCkKICAgICAgICBleGNlcHQgT1NFcnJv -cjoKICAgICAgICAgICAgcGFzcwoKICAgIEBzdGF0aWNtZXRob2QKICAgIGRlZiB3YWl0X2Zvcl9wb3J0KHBvcnQsIHRpbWVvdXQ9MjApOgogICAgICAgIGRl -YWRsaW5lID0gdGltZS5tb25vdG9uaWMoKSArIHRpbWVvdXQKICAgICAgICB3aGlsZSB0aW1lLm1vbm90b25pYygpIDwgZGVhZGxpbmU6CiAgICAgICAgICAg -IGlmIGxpc3RlbmluZyhwb3J0KToKICAgICAgICAgICAgICAgIHJldHVybiBUcnVlCiAgICAgICAgICAgIHRpbWUuc2xlZXAoMC4xKQogICAgICAgIHJldHVy -biBGYWxzZQoKICAgIGRlZiBzdGFydF93ZWJzb2NraWZ5KHNlbGYsIG51bWJlciwgZW52aXJvbm1lbnQsIGV4ZWN1dGFibGUpOgogICAgICAgIHBhdGggPSBl -eGVjdXRhYmxlIGlmICIvIiBpbiBleGVjdXRhYmxlIGVsc2Ugc2h1dGlsLndoaWNoKGV4ZWN1dGFibGUpCiAgICAgICAgaWYgbm90IHBhdGg6CiAgICAgICAg -ICAgIHJhaXNlIE5vVk5DVW5hdmFpbGFibGUoIm1pc3NpbmcgZGlzcGxheSBjb21wb25lbnQ6IHdlYnNvY2tpZnkiKQogICAgICAgIHNlbGYudGVybWluYXRl -X3dlYnNvY2tpZnkobnVtYmVyKQogICAgICAgIHJmYl9wb3J0LCBub3ZuY19wb3J0ID0gNTkwMCArIG51bWJlciwgNjkwMCArIG51bWJlcgogICAgICAgIHBy -b2Nlc3MgPSBzdWJwcm9jZXNzLlBvcGVuKAogICAgICAgICAgICBbcGF0aCwgIi0td2ViIiwgIi91c3Ivc2hhcmUvbm92bmMiLCAiLS1oZWFydGJlYXQiLCAi -MzAiLAogICAgICAgICAgICAgZiIxMjcuMC4wLjE6e25vdm5jX3BvcnR9IiwgZiIxMjcuMC4wLjE6e3JmYl9wb3J0fSJdLAogICAgICAgICAgICBlbnY9ZW52 -aXJvbm1lbnQsIHN0ZGluPXN1YnByb2Nlc3MuREVWTlVMTCwKICAgICAgICAgICAgc3Rkb3V0PXN1YnByb2Nlc3MuREVWTlVMTCwgc3RkZXJyPXN1YnByb2Nl -c3MuREVWTlVMTCkKICAgICAgICBzZWxmLndlYnNvY2tpZnlfcHJvY2Vzc2VzW251bWJlcl0gPSBwcm9jZXNzCiAgICAgICAgaWYgbm90IHNlbGYud2FpdF9m -b3JfcG9ydChub3ZuY19wb3J0LCB0aW1lb3V0PTEwKToKICAgICAgICAgICAgc2VsZi50ZXJtaW5hdGVfd2Vic29ja2lmeShudW1iZXIpCiAgICAgICAgICAg -IHJhaXNlIE5vVk5DVW5hdmFpbGFibGUoIm5vVk5DIGRpZCBub3QgYmVjb21lIHJlYWR5IikKCiAgICBkZWYgc3RhcnRfY29tcG9uZW50cyhzZWxmLCBudW1i -ZXIsIGVudmlyb25tZW50LCBydW50aW1lKToKICAgICAgICBpZiByZWFkeShudW1iZXIpOgogICAgICAgICAgICByZXR1cm4KICAgICAgICByZmJfaXNfcmVh -ZHkgPSByZmJfcmVhZHkobnVtYmVyKQogICAgICAgIHN0YXJ0ZWRfZGVza3RvcCA9IEZhbHNlCiAgICAgICAgaWYgbm90IHJmYl9pc19yZWFkeToKICAgICAg -ICAgICAgc2VsZi50ZXJtaW5hdGUobnVtYmVyKQogICAgICAgICAgICBzdGFydGVkX2Rlc2t0b3AgPSBUcnVlCiAgICAgICAgdm5jID0gc2h1dGlsLndoaWNo -KCJYdm5jIikgb3Igc2h1dGlsLndoaWNoKCJYdGlnZXJ2bmMiKQogICAgICAgIHdlYnNvY2tpZnkgPSBzaHV0aWwud2hpY2goIndlYnNvY2tpZnkiKQogICAg -ICAgIGlmIG5vdCB3ZWJzb2NraWZ5OgogICAgICAgICAgICByYWlzZSBOb1ZOQ1VuYXZhaWxhYmxlKCJkaXNwbGF5IHJ1bnRpbWUgdW5hdmFpbGFibGUiKQog -ICAgICAgIGlmIG5vdCByZmJfaXNfcmVhZHkgYW5kIG5vdCB2bmM6CiAgICAgICAgICAgIHJhaXNlIFJ1bnRpbWVFcnJvcigiZGlzcGxheSBydW50aW1lIHVu -YXZhaWxhYmxlIikKICAgICAgICBpZiBzdGFydGVkX2Rlc2t0b3A6CiAgICAgICAgICAgIHJmYl9wb3J0ID0gNTkwMCArIG51bWJlcgogICAgICAgICAgICBw -cm9jZXNzZXMgPSBbXQogICAgICAgICAgICBwcm9jZXNzZXMuYXBwZW5kKHN1YnByb2Nlc3MuUG9wZW4oCiAgICAgICAgICAgICAgICBbdm5jLCBmIjp7bnVt -YmVyfSIsICItZ2VvbWV0cnkiLCBvcy5lbnZpcm9uLmdldCgiQ01VWF9WTkNfR0VPTUVUUlkiLCAiMTQ0MHg5MDAiKSwKICAgICAgICAgICAgICAgICAiLWRl -cHRoIiwgIjI0IiwgIi1yZmJwb3J0Iiwgc3RyKHJmYl9wb3J0KSwgIi1sb2NhbGhvc3QiLAogICAgICAgICAgICAgICAgICItU2VjdXJpdHlUeXBlcyIsICJO -b25lIiwgIi1BbHdheXNTaGFyZWQiXSwKICAgICAgICAgICAgICAgIGVudj1lbnZpcm9ubWVudCwgc3RkaW49c3VicHJvY2Vzcy5ERVZOVUxMLAogICAgICAg -ICAgICAgICAgc3Rkb3V0PXN1YnByb2Nlc3MuREVWTlVMTCwgc3RkZXJyPXN1YnByb2Nlc3MuREVWTlVMTCkpCiAgICAgICAgICAgIHNlbGYucHJvY2Vzc2Vz -W251bWJlcl0gPSBwcm9jZXNzZXMKICAgICAgICAgICAgaWYgbm90IHNlbGYud2FpdF9mb3JfcG9ydChyZmJfcG9ydCk6CiAgICAgICAgICAgICAgICByYWlz -ZSBSdW50aW1lRXJyb3IoIlh2bmMgZGlkIG5vdCBiZWNvbWUgcmVhZHkiKQogICAgICAgICAgICBidXMgPSBzdWJwcm9jZXNzLnJ1bihbImRidXMtbGF1bmNo -IiwgIi0tc2gtc3ludGF4Il0sIGVudj1lbnZpcm9ubWVudCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3RkaW49c3VicHJvY2Vzcy5ERVZO -VUxMLCBzdGRvdXQ9c3VicHJvY2Vzcy5QSVBFLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdGRlcnI9c3VicHJvY2Vzcy5ERVZOVUxMLCB0 -ZXh0PVRydWUsIHRpbWVvdXQ9MTAsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNoZWNrPVRydWUpLnN0ZG91dAogICAgICAgICAgICBmb3Ig -bGluZSBpbiBidXMuc3BsaXRsaW5lcygpOgogICAgICAgICAgICAgICAgaWYgIj0iIG5vdCBpbiBsaW5lOgogICAgICAgICAgICAgICAgICAgIGNvbnRpbnVl -CiAgICAgICAgICAgICAgICBrZXksIHZhbHVlID0gbGluZS5zcGxpdCgiPSIsIDEpCiAgICAgICAgICAgICAgICBlbnZpcm9ubWVudFtrZXldID0gdmFsdWUu -c3RyaXAoKS5yc3RyaXAoIjsiKS5zdHJpcCgpLnN0cmlwKCInIikKICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgZGJ1c19waWQgPSBpbnQoZW52 -aXJvbm1lbnQuZ2V0KCJEQlVTX1NFU1NJT05fQlVTX1BJRCIsICIwIikpCiAgICAgICAgICAgIGV4Y2VwdCBWYWx1ZUVycm9yOgogICAgICAgICAgICAgICAg -ZGJ1c19waWQgPSAwCiAgICAgICAgICAgIGlmIGRidXNfcGlkID4gMToKICAgICAgICAgICAgICAgIHByb2Nlc3Nlcy5hcHBlbmQoT3duZWRQcm9jZXNzKGRi -dXNfcGlkKSkKICAgICAgICAgICAgZm9yIGV4ZWN1dGFibGUsIGFyZ3MgaW4gWwogICAgICAgICAgICAgICAgKCJvcGVuYm94IiwgW10pLCAoInRpbnQyIiwg -WyItYyIsICIvZXRjL2NtdXgvdGludDJyYyJdKSwKICAgICAgICAgICAgICAgICgidm5jY29uZmlnIiwgWyItbm93aW4iXSksCiAgICAgICAgICAgIF06CiAg -ICAgICAgICAgICAgICBwYXRoID0gZXhlY3V0YWJsZSBpZiAiLyIgaW4gZXhlY3V0YWJsZSBlbHNlIHNodXRpbC53aGljaChleGVjdXRhYmxlKQogICAgICAg -ICAgICAgICAgaWYgbm90IHBhdGg6CiAgICAgICAgICAgICAgICAgICAgaWYgZXhlY3V0YWJsZSA9PSAidm5jY29uZmlnIjoKICAgICAgICAgICAgICAgICAg -ICAgICAgY29udGludWUKICAgICAgICAgICAgICAgICAgICByYWlzZSBSdW50aW1lRXJyb3IoZiJtaXNzaW5nIGRpc3BsYXkgY29tcG9uZW50OiB7ZXhlY3V0 -YWJsZX0iKQogICAgICAgICAgICAgICAgcHJvY2VzcyA9IHN1YnByb2Nlc3MuUG9wZW4oW3BhdGhdICsgYXJncywgZW52PWVudmlyb25tZW50LAogICAgICAg -ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3RkaW49c3VicHJvY2Vzcy5ERVZOVUxMLAogICAgICAgICAgICAgICAgICAgICAgICAgICAg -ICAgICAgICAgICAgICAgc3Rkb3V0PXN1YnByb2Nlc3MuREVWTlVMTCwgc3RkZXJyPXN1YnByb2Nlc3MuREVWTlVMTCkKICAgICAgICAgICAgICAgIHByb2Nl -c3Nlcy5hcHBlbmQocHJvY2VzcykKICAgICAgICBpZiBzdGFydGVkX2Rlc2t0b3Agb3Igbm90IG5vdm5jX3JlYWR5KG51bWJlcik6CiAgICAgICAgICAgIHNl -bGYuc3RhcnRfd2Vic29ja2lmeShudW1iZXIsIGVudmlyb25tZW50LCB3ZWJzb2NraWZ5KQogICAgICAgIChydW50aW1lIC8gImVudiIpLndyaXRlX3RleHQo -ZiJleHBvcnQgRElTUExBWT06e251bWJlcn1cbiIpCgogICAgZGVmIHN1cGVydmlzZShzZWxmLCBudW1iZXIsIGZpbmlzaGVkKToKICAgICAgICBydW50aW1l -ID0gc2VsZi5ydW50aW1lIC8gc3RyKG51bWJlcikKICAgICAgICBydW50aW1lLm1rZGlyKG1vZGU9MG83MDAsIHBhcmVudHM9VHJ1ZSwgZXhpc3Rfb2s9VHJ1 -ZSkKICAgICAgICBlbnZpcm9ubWVudCA9IGRpY3Qob3MuZW52aXJvbiwgRElTUExBWT1mIjp7bnVtYmVyfSIsIENNVVhfREVTS1RPUF9SVU5USU1FX0RJUj1z -dHIocnVudGltZSkpCiAgICAgICAgZW52aXJvbm1lbnRbIkNIUk9NRV9DT05GSUdfSE9NRSJdID0gc3RyKHJ1bnRpbWUgLyAiY2hyb21lLWNvbmZpZyIpCiAg -ICAgICAgZW52aXJvbm1lbnRbIlhER19DT05GSUdfSE9NRSJdID0gc3RyKHJ1bnRpbWUgLyAieGRnLWNvbmZpZyIpCiAgICAgICAgZm9yIGtleSBpbiAoIk5P -VElGWV9TT0NLRVQiLCAiREJVU19TRVNTSU9OX0JVU19BRERSRVNTIiwgIkRCVVNfU0VTU0lPTl9CVVNfUElEIiwKICAgICAgICAgICAgICAgICAgICAiQVRf -U1BJX0JVU19BRERSRVNTIiwgIkFUX1NQSV9CVVMiKToKICAgICAgICAgICAgZW52aXJvbm1lbnQucG9wKGtleSwgTm9uZSkKICAgICAgICB3aGlsZSBub3Qg -c2VsZi5zaHV0ZG93bi5pc19zZXQoKToKICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgIyBUaGUgc3VwZXJ2aXNvciBpcyBhIGhlYWx0aCBjaGVj -aywgbm90IGEgdGltZXItZHJpdmVuIHJlc3RhcnQuCiAgICAgICAgICAgICAgICAjIFByZXNlcnZlIGFuIGFjdGl2ZSBSRkIvbm9WTkMgc2Vzc2lvbiB1bnRp -bCBpdHMgb3duZWQgZW5kcG9pbnRzIGZhaWwuCiAgICAgICAgICAgICAgICBpZiBub3Qgc2VsZi5wcm9jZXNzZXMuZ2V0KG51bWJlcikgb3Igbm90IHJlYWR5 -KG51bWJlcik6CiAgICAgICAgICAgICAgICAgICAgc2VsZi5zdGFydF9jb21wb25lbnRzKG51bWJlciwgZW52aXJvbm1lbnQsIHJ1bnRpbWUpCiAgICAgICAg -ICAgICAgICBzdGF0ZSA9ICJydW5uaW5nIiBpZiByZWFkeShudW1iZXIpIGVsc2UgInVuYXZhaWxhYmxlIgogICAgICAgICAgICBleGNlcHQgTm9WTkNVbmF2 -YWlsYWJsZToKICAgICAgICAgICAgICAgIHNlbGYudGVybWluYXRlX3dlYnNvY2tpZnkobnVtYmVyKQogICAgICAgICAgICAgICAgc3RhdGUgPSAidW5hdmFp -bGFibGUiCiAgICAgICAgICAgIGV4Y2VwdCAoT1NFcnJvciwgUnVudGltZUVycm9yLCBzdWJwcm9jZXNzLlN1YnByb2Nlc3NFcnJvcik6CiAgICAgICAgICAg -ICAgICBzZWxmLnRlcm1pbmF0ZShudW1iZXIpCiAgICAgICAgICAgICAgICBzdGF0ZSA9ICJ1bmF2YWlsYWJsZSIKICAgICAgICAgICAgd2l0aCBzZWxmLmxv -Y2s6CiAgICAgICAgICAgICAgICBzZWxmLnN0YXRlc1tudW1iZXJdID0gc3RhdGUKICAgICAgICAgICAgZmluaXNoZWQuc2V0KCkKICAgICAgICAgICAgc2Vs -Zi5zaHV0ZG93bi53YWl0KDMwKQogICAgICAgIHNlbGYudGVybWluYXRlKG51bWJlcikKCiAgICBkZWYgc25hcHNob3Qoc2VsZik6CiAgICAgICAgd2l0aCBz -ZWxmLmxvY2s6CiAgICAgICAgICAgIHJldHVybiB7InZlcnNpb24iOiAxLCAiY2FuQ3JlYXRlIjogbGVuKHNlbGYuam9icykgPCBNQVhfRElTUExBWVMgLSAx -LAogICAgICAgICAgICAgICAgICAgICJkaXNwbGF5cyI6IFtkZXNjcmlwdG9yKDEsICJydW5uaW5nIiBpZiByZWFkeSgxKSBlbHNlICJ1bmF2YWlsYWJsZSIp -XQogICAgICAgICAgICAgICAgICAgICsgW2Rlc2NyaXB0b3Iobiwgc2VsZi5zdGF0ZXMuZ2V0KG4sICJsYXVuY2hpbmciKSkgZm9yIG4gaW4gc2VsZi5jYXRh -bG9nLm51bWJlcnMoKV19CgogICAgZGVmIGhhbmRsZShzZWxmLCByZXF1ZXN0KToKICAgICAgICBpZiByZXF1ZXN0LmdldCgiYWN0aW9uIikgPT0gImxpc3Qi -OgogICAgICAgICAgICByZXR1cm4gc2VsZi5zbmFwc2hvdCgpCiAgICAgICAgaWYgcmVxdWVzdC5nZXQoImFjdGlvbiIpICE9ICJjcmVhdGUiOgogICAgICAg -ICAgICByYWlzZSBWYWx1ZUVycm9yKCJ1bnN1cHBvcnRlZCBkaXNwbGF5IGFjdGlvbiIpCiAgICAgICAgbnVtYmVyID0gc2VsZi5jYXRhbG9nLmFsbG9jYXRl -KHJlcXVlc3QuZ2V0KCJyZXF1ZXN0IikpCiAgICAgICAgc2VsZi5zdGFydChudW1iZXIpLndhaXQoNTUpCiAgICAgICAgcmVzdWx0ID0gc2VsZi5zbmFwc2hv -dCgpCiAgICAgICAgcmVzdWx0WyJjcmVhdGVkIl0gPSBmImRpc3BsYXk6e251bWJlcn0iCiAgICAgICAgaWYgc2VsZi5zdGF0ZXMuZ2V0KG51bWJlcikgIT0g -InJ1bm5pbmciOgogICAgICAgICAgICByZXN1bHRbImVycm9yIl0gPSAiZGlzcGxheV9zdGFydF9mYWlsZWQiCiAgICAgICAgcmV0dXJuIHJlc3VsdAoKCmRl -ZiBzZXJ2ZShydW50aW1lKToKICAgIHJ1bnRpbWUgPSBQYXRoKHJ1bnRpbWUpCiAgICBydW50aW1lLm1rZGlyKG1vZGU9MG83MDAsIHBhcmVudHM9VHJ1ZSwg -ZXhpc3Rfb2s9VHJ1ZSkKICAgIHdpdGggKHJ1bnRpbWUgLyAiZGlzcGxheS1jb250cm9sLmxvY2siKS5vcGVuKCJ3IikgYXMgbG9jazoKICAgICAgICB0cnk6 -CiAgICAgICAgICAgIGZjbnRsLmZsb2NrKGxvY2ssIGZjbnRsLkxPQ0tfRVggfCBmY250bC5MT0NLX05CKQogICAgICAgIGV4Y2VwdCBCbG9ja2luZ0lPRXJy -b3I6CiAgICAgICAgICAgIHJldHVybgogICAgICAgIGlmIG5vdCBhbGwoc2h1dGlsLndoaWNoKG5hbWUpIGZvciBuYW1lIGluICgid2Vic29ja2lmeSIsICJv -cGVuYm94IiwgImRidXMtbGF1bmNoIikpOgogICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJkaXNwbGF5IHJ1bnRpbWUgdW5hdmFpbGFibGUiKQogICAg -ICAgIGlmIG5vdCAoc2h1dGlsLndoaWNoKCJYdm5jIikgb3Igc2h1dGlsLndoaWNoKCJYdGlnZXJ2bmMiKSk6CiAgICAgICAgICAgIHJhaXNlIFZhbHVlRXJy -b3IoImRpc3BsYXkgcnVudGltZSB1bmF2YWlsYWJsZSIpCiAgICAgICAgc2VydmljZSA9IERpc3BsYXlTZXJ2aWNlKERpc3BsYXlDYXRhbG9nKFBhdGguaG9t -ZSgpIC8gIi5jbXV4IiAvICJkaXNwbGF5cyIpLCBydW50aW1lIC8gImRpc3BsYXlzIikKICAgICAgICBmb3IgbnVtYmVyIGluIHNlcnZpY2UuY2F0YWxvZy5u -dW1iZXJzKCk6CiAgICAgICAgICAgIHNlcnZpY2Uuc3RhcnQobnVtYmVyKQogICAgICAgIHBhdGggPSBydW50aW1lIC8gImRpc3BsYXktY29udHJvbC5zb2Nr -IgogICAgICAgIGlmIHBhdGguZXhpc3RzKCk6CiAgICAgICAgICAgIHBhdGgudW5saW5rKCkKCiAgICAgICAgY2xhc3MgSGFuZGxlcihzb2NrZXRzZXJ2ZXIu -U3RyZWFtUmVxdWVzdEhhbmRsZXIpOgogICAgICAgICAgICBkZWYgaGFuZGxlKHNlbGYpOgogICAgICAgICAgICAgICAgc2VsZi5jb25uZWN0aW9uLnNldHRp -bWVvdXQoNjApCiAgICAgICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICAgICAgcmF3ID0gc2VsZi5yZmlsZS5yZWFkbGluZSg0MDk3KQogICAgICAg -ICAgICAgICAgICAgIGlmIGxlbihyYXcpID4gNDA5NjoKICAgICAgICAgICAgICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJvcigiZGlzcGxheSByZXF1ZXN0 -IHRvbyBsYXJnZSIpCiAgICAgICAgICAgICAgICAgICAgcmVzdWx0ID0gc2VydmljZS5oYW5kbGUoanNvbi5sb2FkcyhyYXcpKQogICAgICAgICAgICAgICAg -ZXhjZXB0IChWYWx1ZUVycm9yLCBLZXlFcnJvciwgVHlwZUVycm9yLCBPU0Vycm9yKToKICAgICAgICAgICAgICAgICAgICByZXN1bHQgPSB7ImVycm9yIjog -ImRpc3BsYXlfcmVxdWVzdF9mYWlsZWQifQogICAgICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgIHNlbGYud2ZpbGUud3JpdGUoanNvbi5k -dW1wcyhyZXN1bHQpLmVuY29kZSgpICsgYiJcbiIpCiAgICAgICAgICAgICAgICBleGNlcHQgT1NFcnJvcjoKICAgICAgICAgICAgICAgICAgICBwYXNzICAj -IFRoZSBwZXJzaXN0ZWQgcmVjZWlwdCByZW1haW5zIHJlcGxheWFibGUgYWZ0ZXIgZGlzY29ubmVjdC4KCiAgICAgICAgY2xhc3MgU2VydmVyKHNvY2tldHNl -cnZlci5UaHJlYWRpbmdVbml4U3RyZWFtU2VydmVyKToKICAgICAgICAgICAgZGFlbW9uX3RocmVhZHMgPSBUcnVlCgogICAgICAgIHByZXZpb3VzX3VtYXNr -ID0gb3MudW1hc2soMG8wNzcpCiAgICAgICAgdHJ5OgogICAgICAgICAgICB3aXRoIFNlcnZlcihzdHIocGF0aCksIEhhbmRsZXIpIGFzIHNlcnZlcjoKICAg -ICAgICAgICAgICAgIG9zLnVtYXNrKHByZXZpb3VzX3VtYXNrKQogICAgICAgICAgICAgICAgc2VydmVyLnNlcnZlX2ZvcmV2ZXIoKQogICAgICAgIGZpbmFs -bHk6CiAgICAgICAgICAgIG9zLnVtYXNrKHByZXZpb3VzX3VtYXNrKQogICAgICAgICAgICBzZXJ2aWNlLnNodXRkb3duLnNldCgpCiAgICAgICAgICAgIHBh -dGgudW5saW5rKG1pc3Npbmdfb2s9VHJ1ZSkKCgpkZWYgbWFpbigpOgogICAgcGFyc2VyID0gYXJncGFyc2UuQXJndW1lbnRQYXJzZXIoZGVzY3JpcHRpb249 -X19kb2NfXykKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoImFjdGlvbiIsIGNob2ljZXM9WyJsaXN0IiwgImNyZWF0ZSIsICJzZXJ2ZSJdKQogICAgcGFyc2Vy -LmFkZF9hcmd1bWVudCgiLS1yZXF1ZXN0LWlkIikKICAgIGFyZ3MgPSBwYXJzZXIucGFyc2VfYXJncygpCiAgICBydW50aW1lID0gb3MuZW52aXJvbi5nZXQo -IkNNVVhfREVTS1RPUF9SVU5USU1FX0RJUiIsICIvcnVuL2NtdXgtZGVza3RvcCIpCiAgICBpZiBhcmdzLmFjdGlvbiA9PSAic2VydmUiOgogICAgICAgIHNl -cnZlKHJ1bnRpbWUpCiAgICAgICAgcmV0dXJuIDAKICAgIGlmIGFyZ3MuYWN0aW9uID09ICJjcmVhdGUiIGFuZCBub3QgYXJncy5yZXF1ZXN0X2lkOgogICAg -ICAgIHBhcnNlci5lcnJvcigiY3JlYXRlIHJlcXVpcmVzIC0tcmVxdWVzdC1pZCAocmV0YWluIGl0IHdoZW4gcmV0cnlpbmcpIikKICAgIHRyeToKICAgICAg -ICB3aXRoIHNvY2tldC5zb2NrZXQoc29ja2V0LkFGX1VOSVgsIHNvY2tldC5TT0NLX1NUUkVBTSkgYXMgY2xpZW50OgogICAgICAgICAgICBjbGllbnQuc2V0 -dGltZW91dCg2MCkKICAgICAgICAgICAgY2xpZW50LmNvbm5lY3Qoc3RyKFBhdGgocnVudGltZSkgLyAiZGlzcGxheS1jb250cm9sLnNvY2siKSkKICAgICAg -ICAgICAgY2xpZW50LnNlbmRhbGwoanNvbi5kdW1wcyh7ImFjdGlvbiI6IGFyZ3MuYWN0aW9uLCAicmVxdWVzdCI6IGFyZ3MucmVxdWVzdF9pZH0pLmVuY29k -ZSgpICsgYiJcbiIpCiAgICAgICAgICAgIHdpdGggY2xpZW50Lm1ha2VmaWxlKCJyYiIpIGFzIHN0cmVhbToKICAgICAgICAgICAgICAgIHJlc3VsdCA9IGpz -b24ubG9hZHMoc3RyZWFtLnJlYWRsaW5lKDY1NTM2KSkKICAgIGV4Y2VwdCAoT1NFcnJvciwgVmFsdWVFcnJvcik6CiAgICAgICAgcmVzdWx0ID0geyJlcnJv -ciI6ICJkaXNwbGF5X3J1bnRpbWVfdW5hdmFpbGFibGUifQogICAgcHJpbnQoanNvbi5kdW1wcyhyZXN1bHQpKQogICAgcmV0dXJuIDEgaWYgImVycm9yIiBp -biByZXN1bHQgZWxzZSAwCgoKaWYgX19uYW1lX18gPT0gIl9fbWFpbl9fIjoKICAgIHJhaXNlIFN5c3RlbUV4aXQobWFpbigpKQo= +cjoKICAgICAgICAgICAgcGFzcwoKICAgIEBzdGF0aWNtZXRob2QKICAgIGRlZiBzY29wZWRfY29tcG9uZW50X3BhdGgoZXhlY3V0YWJsZSwgcGF0aCwgbnVt +YmVyLCBydW50aW1lKToKICAgICAgICAiIiJHaXZlIHBlci1kaXNwbGF5IGRlc2t0b3AgY2xpZW50cyBuYW1lcyB0aGUgOjEgc3VwZXJ2aXNvciBjYW5ub3Qg +bWF0Y2guIiIiCiAgICAgICAgc291cmNlID0gUGF0aChwYXRoKQogICAgICAgIGlmIG5vdCBzb3VyY2UuaXNfZmlsZSgpOgogICAgICAgICAgICByZXR1cm4g +cGF0aAogICAgICAgIGRpcmVjdG9yeSA9IFBhdGgocnVudGltZSkgLyAiYmluIgogICAgICAgIGRpcmVjdG9yeS5ta2Rpcihtb2RlPTBvNzAwLCBwYXJlbnRz +PVRydWUsIGV4aXN0X29rPVRydWUpCiAgICAgICAgdGFyZ2V0ID0gZGlyZWN0b3J5IC8gZiJjbXV4LWRpc3BsYXkte251bWJlcn0te3NvdXJjZS5uYW1lfSIK +ICAgICAgICBpZiBub3QgdGFyZ2V0LmV4aXN0cygpIG9yIHRhcmdldC5zdGF0KCkuc3RfbXRpbWVfbnMgPCBzb3VyY2Uuc3RhdCgpLnN0X210aW1lX25zOgog +ICAgICAgICAgICBzaHV0aWwuY29weTIoc291cmNlLCB0YXJnZXQpCiAgICAgICAgICAgIHRhcmdldC5jaG1vZCgwbzcwMCkKICAgICAgICByZXR1cm4gc3Ry +KHRhcmdldCkKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgd2FpdF9mb3JfcG9ydChwb3J0LCB0aW1lb3V0PTIwKToKICAgICAgICBkZWFkbGluZSA9IHRp +bWUubW9ub3RvbmljKCkgKyB0aW1lb3V0CiAgICAgICAgd2hpbGUgdGltZS5tb25vdG9uaWMoKSA8IGRlYWRsaW5lOgogICAgICAgICAgICBpZiBsaXN0ZW5p +bmcocG9ydCk6CiAgICAgICAgICAgICAgICByZXR1cm4gVHJ1ZQogICAgICAgICAgICB0aW1lLnNsZWVwKDAuMSkKICAgICAgICByZXR1cm4gRmFsc2UKCiAg +ICBkZWYgc3RhcnRfd2Vic29ja2lmeShzZWxmLCBudW1iZXIsIGVudmlyb25tZW50LCBleGVjdXRhYmxlKToKICAgICAgICBwYXRoID0gZXhlY3V0YWJsZSBp +ZiAiLyIgaW4gZXhlY3V0YWJsZSBlbHNlIHNodXRpbC53aGljaChleGVjdXRhYmxlKQogICAgICAgIGlmIG5vdCBwYXRoOgogICAgICAgICAgICByYWlzZSBO +b1ZOQ1VuYXZhaWxhYmxlKCJtaXNzaW5nIGRpc3BsYXkgY29tcG9uZW50OiB3ZWJzb2NraWZ5IikKICAgICAgICBzZWxmLnRlcm1pbmF0ZV93ZWJzb2NraWZ5 +KG51bWJlcikKICAgICAgICByZmJfcG9ydCwgbm92bmNfcG9ydCA9IDU5MDAgKyBudW1iZXIsIDY5MDAgKyBudW1iZXIKICAgICAgICBwcm9jZXNzID0gc3Vi +cHJvY2Vzcy5Qb3BlbigKICAgICAgICAgICAgW3BhdGgsICItLXdlYiIsICIvdXNyL3NoYXJlL25vdm5jIiwgIi0taGVhcnRiZWF0IiwgIjMwIiwKICAgICAg +ICAgICAgIGYiMTI3LjAuMC4xOntub3ZuY19wb3J0fSIsIGYiMTI3LjAuMC4xOntyZmJfcG9ydH0iXSwKICAgICAgICAgICAgZW52PWVudmlyb25tZW50LCBz +dGRpbj1zdWJwcm9jZXNzLkRFVk5VTEwsCiAgICAgICAgICAgIHN0ZG91dD1zdWJwcm9jZXNzLkRFVk5VTEwsIHN0ZGVycj1zdWJwcm9jZXNzLkRFVk5VTEwp +CiAgICAgICAgc2VsZi53ZWJzb2NraWZ5X3Byb2Nlc3Nlc1tudW1iZXJdID0gcHJvY2VzcwogICAgICAgIGlmIG5vdCBzZWxmLndhaXRfZm9yX3BvcnQobm92 +bmNfcG9ydCwgdGltZW91dD0xMCk6CiAgICAgICAgICAgIHNlbGYudGVybWluYXRlX3dlYnNvY2tpZnkobnVtYmVyKQogICAgICAgICAgICByYWlzZSBOb1ZO +Q1VuYXZhaWxhYmxlKCJub1ZOQyBkaWQgbm90IGJlY29tZSByZWFkeSIpCgogICAgZGVmIHN0YXJ0X2NvbXBvbmVudHMoc2VsZiwgbnVtYmVyLCBlbnZpcm9u +bWVudCwgcnVudGltZSk6CiAgICAgICAgaWYgcmVhZHkobnVtYmVyKToKICAgICAgICAgICAgcmV0dXJuCiAgICAgICAgcmZiX2lzX3JlYWR5ID0gcmZiX3Jl +YWR5KG51bWJlcikKICAgICAgICBzdGFydGVkX2Rlc2t0b3AgPSBGYWxzZQogICAgICAgIGlmIG5vdCByZmJfaXNfcmVhZHk6CiAgICAgICAgICAgIHNlbGYu +dGVybWluYXRlKG51bWJlcikKICAgICAgICAgICAgc3RhcnRlZF9kZXNrdG9wID0gVHJ1ZQogICAgICAgIHZuYyA9IHNodXRpbC53aGljaCgiWHZuYyIpIG9y +IHNodXRpbC53aGljaCgiWHRpZ2Vydm5jIikKICAgICAgICB3ZWJzb2NraWZ5ID0gc2h1dGlsLndoaWNoKCJ3ZWJzb2NraWZ5IikKICAgICAgICBpZiBub3Qg +d2Vic29ja2lmeToKICAgICAgICAgICAgcmFpc2UgTm9WTkNVbmF2YWlsYWJsZSgiZGlzcGxheSBydW50aW1lIHVuYXZhaWxhYmxlIikKICAgICAgICBpZiBu +b3QgcmZiX2lzX3JlYWR5IGFuZCBub3Qgdm5jOgogICAgICAgICAgICByYWlzZSBSdW50aW1lRXJyb3IoImRpc3BsYXkgcnVudGltZSB1bmF2YWlsYWJsZSIp +CiAgICAgICAgaWYgc3RhcnRlZF9kZXNrdG9wOgogICAgICAgICAgICByZmJfcG9ydCA9IDU5MDAgKyBudW1iZXIKICAgICAgICAgICAgcHJvY2Vzc2VzID0g +W10KICAgICAgICAgICAgcHJvY2Vzc2VzLmFwcGVuZChzdWJwcm9jZXNzLlBvcGVuKAogICAgICAgICAgICAgICAgW3ZuYywgZiI6e251bWJlcn0iLCAiLWdl +b21ldHJ5Iiwgb3MuZW52aXJvbi5nZXQoIkNNVVhfVk5DX0dFT01FVFJZIiwgIjE0NDB4OTAwIiksCiAgICAgICAgICAgICAgICAgIi1kZXB0aCIsICIyNCIs +ICItcmZicG9ydCIsIHN0cihyZmJfcG9ydCksICItbG9jYWxob3N0IiwKICAgICAgICAgICAgICAgICAiLVNlY3VyaXR5VHlwZXMiLCAiTm9uZSIsICItQWx3 +YXlzU2hhcmVkIl0sCiAgICAgICAgICAgICAgICBlbnY9ZW52aXJvbm1lbnQsIHN0ZGluPXN1YnByb2Nlc3MuREVWTlVMTCwKICAgICAgICAgICAgICAgIHN0 +ZG91dD1zdWJwcm9jZXNzLkRFVk5VTEwsIHN0ZGVycj1zdWJwcm9jZXNzLkRFVk5VTEwpKQogICAgICAgICAgICBzZWxmLnByb2Nlc3Nlc1tudW1iZXJdID0g +cHJvY2Vzc2VzCiAgICAgICAgICAgIGlmIG5vdCBzZWxmLndhaXRfZm9yX3BvcnQocmZiX3BvcnQpOgogICAgICAgICAgICAgICAgcmFpc2UgUnVudGltZUVy +cm9yKCJYdm5jIGRpZCBub3QgYmVjb21lIHJlYWR5IikKICAgICAgICAgICAgYnVzID0gc3VicHJvY2Vzcy5ydW4oWyJkYnVzLWxhdW5jaCIsICItLXNoLXN5 +bnRheCJdLCBlbnY9ZW52aXJvbm1lbnQsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0ZGluPXN1YnByb2Nlc3MuREVWTlVMTCwgc3Rkb3V0 +PXN1YnByb2Nlc3MuUElQRSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3RkZXJyPXN1YnByb2Nlc3MuREVWTlVMTCwgdGV4dD1UcnVlLCB0 +aW1lb3V0PTEwLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjaGVjaz1UcnVlKS5zdGRvdXQKICAgICAgICAgICAgZm9yIGxpbmUgaW4gYnVz +LnNwbGl0bGluZXMoKToKICAgICAgICAgICAgICAgIGlmICI9IiBub3QgaW4gbGluZToKICAgICAgICAgICAgICAgICAgICBjb250aW51ZQogICAgICAgICAg +ICAgICAga2V5LCB2YWx1ZSA9IGxpbmUuc3BsaXQoIj0iLCAxKQogICAgICAgICAgICAgICAgZW52aXJvbm1lbnRba2V5XSA9IHZhbHVlLnN0cmlwKCkucnN0 +cmlwKCI7Iikuc3RyaXAoKS5zdHJpcCgiJyIpCiAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgIGRidXNfcGlkID0gaW50KGVudmlyb25tZW50Lmdl +dCgiREJVU19TRVNTSU9OX0JVU19QSUQiLCAiMCIpKQogICAgICAgICAgICBleGNlcHQgVmFsdWVFcnJvcjoKICAgICAgICAgICAgICAgIGRidXNfcGlkID0g +MAogICAgICAgICAgICBpZiBkYnVzX3BpZCA+IDE6CiAgICAgICAgICAgICAgICBwcm9jZXNzZXMuYXBwZW5kKE93bmVkUHJvY2VzcyhkYnVzX3BpZCkpCiAg +ICAgICAgICAgIGZvciBleGVjdXRhYmxlLCBhcmdzIGluIFsKICAgICAgICAgICAgICAgICgib3BlbmJveCIsIFtdKSwgKCJ0aW50MiIsIFsiLWMiLCAiL2V0 +Yy9jbXV4L3RpbnQycmMiXSksCiAgICAgICAgICAgICAgICAoInZuY2NvbmZpZyIsIFsiLW5vd2luIl0pLAogICAgICAgICAgICBdOgogICAgICAgICAgICAg +ICAgcGF0aCA9IGV4ZWN1dGFibGUgaWYgIi8iIGluIGV4ZWN1dGFibGUgZWxzZSBzaHV0aWwud2hpY2goZXhlY3V0YWJsZSkKICAgICAgICAgICAgICAgIGlm +IG5vdCBwYXRoOgogICAgICAgICAgICAgICAgICAgIGlmIGV4ZWN1dGFibGUgPT0gInZuY2NvbmZpZyI6CiAgICAgICAgICAgICAgICAgICAgICAgIGNvbnRp +bnVlCiAgICAgICAgICAgICAgICAgICAgcmFpc2UgUnVudGltZUVycm9yKGYibWlzc2luZyBkaXNwbGF5IGNvbXBvbmVudDoge2V4ZWN1dGFibGV9IikKICAg +ICAgICAgICAgICAgIHBhdGggPSBzZWxmLnNjb3BlZF9jb21wb25lbnRfcGF0aChleGVjdXRhYmxlLCBwYXRoLCBudW1iZXIsIHJ1bnRpbWUpCiAgICAgICAg +ICAgICAgICBwcm9jZXNzID0gc3VicHJvY2Vzcy5Qb3BlbihbcGF0aF0gKyBhcmdzLCBlbnY9ZW52aXJvbm1lbnQsCiAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICBzdGRpbj1zdWJwcm9jZXNzLkRFVk5VTEwsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBz +dGRvdXQ9c3VicHJvY2Vzcy5ERVZOVUxMLCBzdGRlcnI9c3VicHJvY2Vzcy5ERVZOVUxMKQogICAgICAgICAgICAgICAgcHJvY2Vzc2VzLmFwcGVuZChwcm9j +ZXNzKQogICAgICAgIGlmIHN0YXJ0ZWRfZGVza3RvcCBvciBub3Qgbm92bmNfcmVhZHkobnVtYmVyKToKICAgICAgICAgICAgc2VsZi5zdGFydF93ZWJzb2Nr +aWZ5KG51bWJlciwgZW52aXJvbm1lbnQsIHdlYnNvY2tpZnkpCiAgICAgICAgKHJ1bnRpbWUgLyAiZW52Iikud3JpdGVfdGV4dChmImV4cG9ydCBESVNQTEFZ +PTp7bnVtYmVyfVxuIikKCiAgICBkZWYgc3VwZXJ2aXNlKHNlbGYsIG51bWJlciwgZmluaXNoZWQpOgogICAgICAgIHJ1bnRpbWUgPSBzZWxmLnJ1bnRpbWUg +LyBzdHIobnVtYmVyKQogICAgICAgIHJ1bnRpbWUubWtkaXIobW9kZT0wbzcwMCwgcGFyZW50cz1UcnVlLCBleGlzdF9vaz1UcnVlKQogICAgICAgIGVudmly +b25tZW50ID0gZGljdChvcy5lbnZpcm9uLCBESVNQTEFZPWYiOntudW1iZXJ9IiwgQ01VWF9ERVNLVE9QX1JVTlRJTUVfRElSPXN0cihydW50aW1lKSkKICAg +ICAgICBlbnZpcm9ubWVudFsiQ0hST01FX0NPTkZJR19IT01FIl0gPSBzdHIocnVudGltZSAvICJjaHJvbWUtY29uZmlnIikKICAgICAgICBlbnZpcm9ubWVu +dFsiWERHX0NPTkZJR19IT01FIl0gPSBzdHIocnVudGltZSAvICJ4ZGctY29uZmlnIikKICAgICAgICBmb3Iga2V5IGluICgiTk9USUZZX1NPQ0tFVCIsICJE +QlVTX1NFU1NJT05fQlVTX0FERFJFU1MiLCAiREJVU19TRVNTSU9OX0JVU19QSUQiLAogICAgICAgICAgICAgICAgICAgICJBVF9TUElfQlVTX0FERFJFU1Mi +LCAiQVRfU1BJX0JVUyIpOgogICAgICAgICAgICBlbnZpcm9ubWVudC5wb3Aoa2V5LCBOb25lKQogICAgICAgIHdoaWxlIG5vdCBzZWxmLnNodXRkb3duLmlz +X3NldCgpOgogICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICAjIFRoZSBzdXBlcnZpc29yIGlzIGEgaGVhbHRoIGNoZWNrLCBub3QgYSB0aW1lci1k +cml2ZW4gcmVzdGFydC4KICAgICAgICAgICAgICAgICMgUHJlc2VydmUgYW4gYWN0aXZlIFJGQi9ub1ZOQyBzZXNzaW9uIHVudGlsIGl0cyBvd25lZCBlbmRw +b2ludHMgZmFpbC4KICAgICAgICAgICAgICAgIGlmIG5vdCBzZWxmLnByb2Nlc3Nlcy5nZXQobnVtYmVyKSBvciBub3QgcmVhZHkobnVtYmVyKToKICAgICAg +ICAgICAgICAgICAgICBzZWxmLnN0YXJ0X2NvbXBvbmVudHMobnVtYmVyLCBlbnZpcm9ubWVudCwgcnVudGltZSkKICAgICAgICAgICAgICAgIHN0YXRlID0g +InJ1bm5pbmciIGlmIHJlYWR5KG51bWJlcikgZWxzZSAidW5hdmFpbGFibGUiCiAgICAgICAgICAgIGV4Y2VwdCBOb1ZOQ1VuYXZhaWxhYmxlOgogICAgICAg +ICAgICAgICAgc2VsZi50ZXJtaW5hdGVfd2Vic29ja2lmeShudW1iZXIpCiAgICAgICAgICAgICAgICBzdGF0ZSA9ICJ1bmF2YWlsYWJsZSIKICAgICAgICAg +ICAgZXhjZXB0IChPU0Vycm9yLCBSdW50aW1lRXJyb3IsIHN1YnByb2Nlc3MuU3VicHJvY2Vzc0Vycm9yKToKICAgICAgICAgICAgICAgIHNlbGYudGVybWlu +YXRlKG51bWJlcikKICAgICAgICAgICAgICAgIHN0YXRlID0gInVuYXZhaWxhYmxlIgogICAgICAgICAgICB3aXRoIHNlbGYubG9jazoKICAgICAgICAgICAg +ICAgIHNlbGYuc3RhdGVzW251bWJlcl0gPSBzdGF0ZQogICAgICAgICAgICBmaW5pc2hlZC5zZXQoKQogICAgICAgICAgICBzZWxmLnNodXRkb3duLndhaXQo +MzApCiAgICAgICAgc2VsZi50ZXJtaW5hdGUobnVtYmVyKQoKICAgIGRlZiBzbmFwc2hvdChzZWxmKToKICAgICAgICB3aXRoIHNlbGYubG9jazoKICAgICAg +ICAgICAgcmV0dXJuIHsidmVyc2lvbiI6IDEsICJjYW5DcmVhdGUiOiBsZW4oc2VsZi5qb2JzKSA8IE1BWF9ESVNQTEFZUyAtIDEsCiAgICAgICAgICAgICAg +ICAgICAgImRpc3BsYXlzIjogW2Rlc2NyaXB0b3IoMSwgInJ1bm5pbmciIGlmIHJlYWR5KDEpIGVsc2UgInVuYXZhaWxhYmxlIildCiAgICAgICAgICAgICAg +ICAgICAgKyBbZGVzY3JpcHRvcihuLCBzZWxmLnN0YXRlcy5nZXQobiwgImxhdW5jaGluZyIpKSBmb3IgbiBpbiBzZWxmLmNhdGFsb2cubnVtYmVycygpXX0K +CiAgICBkZWYgaGFuZGxlKHNlbGYsIHJlcXVlc3QpOgogICAgICAgIGlmIHJlcXVlc3QuZ2V0KCJhY3Rpb24iKSA9PSAibGlzdCI6CiAgICAgICAgICAgIHJl +dHVybiBzZWxmLnNuYXBzaG90KCkKICAgICAgICBpZiByZXF1ZXN0LmdldCgiYWN0aW9uIikgIT0gImNyZWF0ZSI6CiAgICAgICAgICAgIHJhaXNlIFZhbHVl +RXJyb3IoInVuc3VwcG9ydGVkIGRpc3BsYXkgYWN0aW9uIikKICAgICAgICBudW1iZXIgPSBzZWxmLmNhdGFsb2cuYWxsb2NhdGUocmVxdWVzdC5nZXQoInJl +cXVlc3QiKSkKICAgICAgICBzZWxmLnN0YXJ0KG51bWJlcikud2FpdCg1NSkKICAgICAgICByZXN1bHQgPSBzZWxmLnNuYXBzaG90KCkKICAgICAgICByZXN1 +bHRbImNyZWF0ZWQiXSA9IGYiZGlzcGxheTp7bnVtYmVyfSIKICAgICAgICBpZiBzZWxmLnN0YXRlcy5nZXQobnVtYmVyKSAhPSAicnVubmluZyI6CiAgICAg +ICAgICAgIHJlc3VsdFsiZXJyb3IiXSA9ICJkaXNwbGF5X3N0YXJ0X2ZhaWxlZCIKICAgICAgICByZXR1cm4gcmVzdWx0CgoKZGVmIHNlcnZlKHJ1bnRpbWUp +OgogICAgcnVudGltZSA9IFBhdGgocnVudGltZSkKICAgIHJ1bnRpbWUubWtkaXIobW9kZT0wbzcwMCwgcGFyZW50cz1UcnVlLCBleGlzdF9vaz1UcnVlKQog +ICAgd2l0aCAocnVudGltZSAvICJkaXNwbGF5LWNvbnRyb2wubG9jayIpLm9wZW4oInciKSBhcyBsb2NrOgogICAgICAgIHRyeToKICAgICAgICAgICAgZmNu +dGwuZmxvY2sobG9jaywgZmNudGwuTE9DS19FWCB8IGZjbnRsLkxPQ0tfTkIpCiAgICAgICAgZXhjZXB0IEJsb2NraW5nSU9FcnJvcjoKICAgICAgICAgICAg +cmV0dXJuCiAgICAgICAgaWYgbm90IGFsbChzaHV0aWwud2hpY2gobmFtZSkgZm9yIG5hbWUgaW4gKCJ3ZWJzb2NraWZ5IiwgIm9wZW5ib3giLCAiZGJ1cy1s +YXVuY2giKSk6CiAgICAgICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoImRpc3BsYXkgcnVudGltZSB1bmF2YWlsYWJsZSIpCiAgICAgICAgaWYgbm90IChzaHV0 +aWwud2hpY2goIlh2bmMiKSBvciBzaHV0aWwud2hpY2goIlh0aWdlcnZuYyIpKToKICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJvcigiZGlzcGxheSBydW50 +aW1lIHVuYXZhaWxhYmxlIikKICAgICAgICBzZXJ2aWNlID0gRGlzcGxheVNlcnZpY2UoRGlzcGxheUNhdGFsb2coUGF0aC5ob21lKCkgLyAiLmNtdXgiIC8g +ImRpc3BsYXlzIiksIHJ1bnRpbWUgLyAiZGlzcGxheXMiKQogICAgICAgIGZvciBudW1iZXIgaW4gc2VydmljZS5jYXRhbG9nLm51bWJlcnMoKToKICAgICAg +ICAgICAgc2VydmljZS5zdGFydChudW1iZXIpCiAgICAgICAgcGF0aCA9IHJ1bnRpbWUgLyAiZGlzcGxheS1jb250cm9sLnNvY2siCiAgICAgICAgaWYgcGF0 +aC5leGlzdHMoKToKICAgICAgICAgICAgcGF0aC51bmxpbmsoKQoKICAgICAgICBjbGFzcyBIYW5kbGVyKHNvY2tldHNlcnZlci5TdHJlYW1SZXF1ZXN0SGFu +ZGxlcik6CiAgICAgICAgICAgIGRlZiBoYW5kbGUoc2VsZik6CiAgICAgICAgICAgICAgICBzZWxmLmNvbm5lY3Rpb24uc2V0dGltZW91dCg2MCkKICAgICAg +ICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgICAgICByYXcgPSBzZWxmLnJmaWxlLnJlYWRsaW5lKDQwOTcpCiAgICAgICAgICAgICAgICAgICAgaWYg +bGVuKHJhdykgPiA0MDk2OgogICAgICAgICAgICAgICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJkaXNwbGF5IHJlcXVlc3QgdG9vIGxhcmdlIikKICAg +ICAgICAgICAgICAgICAgICByZXN1bHQgPSBzZXJ2aWNlLmhhbmRsZShqc29uLmxvYWRzKHJhdykpCiAgICAgICAgICAgICAgICBleGNlcHQgKFZhbHVlRXJy +b3IsIEtleUVycm9yLCBUeXBlRXJyb3IsIE9TRXJyb3IpOgogICAgICAgICAgICAgICAgICAgIHJlc3VsdCA9IHsiZXJyb3IiOiAiZGlzcGxheV9yZXF1ZXN0 +X2ZhaWxlZCJ9CiAgICAgICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICAgICAgc2VsZi53ZmlsZS53cml0ZShqc29uLmR1bXBzKHJlc3VsdCkuZW5j +b2RlKCkgKyBiIlxuIikKICAgICAgICAgICAgICAgIGV4Y2VwdCBPU0Vycm9yOgogICAgICAgICAgICAgICAgICAgIHBhc3MgICMgVGhlIHBlcnNpc3RlZCBy +ZWNlaXB0IHJlbWFpbnMgcmVwbGF5YWJsZSBhZnRlciBkaXNjb25uZWN0LgoKICAgICAgICBjbGFzcyBTZXJ2ZXIoc29ja2V0c2VydmVyLlRocmVhZGluZ1Vu +aXhTdHJlYW1TZXJ2ZXIpOgogICAgICAgICAgICBkYWVtb25fdGhyZWFkcyA9IFRydWUKCiAgICAgICAgcHJldmlvdXNfdW1hc2sgPSBvcy51bWFzaygwbzA3 +NykKICAgICAgICB0cnk6CiAgICAgICAgICAgIHdpdGggU2VydmVyKHN0cihwYXRoKSwgSGFuZGxlcikgYXMgc2VydmVyOgogICAgICAgICAgICAgICAgb3Mu +dW1hc2socHJldmlvdXNfdW1hc2spCiAgICAgICAgICAgICAgICBzZXJ2ZXIuc2VydmVfZm9yZXZlcigpCiAgICAgICAgZmluYWxseToKICAgICAgICAgICAg +b3MudW1hc2socHJldmlvdXNfdW1hc2spCiAgICAgICAgICAgIHNlcnZpY2Uuc2h1dGRvd24uc2V0KCkKICAgICAgICAgICAgcGF0aC51bmxpbmsobWlzc2lu +Z19vaz1UcnVlKQoKCmRlZiBtYWluKCk6CiAgICBwYXJzZXIgPSBhcmdwYXJzZS5Bcmd1bWVudFBhcnNlcihkZXNjcmlwdGlvbj1fX2RvY19fKQogICAgcGFy +c2VyLmFkZF9hcmd1bWVudCgiYWN0aW9uIiwgY2hvaWNlcz1bImxpc3QiLCAiY3JlYXRlIiwgInNlcnZlIl0pCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50KCIt +LXJlcXVlc3QtaWQiKQogICAgYXJncyA9IHBhcnNlci5wYXJzZV9hcmdzKCkKICAgIHJ1bnRpbWUgPSBvcy5lbnZpcm9uLmdldCgiQ01VWF9ERVNLVE9QX1JV +TlRJTUVfRElSIiwgIi9ydW4vY211eC1kZXNrdG9wIikKICAgIGlmIGFyZ3MuYWN0aW9uID09ICJzZXJ2ZSI6CiAgICAgICAgc2VydmUocnVudGltZSkKICAg +ICAgICByZXR1cm4gMAogICAgaWYgYXJncy5hY3Rpb24gPT0gImNyZWF0ZSIgYW5kIG5vdCBhcmdzLnJlcXVlc3RfaWQ6CiAgICAgICAgcGFyc2VyLmVycm9y +KCJjcmVhdGUgcmVxdWlyZXMgLS1yZXF1ZXN0LWlkIChyZXRhaW4gaXQgd2hlbiByZXRyeWluZykiKQogICAgdHJ5OgogICAgICAgIHdpdGggc29ja2V0LnNv +Y2tldChzb2NrZXQuQUZfVU5JWCwgc29ja2V0LlNPQ0tfU1RSRUFNKSBhcyBjbGllbnQ6CiAgICAgICAgICAgIGNsaWVudC5zZXR0aW1lb3V0KDYwKQogICAg +ICAgICAgICBjbGllbnQuY29ubmVjdChzdHIoUGF0aChydW50aW1lKSAvICJkaXNwbGF5LWNvbnRyb2wuc29jayIpKQogICAgICAgICAgICBjbGllbnQuc2Vu +ZGFsbChqc29uLmR1bXBzKHsiYWN0aW9uIjogYXJncy5hY3Rpb24sICJyZXF1ZXN0IjogYXJncy5yZXF1ZXN0X2lkfSkuZW5jb2RlKCkgKyBiIlxuIikKICAg +ICAgICAgICAgd2l0aCBjbGllbnQubWFrZWZpbGUoInJiIikgYXMgc3RyZWFtOgogICAgICAgICAgICAgICAgcmVzdWx0ID0ganNvbi5sb2FkcyhzdHJlYW0u +cmVhZGxpbmUoNjU1MzYpKQogICAgZXhjZXB0IChPU0Vycm9yLCBWYWx1ZUVycm9yKToKICAgICAgICByZXN1bHQgPSB7ImVycm9yIjogImRpc3BsYXlfcnVu +dGltZV91bmF2YWlsYWJsZSJ9CiAgICBwcmludChqc29uLmR1bXBzKHJlc3VsdCkpCiAgICByZXR1cm4gMSBpZiAiZXJyb3IiIGluIHJlc3VsdCBlbHNlIDAK +CgppZiBfX25hbWVfXyA9PSAiX19tYWluX18iOgogICAgcmFpc2UgU3lzdGVtRXhpdChtYWluKCkpCg== """ private static let path = "$HOME/.cmux/cmux-display" diff --git a/Sources/Cloud/PortForward/CloudBrowserAccessState.swift b/Sources/Cloud/PortForward/CloudBrowserAccessState.swift index 1e04d1b24ec9..ecffd9ca5507 100644 --- a/Sources/Cloud/PortForward/CloudBrowserAccessState.swift +++ b/Sources/Cloud/PortForward/CloudBrowserAccessState.swift @@ -181,6 +181,7 @@ final class CloudBrowserAccessState { desktopConnected = false activeNavigationID = nil connectionDeadline.cancel() + startDeadline() attempt += 1 trace("configured") // Reconfiguration invalidates the previous observation generation. @@ -191,7 +192,6 @@ final class CloudBrowserAccessState { func nextURL() -> URL? { guard let remoteURL, let url = model?.url(for: remoteURL) else { - connectionDeadline.cancel() hasCommittedNavigation = false desktopConnected = false navigationURL = nil @@ -204,7 +204,6 @@ final class CloudBrowserAccessState { error = nil desktopFailure = nil dismissedFailure = nil - connectionDeadline.cancel() startDeadline() loaded = false trace("route_ready") @@ -281,6 +280,7 @@ final class CloudBrowserAccessState { desktopConnected = false activeNavigationID = nil connectionDeadline.cancel() + startDeadline() model?.retry() observeRoute() } diff --git a/Sources/Panels/BrowserPanel+CloudConnection.swift b/Sources/Panels/BrowserPanel+CloudConnection.swift index e751f21048f5..f378f5a85dcc 100644 --- a/Sources/Panels/BrowserPanel+CloudConnection.swift +++ b/Sources/Panels/BrowserPanel+CloudConnection.swift @@ -7,6 +7,7 @@ extension BrowserPanel { /// editing the remote workspace layout while removing stale restore /// provenance from this panel. func leaveCloudResourceForLocalNavigation() { + pendingCloudRestoreURL = nil if cloudAccess.retainsCloudResourceForDuplication { SurfaceCatalog.shared.endProjections(panelID: id, reason: .replaced) } @@ -25,6 +26,7 @@ extension BrowserPanel { /// Restore by stable resource identity before loading any saved address. /// A stale/unknown provider leaves an owned placeholder, never a local page. func restoreCloudResource(_ resource: SurfaceResourceID, preferredURL: URL? = nil) { + pendingCloudRestoreURL = preferredURL let catalog = SurfaceCatalog.shared do { try catalog.validateOwnership(of: [resource], at: .workspace(id: workspaceId, placement: .tab)) } catch { cloudAccess.showUnavailable(SurfaceTransferRejection.cloudMachineMismatch.message); return } @@ -39,7 +41,12 @@ extension BrowserPanel { switch CloudPortRoutePlan.plan(resource: known, privateAddress: provider.info.privateAddress) { case .privateDirect(let raw): if let url = URL(string: raw) { - provider.configureBrowser(self, url: Self.cloudRestoredURL(preferredURL, on: url), resourceID: resource) + let configured = provider.configureBrowser( + self, + url: Self.cloudRestoredURL(pendingCloudRestoreURL, on: url), + resourceID: resource + ) + if configured { pendingCloudRestoreURL = nil } } case .unsupported(let message): cloudAccess.showUnavailable(message) } @@ -54,6 +61,10 @@ extension BrowserPanel { return components.url ?? target } + func cloudRestoreURL(on target: URL) -> URL { + Self.cloudRestoredURL(pendingCloudRestoreURL ?? currentURLForTabDuplication, on: target) + } + /// Cloud panes use their own persistent data store so configuring one VM cannot reroute another. func prepareCloudBrowserStore(machineID: String) { let identifier = CloudBrowserRouting.storeID(panelID: id, profileID: profileID, machineID: machineID) diff --git a/Sources/Panels/BrowserPanel.swift b/Sources/Panels/BrowserPanel.swift index ac2ec6631d11..6e81b0356088 100644 --- a/Sources/Panels/BrowserPanel.swift +++ b/Sources/Panels/BrowserPanel.swift @@ -1988,6 +1988,9 @@ final class BrowserPanel: Panel, ObservableObject { var cloudBrowserMachineID: String? var cloudBrowserStoreIdentity: UUID? var cloudBrowserProxyEndpoint: CloudBrowserProxyEndpoint? + /// Saved Cloud path waiting for a provider/resource to become available. + /// It is consumed after the first successful authenticated configuration. + var pendingCloudRestoreURL: URL? /// The workspace ID this panel belongs to private(set) var workspaceId: UUID diff --git a/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift b/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift index f3c1c7226e1c..f3b8e5a6cb30 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift @@ -8,7 +8,10 @@ extension CmuxTuiSurfaceProvider { guard let browser = SurfacePaneFactory.browserPanel(panelID: projection.panelID, in: projection.workspaceID) else { continue } switch CloudPortRoutePlan.plan(resource: resource, privateAddress: info.privateAddress) { case .privateDirect(let raw): - if let url = URL(string: raw) { configureBrowser(browser, url: url, resourceID: resource.id) } + if let url = URL(string: raw) { + let configured = configureBrowser(browser, url: browser.cloudRestoreURL(on: url), resourceID: resource.id) + if configured { browser.pendingCloudRestoreURL = nil } + } case .unsupported(let message): browser.cloudAccess.showUnavailable(message) } @@ -42,7 +45,8 @@ extension CmuxTuiSurfaceProvider { } /// Bind the page to its machine proxy without activating a system VPN. - func configureBrowser(_ browser: BrowserPanel, url: URL, resourceID: SurfaceResourceID? = nil) { + @discardableResult + func configureBrowser(_ browser: BrowserPanel, url: URL, resourceID: SurfaceResourceID? = nil) -> Bool { let requestedPort = url.port ?? (url.scheme?.lowercased() == "https" ? 443 : 80) let fallbackID: SurfaceResourceID = if info.hasDesktop, (CmuxTuiSnapshotParser.desktopPort...6916).contains(requestedPort) { SurfaceResourceID(machine: machine, kind: .display, key: "display:\(requestedPort - 6900)") @@ -56,18 +60,18 @@ extension CmuxTuiSurfaceProvider { browser.cloudAccess.resourceID?.machine == nil || browser.cloudAccess.resourceID?.machine == machine, (try? catalog.validateOwnership(of: [resourceID], at: .workspace(id: browser.workspaceId, placement: .tab))) != nil else { browser.cloudAccess.showUnavailable(SurfaceTransferRejection.cloudMachineMismatch.message) - return + return false } guard let address = info.privateAddress, let privateURL = CloudPortRoutePlan.privateURL(url.absoluteString, address: address) else { browser.cloudAccess.showUnavailable(String(localized: "cloud.portAccess.invalidURL", defaultValue: "This port does not have a valid HTTP or HTTPS address.")) - return + return false } // Check the VM origin before rewriting it to localhost. Otherwise the // implicit localhost allowance could bypass a private-origin deny rule. guard browserPolicy().allowsTrustedInternalURL(privateURL) else { browser.cloudAccess.showUnavailable(String(localized: "browser.error.urlAllowlist.userMessage", defaultValue: "This URL is not allowed by the embedded-browser URL policy.")) - return + return false } let port = privateURL.port ?? (privateURL.scheme?.lowercased() == "https" ? 443 : 80) browser.webView.stopLoading() @@ -79,6 +83,7 @@ extension CmuxTuiSurfaceProvider { model.connect() browser.cloudAccess.routeDidConfigure() materializedPanels.insert(browser.id) + return true } func accessModel(port: Int, address: String, scheme: String = "http") -> CloudPortAccessModel { @@ -141,8 +146,15 @@ extension CmuxTuiSurfaceProvider { switch CloudPortRoutePlan.plan(resource: resource, privateAddress: info.privateAddress) { case .privateDirect(let raw): guard let url = URL(string: raw) else { continue } - configureBrowser(browser, url: url, resourceID: resource.id) - materializedPanels.insert(projection.panelID) + let configured = configureBrowser( + browser, + url: browser.cloudRestoreURL(on: url), + resourceID: resource.id + ) + if configured { + browser.pendingCloudRestoreURL = nil + materializedPanels.insert(projection.panelID) + } case .unsupported: // Keep the placeholder eligible for a later explicit display // discovery; its target may be supplied by the guest catalog. diff --git a/tests/fixtures/cmux-display b/tests/fixtures/cmux-display index b8a087d4e348..b777349e5626 100755 --- a/tests/fixtures/cmux-display +++ b/tests/fixtures/cmux-display @@ -191,6 +191,20 @@ class DisplayService: except OSError: pass + @staticmethod + def scoped_component_path(executable, path, number, runtime): + """Give per-display desktop clients names the :1 supervisor cannot match.""" + source = Path(path) + if not source.is_file(): + return path + directory = Path(runtime) / "bin" + directory.mkdir(mode=0o700, parents=True, exist_ok=True) + target = directory / f"cmux-display-{number}-{source.name}" + if not target.exists() or target.stat().st_mtime_ns < source.stat().st_mtime_ns: + shutil.copy2(source, target) + target.chmod(0o700) + return str(target) + @staticmethod def wait_for_port(port, timeout=20): deadline = time.monotonic() + timeout @@ -266,6 +280,7 @@ class DisplayService: if executable == "vncconfig": continue raise RuntimeError(f"missing display component: {executable}") + path = self.scoped_component_path(executable, path, number, runtime) process = subprocess.Popen([path] + args, env=environment, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) From 9928173daecc83366ad02fd355a40aaf973f4dfd Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 13:38:49 -0700 Subject: [PATCH 34/64] fix: require discovered guest display resources --- .../Surfaces/CmuxTuiSurfaceProvider+Displays.swift | 10 ++++++++-- .../CmuxTuiSurfaceProvider+PortForward.swift | 14 +++++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Sources/Surfaces/CmuxTuiSurfaceProvider+Displays.swift b/Sources/Surfaces/CmuxTuiSurfaceProvider+Displays.swift index e3a3459d3dad..704bb0bb47e0 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProvider+Displays.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProvider+Displays.swift @@ -22,7 +22,7 @@ extension CmuxTuiSurfaceProvider { let generation = currentLifecycleGeneration let refresh = refreshGeneration await displayCoordinator.refresh() - guard isCurrentRefresh(lifecycle: generation, refresh: refresh), displayCoordinator.isAvailable else { return } + guard isCurrentRefresh(lifecycle: generation, refresh: refresh) else { return } publishDisplays() } @@ -41,7 +41,13 @@ extension CmuxTuiSurfaceProvider { } private func publishDisplays() { - for var resource in displayResources { + let resources = displayResources + let desiredIDs = Set(resources.map(\.id)) + for resource in catalog.snapshot.resources(on: machine) + where resource.kind == .display && !desiredIDs.contains(resource.id) { + catalog.remove(resource.id, from: self) + } + for var resource in resources { // Guest discovery owns the connection, while the daemon/catalog // owns existing view placements. Refresh must preserve both. if let existing = catalog.resources[resource.id] { diff --git a/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift b/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift index f3b8e5a6cb30..479f197bbef5 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift @@ -53,9 +53,17 @@ extension CmuxTuiSurfaceProvider { } else { SurfaceResourceID(machine: machine, kind: .browser, key: "port:\(requestedPort)") } - let resourceID = resourceID ?? browser.cloudAccess.resourceID - ?? catalog.projectionRecord(forPanel: browser.id).flatMap { $0.resource.machine.isLocal ? nil : $0.resource } - ?? fallbackID + let projectedResource = catalog.projectionRecord(forPanel: browser.id).flatMap { + $0.resource.machine.isLocal ? nil : $0.resource + } + let explicitResource = resourceID ?? browser.cloudAccess.resourceID ?? projectedResource + if explicitResource == nil, fallbackID.kind == .display, + fallbackID.key != SurfaceResourceID.desktopDisplayKey, + catalog.resources[fallbackID] == nil { + browser.cloudAccess.showUnavailable(CloudGuestDisplaySnapshot.unavailableMessage) + return false + } + let resourceID = explicitResource ?? fallbackID guard resourceID.machine == machine, browser.cloudAccess.resourceID?.machine == nil || browser.cloudAccess.resourceID?.machine == machine, (try? catalog.validateOwnership(of: [resourceID], at: .workspace(id: browser.workspaceId, placement: .tab))) != nil else { From a9a948ac17f2c106ce8f86101e12795963c0a8fd Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 14:25:41 -0700 Subject: [PATCH 35/64] test: cover guest component recovery --- tests/test_cloud_display_catalog.py | 40 ++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/tests/test_cloud_display_catalog.py b/tests/test_cloud_display_catalog.py index 3c2d54b60698..724ac947da9a 100644 --- a/tests/test_cloud_display_catalog.py +++ b/tests/test_cloud_display_catalog.py @@ -3,6 +3,7 @@ import importlib.machinery import importlib.util import json +import os import sys from pathlib import Path import tempfile @@ -104,7 +105,7 @@ def spawn(_command, **options): return Process() def launch(_command, **_options): - return mock.Mock(stdout=f"DBUS_SESSION_BUS_ADDRESS='unix:path=/tmp/bus-{len(environments)}'; export DBUS_SESSION_BUS_ADDRESS;\nDBUS_SESSION_BUS_PID=999999; export DBUS_SESSION_BUS_PID;\n") + return mock.Mock(stdout=f"DBUS_SESSION_BUS_ADDRESS='unix:path=/tmp/bus-{len(environments)}'; export DBUS_SESSION_BUS_ADDRESS;\nDBUS_SESSION_BUS_PID={os.getpid()}; export DBUS_SESSION_BUS_PID;\n") readiness_calls = {} def readiness(number): @@ -165,6 +166,43 @@ def terminate(self): self.assertIs(service.websockify_processes[number], new_websockify) service.shutdown.set() + def test_crashed_session_component_restarts_without_restarting_x(self): + service = display.DisplayService(self.catalog(), self.root / "runtime") + number = 2 + + class Process: + def __init__(self, exit_code=None): + self.exit_code = exit_code + self.terminated = False + + def terminate(self): + self.terminated = True + + def poll(self): + return self.exit_code + + x_server = Process() + dbus = Process() + crashed_openbox = Process(1) + replacement = Process() + service.processes[number] = [x_server, dbus, crashed_openbox] + service.named_processes[number] = {"xvnc": x_server, "dbus": dbus, "openbox": crashed_openbox} + service.websockify_processes[number] = Process() + environment = {"DISPLAY": ":2"} + runtime = self.root / "runtime" / "2" + runtime.mkdir(parents=True) + + with mock.patch.object(display, "ready", return_value=False), \ + mock.patch.object(display, "rfb_ready", return_value=True), \ + mock.patch.object(display, "novnc_ready", return_value=True), \ + mock.patch.object(display.shutil, "which", side_effect=lambda name: name), \ + mock.patch.object(display.subprocess, "Popen", return_value=replacement): + service.start_components(number, environment, runtime) + + self.assertFalse(x_server.terminated) + self.assertIs(service.named_processes[number]["openbox"], replacement) + service.shutdown.set() + def test_additional_desktop_clients_use_display_scoped_process_names(self): service = display.DisplayService(self.catalog(), self.root / "runtime") source = self.root / "openbox" From bed6163b9760c0b79b860df1eb0b1aeb385ae317 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 14:26:33 -0700 Subject: [PATCH 36/64] fix: preserve Cloud provenance and supervise displays --- Sources/Cloud/CloudGuestDisplayScript.swift | 273 ++++++++++-------- .../PortForward/CloudBrowserAccessState.swift | 15 +- Sources/DockSplitStore+BrowserActions.swift | 4 +- .../Panels/BrowserPanel+CloudConnection.swift | 17 +- .../CmuxTuiSurfaceProvider+PortForward.swift | 33 ++- .../Workspace+BrowserDuplication.swift | 4 +- tests/fixtures/cmux-display | 127 +++++--- 7 files changed, 298 insertions(+), 175 deletions(-) diff --git a/Sources/Cloud/CloudGuestDisplayScript.swift b/Sources/Cloud/CloudGuestDisplayScript.swift index 01204dd18930..4fd47b42430c 100644 --- a/Sources/Cloud/CloudGuestDisplayScript.swift +++ b/Sources/Cloud/CloudGuestDisplayScript.swift @@ -65,131 +65,154 @@ ICAgICAgICByZXR1cm4gMAoKCmNsYXNzIE5vVk5DVW5hdmFpbGFibGUoUnVudGltZUVycm9yKToKICAg ZWQgd2hpbGUgdGhlIGd1ZXN0IGRlc2t0b3AgbWF5IGJlIGhlYWx0aHkuIiIiCgoKY2xhc3MgRGlzcGxheVNlcnZpY2U6CiAgICBkZWYgX19pbml0X18oc2Vs ZiwgY2F0YWxvZywgcnVudGltZSwgc3RhcnRlcj1TVEFSVEVSKToKICAgICAgICBzZWxmLmNhdGFsb2cgPSBjYXRhbG9nCiAgICAgICAgc2VsZi5ydW50aW1l ID0gUGF0aChydW50aW1lKQogICAgICAgIHNlbGYuc3RhcnRlciA9IHN0YXJ0ZXIKICAgICAgICBzZWxmLmxvY2sgPSB0aHJlYWRpbmcuTG9jaygpCiAgICAg -ICAgc2VsZi5qb2JzID0ge30KICAgICAgICBzZWxmLnN0YXRlcyA9IHt9CiAgICAgICAgc2VsZi5wcm9jZXNzZXMgPSB7fQogICAgICAgIHNlbGYud2Vic29j -a2lmeV9wcm9jZXNzZXMgPSB7fQogICAgICAgIHNlbGYuc2h1dGRvd24gPSB0aHJlYWRpbmcuRXZlbnQoKQoKICAgIGRlZiBzdGFydChzZWxmLCBudW1iZXIp -OgogICAgICAgIHdpdGggc2VsZi5sb2NrOgogICAgICAgICAgICBpZiBudW1iZXIgaW4gc2VsZi5qb2JzOgogICAgICAgICAgICAgICAgcmV0dXJuIHNlbGYu -am9ic1tudW1iZXJdCiAgICAgICAgICAgIGZpbmlzaGVkID0gdGhyZWFkaW5nLkV2ZW50KCkKICAgICAgICAgICAgc2VsZi5qb2JzW251bWJlcl0gPSBmaW5p -c2hlZAogICAgICAgICAgICBzZWxmLnN0YXRlc1tudW1iZXJdID0gImxhdW5jaGluZyIKICAgICAgICAgICAgdGhyZWFkaW5nLlRocmVhZCh0YXJnZXQ9c2Vs -Zi5zdXBlcnZpc2UsIGFyZ3M9KG51bWJlciwgZmluaXNoZWQpLCBkYWVtb249VHJ1ZSkuc3RhcnQoKQogICAgICAgICAgICByZXR1cm4gZmluaXNoZWQKCiAg -ICBkZWYgdGVybWluYXRlKHNlbGYsIG51bWJlcik6CiAgICAgICAgZm9yIHByb2Nlc3MgaW4gc2VsZi5wcm9jZXNzZXMucG9wKG51bWJlciwgW10pOgogICAg -ICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICBwcm9jZXNzLnRlcm1pbmF0ZSgpCiAgICAgICAgICAgIGV4Y2VwdCBPU0Vycm9yOgogICAgICAgICAgICAg -ICAgcGFzcwogICAgICAgIHNlbGYudGVybWluYXRlX3dlYnNvY2tpZnkobnVtYmVyKQoKICAgIGRlZiB0ZXJtaW5hdGVfd2Vic29ja2lmeShzZWxmLCBudW1i -ZXIpOgogICAgICAgIHByb2Nlc3MgPSBzZWxmLndlYnNvY2tpZnlfcHJvY2Vzc2VzLnBvcChudW1iZXIsIE5vbmUpCiAgICAgICAgaWYgcHJvY2VzcyBpcyBO -b25lOgogICAgICAgICAgICByZXR1cm4KICAgICAgICB0cnk6CiAgICAgICAgICAgIHByb2Nlc3MudGVybWluYXRlKCkKICAgICAgICBleGNlcHQgT1NFcnJv -cjoKICAgICAgICAgICAgcGFzcwoKICAgIEBzdGF0aWNtZXRob2QKICAgIGRlZiBzY29wZWRfY29tcG9uZW50X3BhdGgoZXhlY3V0YWJsZSwgcGF0aCwgbnVt -YmVyLCBydW50aW1lKToKICAgICAgICAiIiJHaXZlIHBlci1kaXNwbGF5IGRlc2t0b3AgY2xpZW50cyBuYW1lcyB0aGUgOjEgc3VwZXJ2aXNvciBjYW5ub3Qg -bWF0Y2guIiIiCiAgICAgICAgc291cmNlID0gUGF0aChwYXRoKQogICAgICAgIGlmIG5vdCBzb3VyY2UuaXNfZmlsZSgpOgogICAgICAgICAgICByZXR1cm4g -cGF0aAogICAgICAgIGRpcmVjdG9yeSA9IFBhdGgocnVudGltZSkgLyAiYmluIgogICAgICAgIGRpcmVjdG9yeS5ta2Rpcihtb2RlPTBvNzAwLCBwYXJlbnRz -PVRydWUsIGV4aXN0X29rPVRydWUpCiAgICAgICAgdGFyZ2V0ID0gZGlyZWN0b3J5IC8gZiJjbXV4LWRpc3BsYXkte251bWJlcn0te3NvdXJjZS5uYW1lfSIK -ICAgICAgICBpZiBub3QgdGFyZ2V0LmV4aXN0cygpIG9yIHRhcmdldC5zdGF0KCkuc3RfbXRpbWVfbnMgPCBzb3VyY2Uuc3RhdCgpLnN0X210aW1lX25zOgog -ICAgICAgICAgICBzaHV0aWwuY29weTIoc291cmNlLCB0YXJnZXQpCiAgICAgICAgICAgIHRhcmdldC5jaG1vZCgwbzcwMCkKICAgICAgICByZXR1cm4gc3Ry -KHRhcmdldCkKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgd2FpdF9mb3JfcG9ydChwb3J0LCB0aW1lb3V0PTIwKToKICAgICAgICBkZWFkbGluZSA9IHRp -bWUubW9ub3RvbmljKCkgKyB0aW1lb3V0CiAgICAgICAgd2hpbGUgdGltZS5tb25vdG9uaWMoKSA8IGRlYWRsaW5lOgogICAgICAgICAgICBpZiBsaXN0ZW5p -bmcocG9ydCk6CiAgICAgICAgICAgICAgICByZXR1cm4gVHJ1ZQogICAgICAgICAgICB0aW1lLnNsZWVwKDAuMSkKICAgICAgICByZXR1cm4gRmFsc2UKCiAg -ICBkZWYgc3RhcnRfd2Vic29ja2lmeShzZWxmLCBudW1iZXIsIGVudmlyb25tZW50LCBleGVjdXRhYmxlKToKICAgICAgICBwYXRoID0gZXhlY3V0YWJsZSBp -ZiAiLyIgaW4gZXhlY3V0YWJsZSBlbHNlIHNodXRpbC53aGljaChleGVjdXRhYmxlKQogICAgICAgIGlmIG5vdCBwYXRoOgogICAgICAgICAgICByYWlzZSBO -b1ZOQ1VuYXZhaWxhYmxlKCJtaXNzaW5nIGRpc3BsYXkgY29tcG9uZW50OiB3ZWJzb2NraWZ5IikKICAgICAgICBzZWxmLnRlcm1pbmF0ZV93ZWJzb2NraWZ5 -KG51bWJlcikKICAgICAgICByZmJfcG9ydCwgbm92bmNfcG9ydCA9IDU5MDAgKyBudW1iZXIsIDY5MDAgKyBudW1iZXIKICAgICAgICBwcm9jZXNzID0gc3Vi -cHJvY2Vzcy5Qb3BlbigKICAgICAgICAgICAgW3BhdGgsICItLXdlYiIsICIvdXNyL3NoYXJlL25vdm5jIiwgIi0taGVhcnRiZWF0IiwgIjMwIiwKICAgICAg -ICAgICAgIGYiMTI3LjAuMC4xOntub3ZuY19wb3J0fSIsIGYiMTI3LjAuMC4xOntyZmJfcG9ydH0iXSwKICAgICAgICAgICAgZW52PWVudmlyb25tZW50LCBz -dGRpbj1zdWJwcm9jZXNzLkRFVk5VTEwsCiAgICAgICAgICAgIHN0ZG91dD1zdWJwcm9jZXNzLkRFVk5VTEwsIHN0ZGVycj1zdWJwcm9jZXNzLkRFVk5VTEwp -CiAgICAgICAgc2VsZi53ZWJzb2NraWZ5X3Byb2Nlc3Nlc1tudW1iZXJdID0gcHJvY2VzcwogICAgICAgIGlmIG5vdCBzZWxmLndhaXRfZm9yX3BvcnQobm92 -bmNfcG9ydCwgdGltZW91dD0xMCk6CiAgICAgICAgICAgIHNlbGYudGVybWluYXRlX3dlYnNvY2tpZnkobnVtYmVyKQogICAgICAgICAgICByYWlzZSBOb1ZO -Q1VuYXZhaWxhYmxlKCJub1ZOQyBkaWQgbm90IGJlY29tZSByZWFkeSIpCgogICAgZGVmIHN0YXJ0X2NvbXBvbmVudHMoc2VsZiwgbnVtYmVyLCBlbnZpcm9u -bWVudCwgcnVudGltZSk6CiAgICAgICAgaWYgcmVhZHkobnVtYmVyKToKICAgICAgICAgICAgcmV0dXJuCiAgICAgICAgcmZiX2lzX3JlYWR5ID0gcmZiX3Jl -YWR5KG51bWJlcikKICAgICAgICBzdGFydGVkX2Rlc2t0b3AgPSBGYWxzZQogICAgICAgIGlmIG5vdCByZmJfaXNfcmVhZHk6CiAgICAgICAgICAgIHNlbGYu -dGVybWluYXRlKG51bWJlcikKICAgICAgICAgICAgc3RhcnRlZF9kZXNrdG9wID0gVHJ1ZQogICAgICAgIHZuYyA9IHNodXRpbC53aGljaCgiWHZuYyIpIG9y -IHNodXRpbC53aGljaCgiWHRpZ2Vydm5jIikKICAgICAgICB3ZWJzb2NraWZ5ID0gc2h1dGlsLndoaWNoKCJ3ZWJzb2NraWZ5IikKICAgICAgICBpZiBub3Qg -d2Vic29ja2lmeToKICAgICAgICAgICAgcmFpc2UgTm9WTkNVbmF2YWlsYWJsZSgiZGlzcGxheSBydW50aW1lIHVuYXZhaWxhYmxlIikKICAgICAgICBpZiBu -b3QgcmZiX2lzX3JlYWR5IGFuZCBub3Qgdm5jOgogICAgICAgICAgICByYWlzZSBSdW50aW1lRXJyb3IoImRpc3BsYXkgcnVudGltZSB1bmF2YWlsYWJsZSIp -CiAgICAgICAgaWYgc3RhcnRlZF9kZXNrdG9wOgogICAgICAgICAgICByZmJfcG9ydCA9IDU5MDAgKyBudW1iZXIKICAgICAgICAgICAgcHJvY2Vzc2VzID0g -W10KICAgICAgICAgICAgcHJvY2Vzc2VzLmFwcGVuZChzdWJwcm9jZXNzLlBvcGVuKAogICAgICAgICAgICAgICAgW3ZuYywgZiI6e251bWJlcn0iLCAiLWdl -b21ldHJ5Iiwgb3MuZW52aXJvbi5nZXQoIkNNVVhfVk5DX0dFT01FVFJZIiwgIjE0NDB4OTAwIiksCiAgICAgICAgICAgICAgICAgIi1kZXB0aCIsICIyNCIs -ICItcmZicG9ydCIsIHN0cihyZmJfcG9ydCksICItbG9jYWxob3N0IiwKICAgICAgICAgICAgICAgICAiLVNlY3VyaXR5VHlwZXMiLCAiTm9uZSIsICItQWx3 -YXlzU2hhcmVkIl0sCiAgICAgICAgICAgICAgICBlbnY9ZW52aXJvbm1lbnQsIHN0ZGluPXN1YnByb2Nlc3MuREVWTlVMTCwKICAgICAgICAgICAgICAgIHN0 -ZG91dD1zdWJwcm9jZXNzLkRFVk5VTEwsIHN0ZGVycj1zdWJwcm9jZXNzLkRFVk5VTEwpKQogICAgICAgICAgICBzZWxmLnByb2Nlc3Nlc1tudW1iZXJdID0g -cHJvY2Vzc2VzCiAgICAgICAgICAgIGlmIG5vdCBzZWxmLndhaXRfZm9yX3BvcnQocmZiX3BvcnQpOgogICAgICAgICAgICAgICAgcmFpc2UgUnVudGltZUVy -cm9yKCJYdm5jIGRpZCBub3QgYmVjb21lIHJlYWR5IikKICAgICAgICAgICAgYnVzID0gc3VicHJvY2Vzcy5ydW4oWyJkYnVzLWxhdW5jaCIsICItLXNoLXN5 -bnRheCJdLCBlbnY9ZW52aXJvbm1lbnQsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0ZGluPXN1YnByb2Nlc3MuREVWTlVMTCwgc3Rkb3V0 -PXN1YnByb2Nlc3MuUElQRSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3RkZXJyPXN1YnByb2Nlc3MuREVWTlVMTCwgdGV4dD1UcnVlLCB0 -aW1lb3V0PTEwLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjaGVjaz1UcnVlKS5zdGRvdXQKICAgICAgICAgICAgZm9yIGxpbmUgaW4gYnVz -LnNwbGl0bGluZXMoKToKICAgICAgICAgICAgICAgIGlmICI9IiBub3QgaW4gbGluZToKICAgICAgICAgICAgICAgICAgICBjb250aW51ZQogICAgICAgICAg -ICAgICAga2V5LCB2YWx1ZSA9IGxpbmUuc3BsaXQoIj0iLCAxKQogICAgICAgICAgICAgICAgZW52aXJvbm1lbnRba2V5XSA9IHZhbHVlLnN0cmlwKCkucnN0 -cmlwKCI7Iikuc3RyaXAoKS5zdHJpcCgiJyIpCiAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgIGRidXNfcGlkID0gaW50KGVudmlyb25tZW50Lmdl -dCgiREJVU19TRVNTSU9OX0JVU19QSUQiLCAiMCIpKQogICAgICAgICAgICBleGNlcHQgVmFsdWVFcnJvcjoKICAgICAgICAgICAgICAgIGRidXNfcGlkID0g -MAogICAgICAgICAgICBpZiBkYnVzX3BpZCA+IDE6CiAgICAgICAgICAgICAgICBwcm9jZXNzZXMuYXBwZW5kKE93bmVkUHJvY2VzcyhkYnVzX3BpZCkpCiAg -ICAgICAgICAgIGZvciBleGVjdXRhYmxlLCBhcmdzIGluIFsKICAgICAgICAgICAgICAgICgib3BlbmJveCIsIFtdKSwgKCJ0aW50MiIsIFsiLWMiLCAiL2V0 -Yy9jbXV4L3RpbnQycmMiXSksCiAgICAgICAgICAgICAgICAoInZuY2NvbmZpZyIsIFsiLW5vd2luIl0pLAogICAgICAgICAgICBdOgogICAgICAgICAgICAg -ICAgcGF0aCA9IGV4ZWN1dGFibGUgaWYgIi8iIGluIGV4ZWN1dGFibGUgZWxzZSBzaHV0aWwud2hpY2goZXhlY3V0YWJsZSkKICAgICAgICAgICAgICAgIGlm -IG5vdCBwYXRoOgogICAgICAgICAgICAgICAgICAgIGlmIGV4ZWN1dGFibGUgPT0gInZuY2NvbmZpZyI6CiAgICAgICAgICAgICAgICAgICAgICAgIGNvbnRp -bnVlCiAgICAgICAgICAgICAgICAgICAgcmFpc2UgUnVudGltZUVycm9yKGYibWlzc2luZyBkaXNwbGF5IGNvbXBvbmVudDoge2V4ZWN1dGFibGV9IikKICAg -ICAgICAgICAgICAgIHBhdGggPSBzZWxmLnNjb3BlZF9jb21wb25lbnRfcGF0aChleGVjdXRhYmxlLCBwYXRoLCBudW1iZXIsIHJ1bnRpbWUpCiAgICAgICAg -ICAgICAgICBwcm9jZXNzID0gc3VicHJvY2Vzcy5Qb3BlbihbcGF0aF0gKyBhcmdzLCBlbnY9ZW52aXJvbm1lbnQsCiAgICAgICAgICAgICAgICAgICAgICAg -ICAgICAgICAgICAgICAgICAgICBzdGRpbj1zdWJwcm9jZXNzLkRFVk5VTEwsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBz -dGRvdXQ9c3VicHJvY2Vzcy5ERVZOVUxMLCBzdGRlcnI9c3VicHJvY2Vzcy5ERVZOVUxMKQogICAgICAgICAgICAgICAgcHJvY2Vzc2VzLmFwcGVuZChwcm9j -ZXNzKQogICAgICAgIGlmIHN0YXJ0ZWRfZGVza3RvcCBvciBub3Qgbm92bmNfcmVhZHkobnVtYmVyKToKICAgICAgICAgICAgc2VsZi5zdGFydF93ZWJzb2Nr -aWZ5KG51bWJlciwgZW52aXJvbm1lbnQsIHdlYnNvY2tpZnkpCiAgICAgICAgKHJ1bnRpbWUgLyAiZW52Iikud3JpdGVfdGV4dChmImV4cG9ydCBESVNQTEFZ -PTp7bnVtYmVyfVxuIikKCiAgICBkZWYgc3VwZXJ2aXNlKHNlbGYsIG51bWJlciwgZmluaXNoZWQpOgogICAgICAgIHJ1bnRpbWUgPSBzZWxmLnJ1bnRpbWUg -LyBzdHIobnVtYmVyKQogICAgICAgIHJ1bnRpbWUubWtkaXIobW9kZT0wbzcwMCwgcGFyZW50cz1UcnVlLCBleGlzdF9vaz1UcnVlKQogICAgICAgIGVudmly -b25tZW50ID0gZGljdChvcy5lbnZpcm9uLCBESVNQTEFZPWYiOntudW1iZXJ9IiwgQ01VWF9ERVNLVE9QX1JVTlRJTUVfRElSPXN0cihydW50aW1lKSkKICAg -ICAgICBlbnZpcm9ubWVudFsiQ0hST01FX0NPTkZJR19IT01FIl0gPSBzdHIocnVudGltZSAvICJjaHJvbWUtY29uZmlnIikKICAgICAgICBlbnZpcm9ubWVu -dFsiWERHX0NPTkZJR19IT01FIl0gPSBzdHIocnVudGltZSAvICJ4ZGctY29uZmlnIikKICAgICAgICBmb3Iga2V5IGluICgiTk9USUZZX1NPQ0tFVCIsICJE -QlVTX1NFU1NJT05fQlVTX0FERFJFU1MiLCAiREJVU19TRVNTSU9OX0JVU19QSUQiLAogICAgICAgICAgICAgICAgICAgICJBVF9TUElfQlVTX0FERFJFU1Mi -LCAiQVRfU1BJX0JVUyIpOgogICAgICAgICAgICBlbnZpcm9ubWVudC5wb3Aoa2V5LCBOb25lKQogICAgICAgIHdoaWxlIG5vdCBzZWxmLnNodXRkb3duLmlz -X3NldCgpOgogICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICAjIFRoZSBzdXBlcnZpc29yIGlzIGEgaGVhbHRoIGNoZWNrLCBub3QgYSB0aW1lci1k -cml2ZW4gcmVzdGFydC4KICAgICAgICAgICAgICAgICMgUHJlc2VydmUgYW4gYWN0aXZlIFJGQi9ub1ZOQyBzZXNzaW9uIHVudGlsIGl0cyBvd25lZCBlbmRw -b2ludHMgZmFpbC4KICAgICAgICAgICAgICAgIGlmIG5vdCBzZWxmLnByb2Nlc3Nlcy5nZXQobnVtYmVyKSBvciBub3QgcmVhZHkobnVtYmVyKToKICAgICAg -ICAgICAgICAgICAgICBzZWxmLnN0YXJ0X2NvbXBvbmVudHMobnVtYmVyLCBlbnZpcm9ubWVudCwgcnVudGltZSkKICAgICAgICAgICAgICAgIHN0YXRlID0g -InJ1bm5pbmciIGlmIHJlYWR5KG51bWJlcikgZWxzZSAidW5hdmFpbGFibGUiCiAgICAgICAgICAgIGV4Y2VwdCBOb1ZOQ1VuYXZhaWxhYmxlOgogICAgICAg -ICAgICAgICAgc2VsZi50ZXJtaW5hdGVfd2Vic29ja2lmeShudW1iZXIpCiAgICAgICAgICAgICAgICBzdGF0ZSA9ICJ1bmF2YWlsYWJsZSIKICAgICAgICAg -ICAgZXhjZXB0IChPU0Vycm9yLCBSdW50aW1lRXJyb3IsIHN1YnByb2Nlc3MuU3VicHJvY2Vzc0Vycm9yKToKICAgICAgICAgICAgICAgIHNlbGYudGVybWlu -YXRlKG51bWJlcikKICAgICAgICAgICAgICAgIHN0YXRlID0gInVuYXZhaWxhYmxlIgogICAgICAgICAgICB3aXRoIHNlbGYubG9jazoKICAgICAgICAgICAg -ICAgIHNlbGYuc3RhdGVzW251bWJlcl0gPSBzdGF0ZQogICAgICAgICAgICBmaW5pc2hlZC5zZXQoKQogICAgICAgICAgICBzZWxmLnNodXRkb3duLndhaXQo -MzApCiAgICAgICAgc2VsZi50ZXJtaW5hdGUobnVtYmVyKQoKICAgIGRlZiBzbmFwc2hvdChzZWxmKToKICAgICAgICB3aXRoIHNlbGYubG9jazoKICAgICAg -ICAgICAgcmV0dXJuIHsidmVyc2lvbiI6IDEsICJjYW5DcmVhdGUiOiBsZW4oc2VsZi5qb2JzKSA8IE1BWF9ESVNQTEFZUyAtIDEsCiAgICAgICAgICAgICAg -ICAgICAgImRpc3BsYXlzIjogW2Rlc2NyaXB0b3IoMSwgInJ1bm5pbmciIGlmIHJlYWR5KDEpIGVsc2UgInVuYXZhaWxhYmxlIildCiAgICAgICAgICAgICAg -ICAgICAgKyBbZGVzY3JpcHRvcihuLCBzZWxmLnN0YXRlcy5nZXQobiwgImxhdW5jaGluZyIpKSBmb3IgbiBpbiBzZWxmLmNhdGFsb2cubnVtYmVycygpXX0K -CiAgICBkZWYgaGFuZGxlKHNlbGYsIHJlcXVlc3QpOgogICAgICAgIGlmIHJlcXVlc3QuZ2V0KCJhY3Rpb24iKSA9PSAibGlzdCI6CiAgICAgICAgICAgIHJl -dHVybiBzZWxmLnNuYXBzaG90KCkKICAgICAgICBpZiByZXF1ZXN0LmdldCgiYWN0aW9uIikgIT0gImNyZWF0ZSI6CiAgICAgICAgICAgIHJhaXNlIFZhbHVl -RXJyb3IoInVuc3VwcG9ydGVkIGRpc3BsYXkgYWN0aW9uIikKICAgICAgICBudW1iZXIgPSBzZWxmLmNhdGFsb2cuYWxsb2NhdGUocmVxdWVzdC5nZXQoInJl -cXVlc3QiKSkKICAgICAgICBzZWxmLnN0YXJ0KG51bWJlcikud2FpdCg1NSkKICAgICAgICByZXN1bHQgPSBzZWxmLnNuYXBzaG90KCkKICAgICAgICByZXN1 -bHRbImNyZWF0ZWQiXSA9IGYiZGlzcGxheTp7bnVtYmVyfSIKICAgICAgICBpZiBzZWxmLnN0YXRlcy5nZXQobnVtYmVyKSAhPSAicnVubmluZyI6CiAgICAg -ICAgICAgIHJlc3VsdFsiZXJyb3IiXSA9ICJkaXNwbGF5X3N0YXJ0X2ZhaWxlZCIKICAgICAgICByZXR1cm4gcmVzdWx0CgoKZGVmIHNlcnZlKHJ1bnRpbWUp -OgogICAgcnVudGltZSA9IFBhdGgocnVudGltZSkKICAgIHJ1bnRpbWUubWtkaXIobW9kZT0wbzcwMCwgcGFyZW50cz1UcnVlLCBleGlzdF9vaz1UcnVlKQog -ICAgd2l0aCAocnVudGltZSAvICJkaXNwbGF5LWNvbnRyb2wubG9jayIpLm9wZW4oInciKSBhcyBsb2NrOgogICAgICAgIHRyeToKICAgICAgICAgICAgZmNu -dGwuZmxvY2sobG9jaywgZmNudGwuTE9DS19FWCB8IGZjbnRsLkxPQ0tfTkIpCiAgICAgICAgZXhjZXB0IEJsb2NraW5nSU9FcnJvcjoKICAgICAgICAgICAg -cmV0dXJuCiAgICAgICAgaWYgbm90IGFsbChzaHV0aWwud2hpY2gobmFtZSkgZm9yIG5hbWUgaW4gKCJ3ZWJzb2NraWZ5IiwgIm9wZW5ib3giLCAiZGJ1cy1s -YXVuY2giKSk6CiAgICAgICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoImRpc3BsYXkgcnVudGltZSB1bmF2YWlsYWJsZSIpCiAgICAgICAgaWYgbm90IChzaHV0 -aWwud2hpY2goIlh2bmMiKSBvciBzaHV0aWwud2hpY2goIlh0aWdlcnZuYyIpKToKICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJvcigiZGlzcGxheSBydW50 -aW1lIHVuYXZhaWxhYmxlIikKICAgICAgICBzZXJ2aWNlID0gRGlzcGxheVNlcnZpY2UoRGlzcGxheUNhdGFsb2coUGF0aC5ob21lKCkgLyAiLmNtdXgiIC8g -ImRpc3BsYXlzIiksIHJ1bnRpbWUgLyAiZGlzcGxheXMiKQogICAgICAgIGZvciBudW1iZXIgaW4gc2VydmljZS5jYXRhbG9nLm51bWJlcnMoKToKICAgICAg -ICAgICAgc2VydmljZS5zdGFydChudW1iZXIpCiAgICAgICAgcGF0aCA9IHJ1bnRpbWUgLyAiZGlzcGxheS1jb250cm9sLnNvY2siCiAgICAgICAgaWYgcGF0 -aC5leGlzdHMoKToKICAgICAgICAgICAgcGF0aC51bmxpbmsoKQoKICAgICAgICBjbGFzcyBIYW5kbGVyKHNvY2tldHNlcnZlci5TdHJlYW1SZXF1ZXN0SGFu -ZGxlcik6CiAgICAgICAgICAgIGRlZiBoYW5kbGUoc2VsZik6CiAgICAgICAgICAgICAgICBzZWxmLmNvbm5lY3Rpb24uc2V0dGltZW91dCg2MCkKICAgICAg -ICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgICAgICByYXcgPSBzZWxmLnJmaWxlLnJlYWRsaW5lKDQwOTcpCiAgICAgICAgICAgICAgICAgICAgaWYg -bGVuKHJhdykgPiA0MDk2OgogICAgICAgICAgICAgICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJkaXNwbGF5IHJlcXVlc3QgdG9vIGxhcmdlIikKICAg -ICAgICAgICAgICAgICAgICByZXN1bHQgPSBzZXJ2aWNlLmhhbmRsZShqc29uLmxvYWRzKHJhdykpCiAgICAgICAgICAgICAgICBleGNlcHQgKFZhbHVlRXJy -b3IsIEtleUVycm9yLCBUeXBlRXJyb3IsIE9TRXJyb3IpOgogICAgICAgICAgICAgICAgICAgIHJlc3VsdCA9IHsiZXJyb3IiOiAiZGlzcGxheV9yZXF1ZXN0 -X2ZhaWxlZCJ9CiAgICAgICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICAgICAgc2VsZi53ZmlsZS53cml0ZShqc29uLmR1bXBzKHJlc3VsdCkuZW5j -b2RlKCkgKyBiIlxuIikKICAgICAgICAgICAgICAgIGV4Y2VwdCBPU0Vycm9yOgogICAgICAgICAgICAgICAgICAgIHBhc3MgICMgVGhlIHBlcnNpc3RlZCBy -ZWNlaXB0IHJlbWFpbnMgcmVwbGF5YWJsZSBhZnRlciBkaXNjb25uZWN0LgoKICAgICAgICBjbGFzcyBTZXJ2ZXIoc29ja2V0c2VydmVyLlRocmVhZGluZ1Vu -aXhTdHJlYW1TZXJ2ZXIpOgogICAgICAgICAgICBkYWVtb25fdGhyZWFkcyA9IFRydWUKCiAgICAgICAgcHJldmlvdXNfdW1hc2sgPSBvcy51bWFzaygwbzA3 -NykKICAgICAgICB0cnk6CiAgICAgICAgICAgIHdpdGggU2VydmVyKHN0cihwYXRoKSwgSGFuZGxlcikgYXMgc2VydmVyOgogICAgICAgICAgICAgICAgb3Mu -dW1hc2socHJldmlvdXNfdW1hc2spCiAgICAgICAgICAgICAgICBzZXJ2ZXIuc2VydmVfZm9yZXZlcigpCiAgICAgICAgZmluYWxseToKICAgICAgICAgICAg -b3MudW1hc2socHJldmlvdXNfdW1hc2spCiAgICAgICAgICAgIHNlcnZpY2Uuc2h1dGRvd24uc2V0KCkKICAgICAgICAgICAgcGF0aC51bmxpbmsobWlzc2lu -Z19vaz1UcnVlKQoKCmRlZiBtYWluKCk6CiAgICBwYXJzZXIgPSBhcmdwYXJzZS5Bcmd1bWVudFBhcnNlcihkZXNjcmlwdGlvbj1fX2RvY19fKQogICAgcGFy -c2VyLmFkZF9hcmd1bWVudCgiYWN0aW9uIiwgY2hvaWNlcz1bImxpc3QiLCAiY3JlYXRlIiwgInNlcnZlIl0pCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50KCIt -LXJlcXVlc3QtaWQiKQogICAgYXJncyA9IHBhcnNlci5wYXJzZV9hcmdzKCkKICAgIHJ1bnRpbWUgPSBvcy5lbnZpcm9uLmdldCgiQ01VWF9ERVNLVE9QX1JV -TlRJTUVfRElSIiwgIi9ydW4vY211eC1kZXNrdG9wIikKICAgIGlmIGFyZ3MuYWN0aW9uID09ICJzZXJ2ZSI6CiAgICAgICAgc2VydmUocnVudGltZSkKICAg -ICAgICByZXR1cm4gMAogICAgaWYgYXJncy5hY3Rpb24gPT0gImNyZWF0ZSIgYW5kIG5vdCBhcmdzLnJlcXVlc3RfaWQ6CiAgICAgICAgcGFyc2VyLmVycm9y -KCJjcmVhdGUgcmVxdWlyZXMgLS1yZXF1ZXN0LWlkIChyZXRhaW4gaXQgd2hlbiByZXRyeWluZykiKQogICAgdHJ5OgogICAgICAgIHdpdGggc29ja2V0LnNv -Y2tldChzb2NrZXQuQUZfVU5JWCwgc29ja2V0LlNPQ0tfU1RSRUFNKSBhcyBjbGllbnQ6CiAgICAgICAgICAgIGNsaWVudC5zZXR0aW1lb3V0KDYwKQogICAg -ICAgICAgICBjbGllbnQuY29ubmVjdChzdHIoUGF0aChydW50aW1lKSAvICJkaXNwbGF5LWNvbnRyb2wuc29jayIpKQogICAgICAgICAgICBjbGllbnQuc2Vu -ZGFsbChqc29uLmR1bXBzKHsiYWN0aW9uIjogYXJncy5hY3Rpb24sICJyZXF1ZXN0IjogYXJncy5yZXF1ZXN0X2lkfSkuZW5jb2RlKCkgKyBiIlxuIikKICAg -ICAgICAgICAgd2l0aCBjbGllbnQubWFrZWZpbGUoInJiIikgYXMgc3RyZWFtOgogICAgICAgICAgICAgICAgcmVzdWx0ID0ganNvbi5sb2FkcyhzdHJlYW0u -cmVhZGxpbmUoNjU1MzYpKQogICAgZXhjZXB0IChPU0Vycm9yLCBWYWx1ZUVycm9yKToKICAgICAgICByZXN1bHQgPSB7ImVycm9yIjogImRpc3BsYXlfcnVu -dGltZV91bmF2YWlsYWJsZSJ9CiAgICBwcmludChqc29uLmR1bXBzKHJlc3VsdCkpCiAgICByZXR1cm4gMSBpZiAiZXJyb3IiIGluIHJlc3VsdCBlbHNlIDAK -CgppZiBfX25hbWVfXyA9PSAiX19tYWluX18iOgogICAgcmFpc2UgU3lzdGVtRXhpdChtYWluKCkpCg== +ICAgc2VsZi5qb2JzID0ge30KICAgICAgICBzZWxmLnN0YXRlcyA9IHt9CiAgICAgICAgc2VsZi5wcm9jZXNzZXMgPSB7fQogICAgICAgIHNlbGYubmFtZWRf +cHJvY2Vzc2VzID0ge30KICAgICAgICBzZWxmLndlYnNvY2tpZnlfcHJvY2Vzc2VzID0ge30KICAgICAgICBzZWxmLnNodXRkb3duID0gdGhyZWFkaW5nLkV2 +ZW50KCkKCiAgICBkZWYgc3RhcnQoc2VsZiwgbnVtYmVyKToKICAgICAgICB3aXRoIHNlbGYubG9jazoKICAgICAgICAgICAgaWYgbnVtYmVyIGluIHNlbGYu +am9iczoKICAgICAgICAgICAgICAgIHJldHVybiBzZWxmLmpvYnNbbnVtYmVyXQogICAgICAgICAgICBmaW5pc2hlZCA9IHRocmVhZGluZy5FdmVudCgpCiAg +ICAgICAgICAgIHNlbGYuam9ic1tudW1iZXJdID0gZmluaXNoZWQKICAgICAgICAgICAgc2VsZi5zdGF0ZXNbbnVtYmVyXSA9ICJsYXVuY2hpbmciCiAgICAg +ICAgICAgIHRocmVhZGluZy5UaHJlYWQodGFyZ2V0PXNlbGYuc3VwZXJ2aXNlLCBhcmdzPShudW1iZXIsIGZpbmlzaGVkKSwgZGFlbW9uPVRydWUpLnN0YXJ0 +KCkKICAgICAgICAgICAgcmV0dXJuIGZpbmlzaGVkCgogICAgZGVmIHRlcm1pbmF0ZShzZWxmLCBudW1iZXIpOgogICAgICAgIGZvciBwcm9jZXNzIGluIHNl +bGYucHJvY2Vzc2VzLnBvcChudW1iZXIsIFtdKToKICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgcHJvY2Vzcy50ZXJtaW5hdGUoKQogICAgICAg +ICAgICBleGNlcHQgT1NFcnJvcjoKICAgICAgICAgICAgICAgIHBhc3MKICAgICAgICBzZWxmLm5hbWVkX3Byb2Nlc3Nlcy5wb3AobnVtYmVyLCBOb25lKQog +ICAgICAgIHNlbGYudGVybWluYXRlX3dlYnNvY2tpZnkobnVtYmVyKQoKICAgIGRlZiB0ZXJtaW5hdGVfd2Vic29ja2lmeShzZWxmLCBudW1iZXIpOgogICAg +ICAgIHByb2Nlc3MgPSBzZWxmLndlYnNvY2tpZnlfcHJvY2Vzc2VzLnBvcChudW1iZXIsIE5vbmUpCiAgICAgICAgaWYgcHJvY2VzcyBpcyBOb25lOgogICAg +ICAgICAgICByZXR1cm4KICAgICAgICB0cnk6CiAgICAgICAgICAgIHByb2Nlc3MudGVybWluYXRlKCkKICAgICAgICBleGNlcHQgT1NFcnJvcjoKICAgICAg +ICAgICAgcGFzcwoKICAgIEBzdGF0aWNtZXRob2QKICAgIGRlZiBzY29wZWRfY29tcG9uZW50X3BhdGgoZXhlY3V0YWJsZSwgcGF0aCwgbnVtYmVyLCBydW50 +aW1lKToKICAgICAgICAiIiJHaXZlIHBlci1kaXNwbGF5IGRlc2t0b3AgY2xpZW50cyBuYW1lcyB0aGUgOjEgc3VwZXJ2aXNvciBjYW5ub3QgbWF0Y2guIiIi +CiAgICAgICAgc291cmNlID0gUGF0aChwYXRoKQogICAgICAgIGlmIG5vdCBzb3VyY2UuaXNfZmlsZSgpOgogICAgICAgICAgICByZXR1cm4gcGF0aAogICAg +ICAgIGRpcmVjdG9yeSA9IFBhdGgocnVudGltZSkgLyAiYmluIgogICAgICAgIGRpcmVjdG9yeS5ta2Rpcihtb2RlPTBvNzAwLCBwYXJlbnRzPVRydWUsIGV4 +aXN0X29rPVRydWUpCiAgICAgICAgdGFyZ2V0ID0gZGlyZWN0b3J5IC8gZiJjbXV4LWRpc3BsYXkte251bWJlcn0te3NvdXJjZS5uYW1lfSIKICAgICAgICBp +ZiBub3QgdGFyZ2V0LmV4aXN0cygpIG9yIHRhcmdldC5zdGF0KCkuc3RfbXRpbWVfbnMgPCBzb3VyY2Uuc3RhdCgpLnN0X210aW1lX25zOgogICAgICAgICAg +ICBzaHV0aWwuY29weTIoc291cmNlLCB0YXJnZXQpCiAgICAgICAgICAgIHRhcmdldC5jaG1vZCgwbzcwMCkKICAgICAgICByZXR1cm4gc3RyKHRhcmdldCkK +CiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgd2FpdF9mb3JfcG9ydChwb3J0LCB0aW1lb3V0PTIwKToKICAgICAgICBkZWFkbGluZSA9IHRpbWUubW9ub3Rv +bmljKCkgKyB0aW1lb3V0CiAgICAgICAgd2hpbGUgdGltZS5tb25vdG9uaWMoKSA8IGRlYWRsaW5lOgogICAgICAgICAgICBpZiBsaXN0ZW5pbmcocG9ydCk6 +CiAgICAgICAgICAgICAgICByZXR1cm4gVHJ1ZQogICAgICAgICAgICB0aW1lLnNsZWVwKDAuMSkKICAgICAgICByZXR1cm4gRmFsc2UKCiAgICBAc3RhdGlj +bWV0aG9kCiAgICBkZWYgcHJvY2Vzc19hbGl2ZShwcm9jZXNzKToKICAgICAgICBwb2xsID0gZ2V0YXR0cihwcm9jZXNzLCAicG9sbCIsIE5vbmUpCiAgICAg +ICAgcmV0dXJuIHBvbGwgaXMgTm9uZSBvciBwb2xsKCkgaXMgTm9uZQoKICAgIGRlZiBjb21wb25lbnRzX2FsaXZlKHNlbGYsIG51bWJlcik6CiAgICAgICAg +bmFtZWQgPSBzZWxmLm5hbWVkX3Byb2Nlc3Nlcy5nZXQobnVtYmVyKQogICAgICAgIGlmIG5hbWVkIGlzIG5vdCBOb25lOgogICAgICAgICAgICByZXR1cm4g +Ym9vbChuYW1lZCkgYW5kIGFsbChzZWxmLnByb2Nlc3NfYWxpdmUocHJvY2VzcykgZm9yIHByb2Nlc3MgaW4gbmFtZWQudmFsdWVzKCkpCiAgICAgICAgcHJv +Y2Vzc2VzID0gc2VsZi5wcm9jZXNzZXMuZ2V0KG51bWJlciwgW10pCiAgICAgICAgcmV0dXJuIGJvb2wocHJvY2Vzc2VzKSBhbmQgYWxsKHNlbGYucHJvY2Vz +c19hbGl2ZShwcm9jZXNzKSBmb3IgcHJvY2VzcyBpbiBwcm9jZXNzZXMpCgogICAgZGVmIHNlcnZpY2VfcmVhZHkoc2VsZiwgbnVtYmVyKToKICAgICAgICBp +ZiBub3QgcmVhZHkobnVtYmVyKSBvciBub3Qgc2VsZi5jb21wb25lbnRzX2FsaXZlKG51bWJlcik6CiAgICAgICAgICAgIHJldHVybiBGYWxzZQogICAgICAg +IHdlYnNvY2tpZnkgPSBzZWxmLndlYnNvY2tpZnlfcHJvY2Vzc2VzLmdldChudW1iZXIpCiAgICAgICAgcmV0dXJuIHdlYnNvY2tpZnkgaXMgTm9uZSBvciBz +ZWxmLnByb2Nlc3NfYWxpdmUod2Vic29ja2lmeSkKCiAgICBkZWYgdHJhY2tfcHJvY2VzcyhzZWxmLCBudW1iZXIsIG5hbWUsIHByb2Nlc3MpOgogICAgICAg +IHNlbGYucHJvY2Vzc2VzLnNldGRlZmF1bHQobnVtYmVyLCBbXSkuYXBwZW5kKHByb2Nlc3MpCiAgICAgICAgc2VsZi5uYW1lZF9wcm9jZXNzZXMuc2V0ZGVm +YXVsdChudW1iZXIsIHt9KVtuYW1lXSA9IHByb2Nlc3MKCiAgICBkZWYgc3RhcnRfZGJ1cyhzZWxmLCBudW1iZXIsIGVudmlyb25tZW50KToKICAgICAgICBi +dXMgPSBzdWJwcm9jZXNzLnJ1bihbImRidXMtbGF1bmNoIiwgIi0tc2gtc3ludGF4Il0sIGVudj1lbnZpcm9ubWVudCwKICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICBzdGRpbj1zdWJwcm9jZXNzLkRFVk5VTEwsIHN0ZG91dD1zdWJwcm9jZXNzLlBJUEUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3Rk +ZXJyPXN1YnByb2Nlc3MuREVWTlVMTCwgdGV4dD1UcnVlLCB0aW1lb3V0PTEwLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNoZWNrPVRydWUpLnN0 +ZG91dAogICAgICAgIGZvciBsaW5lIGluIGJ1cy5zcGxpdGxpbmVzKCk6CiAgICAgICAgICAgIGlmICI9IiBub3QgaW4gbGluZToKICAgICAgICAgICAgICAg +IGNvbnRpbnVlCiAgICAgICAgICAgIGtleSwgdmFsdWUgPSBsaW5lLnNwbGl0KCI9IiwgMSkKICAgICAgICAgICAgZW52aXJvbm1lbnRba2V5XSA9IHZhbHVl +LnN0cmlwKCkucnN0cmlwKCI7Iikuc3RyaXAoKS5zdHJpcCgiJyIpCiAgICAgICAgdHJ5OgogICAgICAgICAgICBkYnVzX3BpZCA9IGludChlbnZpcm9ubWVu +dC5nZXQoIkRCVVNfU0VTU0lPTl9CVVNfUElEIiwgIjAiKSkKICAgICAgICBleGNlcHQgVmFsdWVFcnJvcjoKICAgICAgICAgICAgZGJ1c19waWQgPSAwCiAg +ICAgICAgaWYgZGJ1c19waWQgPiAxOgogICAgICAgICAgICBzZWxmLnRyYWNrX3Byb2Nlc3MobnVtYmVyLCAiZGJ1cyIsIE93bmVkUHJvY2VzcyhkYnVzX3Bp +ZCkpCgogICAgZGVmIHN0YXJ0X3Nlc3Npb25fY29tcG9uZW50cyhzZWxmLCBudW1iZXIsIGVudmlyb25tZW50LCBydW50aW1lKToKICAgICAgICBmb3IgZXhl +Y3V0YWJsZSwgYXJncyBpbiBbCiAgICAgICAgICAgICgib3BlbmJveCIsIFtdKSwgKCJ0aW50MiIsIFsiLWMiLCAiL2V0Yy9jbXV4L3RpbnQycmMiXSksCiAg +ICAgICAgICAgICgidm5jY29uZmlnIiwgWyItbm93aW4iXSksCiAgICAgICAgXToKICAgICAgICAgICAgZXhpc3RpbmcgPSBzZWxmLm5hbWVkX3Byb2Nlc3Nl +cy5nZXQobnVtYmVyLCB7fSkuZ2V0KGV4ZWN1dGFibGUpCiAgICAgICAgICAgIGlmIGV4aXN0aW5nIGlzIG5vdCBOb25lIGFuZCBzZWxmLnByb2Nlc3NfYWxp +dmUoZXhpc3RpbmcpOgogICAgICAgICAgICAgICAgY29udGludWUKICAgICAgICAgICAgcGF0aCA9IGV4ZWN1dGFibGUgaWYgIi8iIGluIGV4ZWN1dGFibGUg +ZWxzZSBzaHV0aWwud2hpY2goZXhlY3V0YWJsZSkKICAgICAgICAgICAgaWYgbm90IHBhdGg6CiAgICAgICAgICAgICAgICBpZiBleGVjdXRhYmxlID09ICJ2 +bmNjb25maWciOgogICAgICAgICAgICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgICAgICAgICByYWlzZSBSdW50aW1lRXJyb3IoZiJtaXNzaW5nIGRpc3Bs +YXkgY29tcG9uZW50OiB7ZXhlY3V0YWJsZX0iKQogICAgICAgICAgICBwYXRoID0gc2VsZi5zY29wZWRfY29tcG9uZW50X3BhdGgoZXhlY3V0YWJsZSwgcGF0 +aCwgbnVtYmVyLCBydW50aW1lKQogICAgICAgICAgICBwcm9jZXNzID0gc3VicHJvY2Vzcy5Qb3BlbihbcGF0aF0gKyBhcmdzLCBlbnY9ZW52aXJvbm1lbnQs +CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0ZGluPXN1YnByb2Nlc3MuREVWTlVMTCwKICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgc3Rkb3V0PXN1YnByb2Nlc3MuREVWTlVMTCwgc3RkZXJyPXN1YnByb2Nlc3MuREVWTlVMTCkKICAgICAgICAgICAgc2VsZi50cmFj +a19wcm9jZXNzKG51bWJlciwgZXhlY3V0YWJsZSwgcHJvY2VzcykKCiAgICBkZWYgc3RhcnRfd2Vic29ja2lmeShzZWxmLCBudW1iZXIsIGVudmlyb25tZW50 +LCBleGVjdXRhYmxlKToKICAgICAgICBwYXRoID0gZXhlY3V0YWJsZSBpZiAiLyIgaW4gZXhlY3V0YWJsZSBlbHNlIHNodXRpbC53aGljaChleGVjdXRhYmxl +KQogICAgICAgIGlmIG5vdCBwYXRoOgogICAgICAgICAgICByYWlzZSBOb1ZOQ1VuYXZhaWxhYmxlKCJtaXNzaW5nIGRpc3BsYXkgY29tcG9uZW50OiB3ZWJz +b2NraWZ5IikKICAgICAgICBzZWxmLnRlcm1pbmF0ZV93ZWJzb2NraWZ5KG51bWJlcikKICAgICAgICByZmJfcG9ydCwgbm92bmNfcG9ydCA9IDU5MDAgKyBu +dW1iZXIsIDY5MDAgKyBudW1iZXIKICAgICAgICBwcm9jZXNzID0gc3VicHJvY2Vzcy5Qb3BlbigKICAgICAgICAgICAgW3BhdGgsICItLXdlYiIsICIvdXNy +L3NoYXJlL25vdm5jIiwgIi0taGVhcnRiZWF0IiwgIjMwIiwKICAgICAgICAgICAgIGYiMTI3LjAuMC4xOntub3ZuY19wb3J0fSIsIGYiMTI3LjAuMC4xOnty +ZmJfcG9ydH0iXSwKICAgICAgICAgICAgZW52PWVudmlyb25tZW50LCBzdGRpbj1zdWJwcm9jZXNzLkRFVk5VTEwsCiAgICAgICAgICAgIHN0ZG91dD1zdWJw +cm9jZXNzLkRFVk5VTEwsIHN0ZGVycj1zdWJwcm9jZXNzLkRFVk5VTEwpCiAgICAgICAgc2VsZi53ZWJzb2NraWZ5X3Byb2Nlc3Nlc1tudW1iZXJdID0gcHJv +Y2VzcwogICAgICAgIGlmIG5vdCBzZWxmLndhaXRfZm9yX3BvcnQobm92bmNfcG9ydCwgdGltZW91dD0xMCk6CiAgICAgICAgICAgIHNlbGYudGVybWluYXRl +X3dlYnNvY2tpZnkobnVtYmVyKQogICAgICAgICAgICByYWlzZSBOb1ZOQ1VuYXZhaWxhYmxlKCJub1ZOQyBkaWQgbm90IGJlY29tZSByZWFkeSIpCgogICAg +ZGVmIHN0YXJ0X2NvbXBvbmVudHMoc2VsZiwgbnVtYmVyLCBlbnZpcm9ubWVudCwgcnVudGltZSk6CiAgICAgICAgaWYgc2VsZi5zZXJ2aWNlX3JlYWR5KG51 +bWJlcik6CiAgICAgICAgICAgIHJldHVybgogICAgICAgIHJmYl9pc19yZWFkeSA9IHJmYl9yZWFkeShudW1iZXIpCiAgICAgICAgdm5jID0gc2h1dGlsLndo +aWNoKCJYdm5jIikgb3Igc2h1dGlsLndoaWNoKCJYdGlnZXJ2bmMiKQogICAgICAgIHdlYnNvY2tpZnkgPSBzaHV0aWwud2hpY2goIndlYnNvY2tpZnkiKQog +ICAgICAgIGlmIG5vdCB3ZWJzb2NraWZ5OgogICAgICAgICAgICByYWlzZSBOb1ZOQ1VuYXZhaWxhYmxlKCJkaXNwbGF5IHJ1bnRpbWUgdW5hdmFpbGFibGUi +KQogICAgICAgIGlmIG5vdCByZmJfaXNfcmVhZHkgYW5kIG5vdCB2bmM6CiAgICAgICAgICAgIHJhaXNlIFJ1bnRpbWVFcnJvcigiZGlzcGxheSBydW50aW1l +IHVuYXZhaWxhYmxlIikKICAgICAgICBpZiBub3QgcmZiX2lzX3JlYWR5OgogICAgICAgICAgICBzZWxmLnRlcm1pbmF0ZShudW1iZXIpCiAgICAgICAgICAg +IHJmYl9wb3J0ID0gNTkwMCArIG51bWJlcgogICAgICAgICAgICBzZWxmLnByb2Nlc3Nlc1tudW1iZXJdID0gW10KICAgICAgICAgICAgc2VsZi5uYW1lZF9w +cm9jZXNzZXNbbnVtYmVyXSA9IHt9CiAgICAgICAgICAgIHNlbGYudHJhY2tfcHJvY2VzcyhudW1iZXIsICJ4dm5jIiwgc3VicHJvY2Vzcy5Qb3BlbigKICAg +ICAgICAgICAgICAgIFt2bmMsIGYiOntudW1iZXJ9IiwgIi1nZW9tZXRyeSIsIG9zLmVudmlyb24uZ2V0KCJDTVVYX1ZOQ19HRU9NRVRSWSIsICIxNDQweDkw +MCIpLAogICAgICAgICAgICAgICAgICItZGVwdGgiLCAiMjQiLCAiLXJmYnBvcnQiLCBzdHIocmZiX3BvcnQpLCAiLWxvY2FsaG9zdCIsCiAgICAgICAgICAg +ICAgICAgIi1TZWN1cml0eVR5cGVzIiwgIk5vbmUiLCAiLUFsd2F5c1NoYXJlZCJdLAogICAgICAgICAgICAgICAgZW52PWVudmlyb25tZW50LCBzdGRpbj1z +dWJwcm9jZXNzLkRFVk5VTEwsCiAgICAgICAgICAgICAgICBzdGRvdXQ9c3VicHJvY2Vzcy5ERVZOVUxMLCBzdGRlcnI9c3VicHJvY2Vzcy5ERVZOVUxMKSkK +ICAgICAgICAgICAgaWYgbm90IHNlbGYud2FpdF9mb3JfcG9ydChyZmJfcG9ydCk6CiAgICAgICAgICAgICAgICByYWlzZSBSdW50aW1lRXJyb3IoIlh2bmMg +ZGlkIG5vdCBiZWNvbWUgcmVhZHkiKQogICAgICAgICAgICBzZWxmLnN0YXJ0X2RidXMobnVtYmVyLCBlbnZpcm9ubWVudCkKICAgICAgICAgICAgc2VsZi5z +dGFydF9zZXNzaW9uX2NvbXBvbmVudHMobnVtYmVyLCBlbnZpcm9ubWVudCwgcnVudGltZSkKICAgICAgICBlbGlmIG51bWJlciBpbiBzZWxmLm5hbWVkX3By +b2Nlc3NlczoKICAgICAgICAgICAgbmFtZWQgPSBzZWxmLm5hbWVkX3Byb2Nlc3Nlc1tudW1iZXJdCiAgICAgICAgICAgIGRidXMgPSBuYW1lZC5nZXQoImRi +dXMiKQogICAgICAgICAgICBpZiBkYnVzIGlzIE5vbmUgb3Igbm90IHNlbGYucHJvY2Vzc19hbGl2ZShkYnVzKToKICAgICAgICAgICAgICAgIGZvciBuYW1l +IGluICgib3BlbmJveCIsICJ0aW50MiIsICJ2bmNjb25maWciKToKICAgICAgICAgICAgICAgICAgICBwcm9jZXNzID0gbmFtZWQucG9wKG5hbWUsIE5vbmUp +CiAgICAgICAgICAgICAgICAgICAgaWYgcHJvY2VzcyBpcyBub3QgTm9uZToKICAgICAgICAgICAgICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAg +ICAgICAgICAgICAgcHJvY2Vzcy50ZXJtaW5hdGUoKQogICAgICAgICAgICAgICAgICAgICAgICBleGNlcHQgT1NFcnJvcjoKICAgICAgICAgICAgICAgICAg +ICAgICAgICAgIHBhc3MKICAgICAgICAgICAgICAgIHNlbGYuc3RhcnRfZGJ1cyhudW1iZXIsIGVudmlyb25tZW50KQogICAgICAgICAgICBzZWxmLnN0YXJ0 +X3Nlc3Npb25fY29tcG9uZW50cyhudW1iZXIsIGVudmlyb25tZW50LCBydW50aW1lKQogICAgICAgIHdlYnNvY2tpZnlfcHJvY2VzcyA9IHNlbGYud2Vic29j +a2lmeV9wcm9jZXNzZXMuZ2V0KG51bWJlcikKICAgICAgICBpZiAod2Vic29ja2lmeV9wcm9jZXNzIGlzIE5vbmUgb3Igbm90IHNlbGYucHJvY2Vzc19hbGl2 +ZSh3ZWJzb2NraWZ5X3Byb2Nlc3MpCiAgICAgICAgICAgICAgICBvciBub3Qgbm92bmNfcmVhZHkobnVtYmVyKSk6CiAgICAgICAgICAgIHNlbGYuc3RhcnRf +d2Vic29ja2lmeShudW1iZXIsIGVudmlyb25tZW50LCB3ZWJzb2NraWZ5KQogICAgICAgIChydW50aW1lIC8gImVudiIpLndyaXRlX3RleHQoZiJleHBvcnQg +RElTUExBWT06e251bWJlcn1cbiIpCgogICAgZGVmIHN1cGVydmlzZShzZWxmLCBudW1iZXIsIGZpbmlzaGVkKToKICAgICAgICBydW50aW1lID0gc2VsZi5y +dW50aW1lIC8gc3RyKG51bWJlcikKICAgICAgICBydW50aW1lLm1rZGlyKG1vZGU9MG83MDAsIHBhcmVudHM9VHJ1ZSwgZXhpc3Rfb2s9VHJ1ZSkKICAgICAg +ICBlbnZpcm9ubWVudCA9IGRpY3Qob3MuZW52aXJvbiwgRElTUExBWT1mIjp7bnVtYmVyfSIsIENNVVhfREVTS1RPUF9SVU5USU1FX0RJUj1zdHIocnVudGlt +ZSkpCiAgICAgICAgZW52aXJvbm1lbnRbIkNIUk9NRV9DT05GSUdfSE9NRSJdID0gc3RyKHJ1bnRpbWUgLyAiY2hyb21lLWNvbmZpZyIpCiAgICAgICAgZW52 +aXJvbm1lbnRbIlhER19DT05GSUdfSE9NRSJdID0gc3RyKHJ1bnRpbWUgLyAieGRnLWNvbmZpZyIpCiAgICAgICAgZm9yIGtleSBpbiAoIk5PVElGWV9TT0NL +RVQiLCAiREJVU19TRVNTSU9OX0JVU19BRERSRVNTIiwgIkRCVVNfU0VTU0lPTl9CVVNfUElEIiwKICAgICAgICAgICAgICAgICAgICAiQVRfU1BJX0JVU19B +RERSRVNTIiwgIkFUX1NQSV9CVVMiKToKICAgICAgICAgICAgZW52aXJvbm1lbnQucG9wKGtleSwgTm9uZSkKICAgICAgICB3aGlsZSBub3Qgc2VsZi5zaHV0 +ZG93bi5pc19zZXQoKToKICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgIyBUaGUgc3VwZXJ2aXNvciBpcyBhIGhlYWx0aCBjaGVjaywgbm90IGEg +dGltZXItZHJpdmVuIHJlc3RhcnQuCiAgICAgICAgICAgICAgICAjIFByZXNlcnZlIGFuIGFjdGl2ZSBSRkIvbm9WTkMgc2Vzc2lvbiB1bnRpbCBpdHMgb3du +ZWQgZW5kcG9pbnRzIGZhaWwuCiAgICAgICAgICAgICAgICBpZiBub3Qgc2VsZi5zZXJ2aWNlX3JlYWR5KG51bWJlcik6CiAgICAgICAgICAgICAgICAgICAg +c2VsZi5zdGFydF9jb21wb25lbnRzKG51bWJlciwgZW52aXJvbm1lbnQsIHJ1bnRpbWUpCiAgICAgICAgICAgICAgICBzdGF0ZSA9ICJydW5uaW5nIiBpZiBz +ZWxmLnNlcnZpY2VfcmVhZHkobnVtYmVyKSBlbHNlICJ1bmF2YWlsYWJsZSIKICAgICAgICAgICAgZXhjZXB0IE5vVk5DVW5hdmFpbGFibGU6CiAgICAgICAg +ICAgICAgICBzZWxmLnRlcm1pbmF0ZV93ZWJzb2NraWZ5KG51bWJlcikKICAgICAgICAgICAgICAgIHN0YXRlID0gInVuYXZhaWxhYmxlIgogICAgICAgICAg +ICBleGNlcHQgKE9TRXJyb3IsIFJ1bnRpbWVFcnJvciwgc3VicHJvY2Vzcy5TdWJwcm9jZXNzRXJyb3IpOgogICAgICAgICAgICAgICAgc2VsZi50ZXJtaW5h +dGUobnVtYmVyKQogICAgICAgICAgICAgICAgc3RhdGUgPSAidW5hdmFpbGFibGUiCiAgICAgICAgICAgIHdpdGggc2VsZi5sb2NrOgogICAgICAgICAgICAg +ICAgc2VsZi5zdGF0ZXNbbnVtYmVyXSA9IHN0YXRlCiAgICAgICAgICAgIGZpbmlzaGVkLnNldCgpCiAgICAgICAgICAgIHNlbGYuc2h1dGRvd24ud2FpdCgz +MCkKICAgICAgICBzZWxmLnRlcm1pbmF0ZShudW1iZXIpCgogICAgZGVmIHNuYXBzaG90KHNlbGYpOgogICAgICAgIHdpdGggc2VsZi5sb2NrOgogICAgICAg +ICAgICByZXR1cm4geyJ2ZXJzaW9uIjogMSwgImNhbkNyZWF0ZSI6IGxlbihzZWxmLmpvYnMpIDwgTUFYX0RJU1BMQVlTIC0gMSwKICAgICAgICAgICAgICAg +ICAgICAiZGlzcGxheXMiOiBbZGVzY3JpcHRvcigxLCAicnVubmluZyIgaWYgcmVhZHkoMSkgZWxzZSAidW5hdmFpbGFibGUiKV0KICAgICAgICAgICAgICAg +ICAgICArIFtkZXNjcmlwdG9yKG4sIHNlbGYuc3RhdGVzLmdldChuLCAibGF1bmNoaW5nIikpIGZvciBuIGluIHNlbGYuY2F0YWxvZy5udW1iZXJzKCldfQoK +ICAgIGRlZiBoYW5kbGUoc2VsZiwgcmVxdWVzdCk6CiAgICAgICAgaWYgcmVxdWVzdC5nZXQoImFjdGlvbiIpID09ICJsaXN0IjoKICAgICAgICAgICAgcmV0 +dXJuIHNlbGYuc25hcHNob3QoKQogICAgICAgIGlmIHJlcXVlc3QuZ2V0KCJhY3Rpb24iKSAhPSAiY3JlYXRlIjoKICAgICAgICAgICAgcmFpc2UgVmFsdWVF +cnJvcigidW5zdXBwb3J0ZWQgZGlzcGxheSBhY3Rpb24iKQogICAgICAgIG51bWJlciA9IHNlbGYuY2F0YWxvZy5hbGxvY2F0ZShyZXF1ZXN0LmdldCgicmVx +dWVzdCIpKQogICAgICAgIHNlbGYuc3RhcnQobnVtYmVyKS53YWl0KDU1KQogICAgICAgIHJlc3VsdCA9IHNlbGYuc25hcHNob3QoKQogICAgICAgIHJlc3Vs +dFsiY3JlYXRlZCJdID0gZiJkaXNwbGF5OntudW1iZXJ9IgogICAgICAgIGlmIHNlbGYuc3RhdGVzLmdldChudW1iZXIpICE9ICJydW5uaW5nIjoKICAgICAg +ICAgICAgcmVzdWx0WyJlcnJvciJdID0gImRpc3BsYXlfc3RhcnRfZmFpbGVkIgogICAgICAgIHJldHVybiByZXN1bHQKCgpkZWYgc2VydmUocnVudGltZSk6 +CiAgICBydW50aW1lID0gUGF0aChydW50aW1lKQogICAgcnVudGltZS5ta2Rpcihtb2RlPTBvNzAwLCBwYXJlbnRzPVRydWUsIGV4aXN0X29rPVRydWUpCiAg +ICB3aXRoIChydW50aW1lIC8gImRpc3BsYXktY29udHJvbC5sb2NrIikub3BlbigidyIpIGFzIGxvY2s6CiAgICAgICAgdHJ5OgogICAgICAgICAgICBmY250 +bC5mbG9jayhsb2NrLCBmY250bC5MT0NLX0VYIHwgZmNudGwuTE9DS19OQikKICAgICAgICBleGNlcHQgQmxvY2tpbmdJT0Vycm9yOgogICAgICAgICAgICBy +ZXR1cm4KICAgICAgICBpZiBub3QgYWxsKHNodXRpbC53aGljaChuYW1lKSBmb3IgbmFtZSBpbiAoIndlYnNvY2tpZnkiLCAib3BlbmJveCIsICJkYnVzLWxh +dW5jaCIpKToKICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJvcigiZGlzcGxheSBydW50aW1lIHVuYXZhaWxhYmxlIikKICAgICAgICBpZiBub3QgKHNodXRp +bC53aGljaCgiWHZuYyIpIG9yIHNodXRpbC53aGljaCgiWHRpZ2Vydm5jIikpOgogICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJkaXNwbGF5IHJ1bnRp +bWUgdW5hdmFpbGFibGUiKQogICAgICAgIHNlcnZpY2UgPSBEaXNwbGF5U2VydmljZShEaXNwbGF5Q2F0YWxvZyhQYXRoLmhvbWUoKSAvICIuY211eCIgLyAi +ZGlzcGxheXMiKSwgcnVudGltZSAvICJkaXNwbGF5cyIpCiAgICAgICAgZm9yIG51bWJlciBpbiBzZXJ2aWNlLmNhdGFsb2cubnVtYmVycygpOgogICAgICAg +ICAgICBzZXJ2aWNlLnN0YXJ0KG51bWJlcikKICAgICAgICBwYXRoID0gcnVudGltZSAvICJkaXNwbGF5LWNvbnRyb2wuc29jayIKICAgICAgICBpZiBwYXRo +LmV4aXN0cygpOgogICAgICAgICAgICBwYXRoLnVubGluaygpCgogICAgICAgIGNsYXNzIEhhbmRsZXIoc29ja2V0c2VydmVyLlN0cmVhbVJlcXVlc3RIYW5k +bGVyKToKICAgICAgICAgICAgZGVmIGhhbmRsZShzZWxmKToKICAgICAgICAgICAgICAgIHNlbGYuY29ubmVjdGlvbi5zZXR0aW1lb3V0KDYwKQogICAgICAg +ICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgIHJhdyA9IHNlbGYucmZpbGUucmVhZGxpbmUoNDA5NykKICAgICAgICAgICAgICAgICAgICBpZiBs +ZW4ocmF3KSA+IDQwOTY6CiAgICAgICAgICAgICAgICAgICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoImRpc3BsYXkgcmVxdWVzdCB0b28gbGFyZ2UiKQogICAg +ICAgICAgICAgICAgICAgIHJlc3VsdCA9IHNlcnZpY2UuaGFuZGxlKGpzb24ubG9hZHMocmF3KSkKICAgICAgICAgICAgICAgIGV4Y2VwdCAoVmFsdWVFcnJv +ciwgS2V5RXJyb3IsIFR5cGVFcnJvciwgT1NFcnJvcik6CiAgICAgICAgICAgICAgICAgICAgcmVzdWx0ID0geyJlcnJvciI6ICJkaXNwbGF5X3JlcXVlc3Rf +ZmFpbGVkIn0KICAgICAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgICAgICBzZWxmLndmaWxlLndyaXRlKGpzb24uZHVtcHMocmVzdWx0KS5lbmNv +ZGUoKSArIGIiXG4iKQogICAgICAgICAgICAgICAgZXhjZXB0IE9TRXJyb3I6CiAgICAgICAgICAgICAgICAgICAgcGFzcyAgIyBUaGUgcGVyc2lzdGVkIHJl +Y2VpcHQgcmVtYWlucyByZXBsYXlhYmxlIGFmdGVyIGRpc2Nvbm5lY3QuCgogICAgICAgIGNsYXNzIFNlcnZlcihzb2NrZXRzZXJ2ZXIuVGhyZWFkaW5nVW5p +eFN0cmVhbVNlcnZlcik6CiAgICAgICAgICAgIGRhZW1vbl90aHJlYWRzID0gVHJ1ZQoKICAgICAgICBwcmV2aW91c191bWFzayA9IG9zLnVtYXNrKDBvMDc3 +KQogICAgICAgIHRyeToKICAgICAgICAgICAgd2l0aCBTZXJ2ZXIoc3RyKHBhdGgpLCBIYW5kbGVyKSBhcyBzZXJ2ZXI6CiAgICAgICAgICAgICAgICBvcy51 +bWFzayhwcmV2aW91c191bWFzaykKICAgICAgICAgICAgICAgIHNlcnZlci5zZXJ2ZV9mb3JldmVyKCkKICAgICAgICBmaW5hbGx5OgogICAgICAgICAgICBv +cy51bWFzayhwcmV2aW91c191bWFzaykKICAgICAgICAgICAgc2VydmljZS5zaHV0ZG93bi5zZXQoKQogICAgICAgICAgICBwYXRoLnVubGluayhtaXNzaW5n +X29rPVRydWUpCgoKZGVmIG1haW4oKToKICAgIHBhcnNlciA9IGFyZ3BhcnNlLkFyZ3VtZW50UGFyc2VyKGRlc2NyaXB0aW9uPV9fZG9jX18pCiAgICBwYXJz +ZXIuYWRkX2FyZ3VtZW50KCJhY3Rpb24iLCBjaG9pY2VzPVsibGlzdCIsICJjcmVhdGUiLCAic2VydmUiXSkKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoIi0t +cmVxdWVzdC1pZCIpCiAgICBhcmdzID0gcGFyc2VyLnBhcnNlX2FyZ3MoKQogICAgcnVudGltZSA9IG9zLmVudmlyb24uZ2V0KCJDTVVYX0RFU0tUT1BfUlVO +VElNRV9ESVIiLCAiL3J1bi9jbXV4LWRlc2t0b3AiKQogICAgaWYgYXJncy5hY3Rpb24gPT0gInNlcnZlIjoKICAgICAgICBzZXJ2ZShydW50aW1lKQogICAg +ICAgIHJldHVybiAwCiAgICBpZiBhcmdzLmFjdGlvbiA9PSAiY3JlYXRlIiBhbmQgbm90IGFyZ3MucmVxdWVzdF9pZDoKICAgICAgICBwYXJzZXIuZXJyb3Io +ImNyZWF0ZSByZXF1aXJlcyAtLXJlcXVlc3QtaWQgKHJldGFpbiBpdCB3aGVuIHJldHJ5aW5nKSIpCiAgICB0cnk6CiAgICAgICAgd2l0aCBzb2NrZXQuc29j +a2V0KHNvY2tldC5BRl9VTklYLCBzb2NrZXQuU09DS19TVFJFQU0pIGFzIGNsaWVudDoKICAgICAgICAgICAgY2xpZW50LnNldHRpbWVvdXQoNjApCiAgICAg +ICAgICAgIGNsaWVudC5jb25uZWN0KHN0cihQYXRoKHJ1bnRpbWUpIC8gImRpc3BsYXktY29udHJvbC5zb2NrIikpCiAgICAgICAgICAgIGNsaWVudC5zZW5k +YWxsKGpzb24uZHVtcHMoeyJhY3Rpb24iOiBhcmdzLmFjdGlvbiwgInJlcXVlc3QiOiBhcmdzLnJlcXVlc3RfaWR9KS5lbmNvZGUoKSArIGIiXG4iKQogICAg +ICAgICAgICB3aXRoIGNsaWVudC5tYWtlZmlsZSgicmIiKSBhcyBzdHJlYW06CiAgICAgICAgICAgICAgICByZXN1bHQgPSBqc29uLmxvYWRzKHN0cmVhbS5y +ZWFkbGluZSg2NTUzNikpCiAgICBleGNlcHQgKE9TRXJyb3IsIFZhbHVlRXJyb3IpOgogICAgICAgIHJlc3VsdCA9IHsiZXJyb3IiOiAiZGlzcGxheV9ydW50 +aW1lX3VuYXZhaWxhYmxlIn0KICAgIHByaW50KGpzb24uZHVtcHMocmVzdWx0KSkKICAgIHJldHVybiAxIGlmICJlcnJvciIgaW4gcmVzdWx0IGVsc2UgMAoK +CmlmIF9fbmFtZV9fID09ICJfX21haW5fXyI6CiAgICByYWlzZSBTeXN0ZW1FeGl0KG1haW4oKSkK """ private static let path = "$HOME/.cmux/cmux-display" diff --git a/Sources/Cloud/PortForward/CloudBrowserAccessState.swift b/Sources/Cloud/PortForward/CloudBrowserAccessState.swift index ecffd9ca5507..f1554c8d5741 100644 --- a/Sources/Cloud/PortForward/CloudBrowserAccessState.swift +++ b/Sources/Cloud/PortForward/CloudBrowserAccessState.swift @@ -288,7 +288,13 @@ final class CloudBrowserAccessState { func owns(_ url: URL) -> Bool { guard let remoteURL else { return false } if model?.usesBrowserProxy == true { - return ["http", "https"].contains(url.scheme?.lowercased() ?? "") && url.host?.lowercased() == remoteURL.host?.lowercased() + guard ["http", "https"].contains(url.scheme?.lowercased() ?? ""), + url.host?.lowercased() == remoteURL.host?.lowercased() else { return false } + if let resourceID, let expectedPort = Self.displayPort(for: resourceID) { + let requestedPort = url.port ?? (url.scheme?.lowercased() == "https" ? 443 : 80) + return requestedPort == expectedPort + } + return true } return Self.sameService(url, remoteURL) || navigationURL.map { Self.sameService(url, $0) } == true } @@ -323,4 +329,11 @@ final class CloudBrowserAccessState { a.scheme?.lowercased() == b.scheme?.lowercased() && a.host?.lowercased() == b.host?.lowercased() && (a.port ?? (a.scheme == "https" ? 443 : 80)) == (b.port ?? (b.scheme == "https" ? 443 : 80)) } + + private static func displayPort(for resource: SurfaceResourceID) -> Int? { + guard resource.kind == .display, + let number = Int(resource.key.split(separator: ":").last ?? ""), + (1...16).contains(number) else { return nil } + return 6900 + number + } } diff --git a/Sources/DockSplitStore+BrowserActions.swift b/Sources/DockSplitStore+BrowserActions.swift index bbee5241a640..7078734aee9b 100644 --- a/Sources/DockSplitStore+BrowserActions.swift +++ b/Sources/DockSplitStore+BrowserActions.swift @@ -16,11 +16,11 @@ extension DockSplitStore { } let tabs = bonsplitController.tabs(inPane: paneId) let catalog = SurfaceCatalog.shared - let keepsCloudRoute = browser.cloudAccess.retainsCloudResourceForDuplication + let keepsCloudRoute = browser.retainsCloudResourceForDuplication let record = keepsCloudRoute ? catalog.projectionRecord(forPanel: panelId).flatMap { $0.resource.machine.isLocal ? nil : $0 } : nil - let resource = keepsCloudRoute ? (record?.resource ?? browser.cloudAccess.resourceID) : nil + let resource = keepsCloudRoute ? (record?.resource ?? browser.cloudResourceForDuplication) : nil let isCloud = resource?.machine.isLocal == false guard surfaceOwnershipPolicy.rejection(for: machineOwningSurface(panelId)) == nil else { return nil } guard let anchorIndex = tabs.firstIndex(where: { diff --git a/Sources/Panels/BrowserPanel+CloudConnection.swift b/Sources/Panels/BrowserPanel+CloudConnection.swift index f378f5a85dcc..b3d0b00a6036 100644 --- a/Sources/Panels/BrowserPanel+CloudConnection.swift +++ b/Sources/Panels/BrowserPanel+CloudConnection.swift @@ -8,14 +8,16 @@ extension BrowserPanel { /// provenance from this panel. func leaveCloudResourceForLocalNavigation() { pendingCloudRestoreURL = nil - if cloudAccess.retainsCloudResourceForDuplication { + if retainsCloudResourceForDuplication { SurfaceCatalog.shared.endProjections(panelID: id, reason: .replaced) } cloudAccess.leave() } - var cloudResourceForSession: SurfaceResourceID? { - guard cloudAccess.retainsCloudResourceForDuplication else { return nil } + /// The catalog projection remains authoritative while a Cloud pane is an + /// unavailable placeholder and before its provider has configured the + /// browser. Deliberate external navigation removes that projection first. + var cloudResourceForDuplication: SurfaceResourceID? { if let resource = cloudAccess.resourceID, !resource.machine.isLocal { return resource } @@ -23,6 +25,15 @@ extension BrowserPanel { return resource?.machine.isLocal == false ? resource : nil } + var retainsCloudResourceForDuplication: Bool { + cloudAccess.model != nil || cloudResourceForDuplication != nil + } + + var cloudResourceForSession: SurfaceResourceID? { + guard retainsCloudResourceForDuplication else { return nil } + return cloudResourceForDuplication + } + /// Restore by stable resource identity before loading any saved address. /// A stale/unknown provider leaves an owned placeholder, never a local page. func restoreCloudResource(_ resource: SurfaceResourceID, preferredURL: URL? = nil) { diff --git a/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift b/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift index 479f197bbef5..fed9be0a1017 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift @@ -56,7 +56,25 @@ extension CmuxTuiSurfaceProvider { let projectedResource = catalog.projectionRecord(forPanel: browser.id).flatMap { $0.resource.machine.isLocal ? nil : $0.resource } - let explicitResource = resourceID ?? browser.cloudAccess.resourceID ?? projectedResource + let retainedResource = browser.cloudAccess.resourceID ?? projectedResource + let explicitResource: SurfaceResourceID? + if let resourceID { + explicitResource = resourceID + } else if let retainedResource, + Self.port(for: retainedResource, catalog: catalog) == nil + || Self.port(for: retainedResource, catalog: catalog) == requestedPort { + explicitResource = retainedResource + } else { + // A URL that changes the service port must resolve to the requested + // catalog slot instead of carrying the old display identity forward. + explicitResource = nil + } + if let explicitResource, + let expectedPort = Self.port(for: explicitResource, catalog: catalog), + expectedPort != requestedPort { + browser.cloudAccess.showUnavailable(CloudGuestDisplaySnapshot.unavailableMessage) + return false + } if explicitResource == nil, fallbackID.kind == .display, fallbackID.key != SurfaceResourceID.desktopDisplayKey, catalog.resources[fallbackID] == nil { @@ -94,6 +112,19 @@ extension CmuxTuiSurfaceProvider { return true } + private static func port(for resource: SurfaceResourceID, catalog: SurfaceCatalog) -> Int? { + if let port = catalog.resources[resource]?.port { return port } + if resource.kind == .display, + let number = Int(resource.key.split(separator: ":").last ?? ""), (1...16).contains(number) { + return 6900 + number + } + if resource.kind == .browser, resource.key.hasPrefix("port:"), + let port = Int(resource.key.dropFirst("port:".count)) { + return port + } + return nil + } + func accessModel(port: Int, address: String, scheme: String = "http") -> CloudPortAccessModel { let target = CloudPortForwardTarget(host: address, port: port) return portAccessStore.model(machineID: machineID, target: target, scheme: scheme) { diff --git a/Sources/Surfaces/Workspace+BrowserDuplication.swift b/Sources/Surfaces/Workspace+BrowserDuplication.swift index 9fe0a8d1949d..c4730f9ec6a4 100644 --- a/Sources/Surfaces/Workspace+BrowserDuplication.swift +++ b/Sources/Surfaces/Workspace+BrowserDuplication.swift @@ -8,11 +8,11 @@ extension Workspace { let paneId = paneId(forPanelId: panelId), let browser = browserPanel(for: panelId) else { return nil } let catalog = SurfaceCatalog.shared - let keepsCloudRoute = browser.cloudAccess.retainsCloudResourceForDuplication + let keepsCloudRoute = browser.retainsCloudResourceForDuplication let record = keepsCloudRoute ? catalog.projectionRecord(forPanel: panelId).flatMap { $0.resource.machine.isLocal ? nil : $0 } : nil - let resource = keepsCloudRoute ? (record?.resource ?? browser.cloudAccess.resourceID) : nil + let resource = keepsCloudRoute ? (record?.resource ?? browser.cloudResourceForDuplication) : nil guard surfaceOwnershipPolicy.rejection(for: machineOwningSurface(panelId)) == nil else { return nil } let isCloud = resource?.machine.isLocal == false let targetIndex = insertionIndexToRight(of: anchorTabId, inPane: paneId) diff --git a/tests/fixtures/cmux-display b/tests/fixtures/cmux-display index b777349e5626..2aea81296256 100755 --- a/tests/fixtures/cmux-display +++ b/tests/fixtures/cmux-display @@ -161,6 +161,7 @@ class DisplayService: self.jobs = {} self.states = {} self.processes = {} + self.named_processes = {} self.websockify_processes = {} self.shutdown = threading.Event() @@ -180,6 +181,7 @@ class DisplayService: process.terminate() except OSError: pass + self.named_processes.pop(number, None) self.terminate_websockify(number) def terminate_websockify(self, number): @@ -214,6 +216,64 @@ class DisplayService: time.sleep(0.1) return False + @staticmethod + def process_alive(process): + poll = getattr(process, "poll", None) + return poll is None or poll() is None + + def components_alive(self, number): + named = self.named_processes.get(number) + if named is not None: + return bool(named) and all(self.process_alive(process) for process in named.values()) + processes = self.processes.get(number, []) + return bool(processes) and all(self.process_alive(process) for process in processes) + + def service_ready(self, number): + if not ready(number) or not self.components_alive(number): + return False + websockify = self.websockify_processes.get(number) + return websockify is None or self.process_alive(websockify) + + def track_process(self, number, name, process): + self.processes.setdefault(number, []).append(process) + self.named_processes.setdefault(number, {})[name] = process + + def start_dbus(self, number, environment): + bus = subprocess.run(["dbus-launch", "--sh-syntax"], env=environment, + stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, text=True, timeout=10, + check=True).stdout + for line in bus.splitlines(): + if "=" not in line: + continue + key, value = line.split("=", 1) + environment[key] = value.strip().rstrip(";").strip().strip("'") + try: + dbus_pid = int(environment.get("DBUS_SESSION_BUS_PID", "0")) + except ValueError: + dbus_pid = 0 + if dbus_pid > 1: + self.track_process(number, "dbus", OwnedProcess(dbus_pid)) + + def start_session_components(self, number, environment, runtime): + for executable, args in [ + ("openbox", []), ("tint2", ["-c", "/etc/cmux/tint2rc"]), + ("vncconfig", ["-nowin"]), + ]: + existing = self.named_processes.get(number, {}).get(executable) + if existing is not None and self.process_alive(existing): + continue + path = executable if "/" in executable else shutil.which(executable) + if not path: + if executable == "vncconfig": + continue + raise RuntimeError(f"missing display component: {executable}") + path = self.scoped_component_path(executable, path, number, runtime) + process = subprocess.Popen([path] + args, env=environment, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + self.track_process(number, executable, process) + def start_websockify(self, number, environment, executable): path = executable if "/" in executable else shutil.which(executable) if not path: @@ -231,61 +291,46 @@ class DisplayService: raise NoVNCUnavailable("noVNC did not become ready") def start_components(self, number, environment, runtime): - if ready(number): + if self.service_ready(number): return rfb_is_ready = rfb_ready(number) - started_desktop = False - if not rfb_is_ready: - self.terminate(number) - started_desktop = True vnc = shutil.which("Xvnc") or shutil.which("Xtigervnc") websockify = shutil.which("websockify") if not websockify: raise NoVNCUnavailable("display runtime unavailable") if not rfb_is_ready and not vnc: raise RuntimeError("display runtime unavailable") - if started_desktop: + if not rfb_is_ready: + self.terminate(number) rfb_port = 5900 + number - processes = [] - processes.append(subprocess.Popen( + self.processes[number] = [] + self.named_processes[number] = {} + self.track_process(number, "xvnc", subprocess.Popen( [vnc, f":{number}", "-geometry", os.environ.get("CMUX_VNC_GEOMETRY", "1440x900"), "-depth", "24", "-rfbport", str(rfb_port), "-localhost", "-SecurityTypes", "None", "-AlwaysShared"], env=environment, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)) - self.processes[number] = processes if not self.wait_for_port(rfb_port): raise RuntimeError("Xvnc did not become ready") - bus = subprocess.run(["dbus-launch", "--sh-syntax"], env=environment, - stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, text=True, timeout=10, - check=True).stdout - for line in bus.splitlines(): - if "=" not in line: - continue - key, value = line.split("=", 1) - environment[key] = value.strip().rstrip(";").strip().strip("'") - try: - dbus_pid = int(environment.get("DBUS_SESSION_BUS_PID", "0")) - except ValueError: - dbus_pid = 0 - if dbus_pid > 1: - processes.append(OwnedProcess(dbus_pid)) - for executable, args in [ - ("openbox", []), ("tint2", ["-c", "/etc/cmux/tint2rc"]), - ("vncconfig", ["-nowin"]), - ]: - path = executable if "/" in executable else shutil.which(executable) - if not path: - if executable == "vncconfig": - continue - raise RuntimeError(f"missing display component: {executable}") - path = self.scoped_component_path(executable, path, number, runtime) - process = subprocess.Popen([path] + args, env=environment, - stdin=subprocess.DEVNULL, - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - processes.append(process) - if started_desktop or not novnc_ready(number): + self.start_dbus(number, environment) + self.start_session_components(number, environment, runtime) + elif number in self.named_processes: + named = self.named_processes[number] + dbus = named.get("dbus") + if dbus is None or not self.process_alive(dbus): + for name in ("openbox", "tint2", "vncconfig"): + process = named.pop(name, None) + if process is not None: + try: + process.terminate() + except OSError: + pass + self.start_dbus(number, environment) + self.start_session_components(number, environment, runtime) + websockify_process = self.websockify_processes.get(number) + if (websockify_process is None or not self.process_alive(websockify_process) + or not novnc_ready(number)): self.start_websockify(number, environment, websockify) (runtime / "env").write_text(f"export DISPLAY=:{number}\n") @@ -302,9 +347,9 @@ class DisplayService: try: # The supervisor is a health check, not a timer-driven restart. # Preserve an active RFB/noVNC session until its owned endpoints fail. - if not self.processes.get(number) or not ready(number): + if not self.service_ready(number): self.start_components(number, environment, runtime) - state = "running" if ready(number) else "unavailable" + state = "running" if self.service_ready(number) else "unavailable" except NoVNCUnavailable: self.terminate_websockify(number) state = "unavailable" From 30e94d5100285915b127e6c1f921c3b6b9c01444 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 14:39:45 -0700 Subject: [PATCH 37/64] test: reject failed display catalog responses --- cmuxTests/CloudDisplayCatalogTests.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/cmuxTests/CloudDisplayCatalogTests.swift b/cmuxTests/CloudDisplayCatalogTests.swift index c82ea9491e2f..8883d62f5c87 100644 --- a/cmuxTests/CloudDisplayCatalogTests.swift +++ b/cmuxTests/CloudDisplayCatalogTests.swift @@ -65,6 +65,20 @@ struct CloudDisplayCatalogTests { #expect(!service.canCreate && service.snapshot == nil) } + @Test("A failed guest response cannot publish a valid-looking stale catalog") + func nonzeroRefreshClearsSnapshot() async { + var failed = false + let service = CloudDisplayCoordinator { command, _ in + if failed { return .init(exitCode: 1, stdout: initial, stderr: "guest unavailable") } + return .init(exitCode: 0, stdout: initial, stderr: "") + } + await service.refresh() + #expect(service.snapshot != nil) + failed = true + await service.refresh() + #expect(service.snapshot == nil && !service.canCreate) + } + @Test("A failed refresh invalidates the cached guest catalog") func failedRefreshClearsSnapshot() async { var shouldFail = false From 10c6557fcdb53a037dcc7ed213b21d7667d89909 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 14:39:56 -0700 Subject: [PATCH 38/64] fix: fence display discovery by response and auth --- Sources/Cloud/CloudDisplayCoordinator.swift | 5 ++++- Sources/Surfaces/CmuxTuiSurfaceProviders.swift | 5 ++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Sources/Cloud/CloudDisplayCoordinator.swift b/Sources/Cloud/CloudDisplayCoordinator.swift index d8b5775730c8..ff27ee89dda8 100644 --- a/Sources/Cloud/CloudDisplayCoordinator.swift +++ b/Sources/Cloud/CloudDisplayCoordinator.swift @@ -28,10 +28,13 @@ final class CloudDisplayCoordinator { let task = Task { [weak self, execute] in do { let response = try await execute(CloudGuestDisplayScript.command(action: "list"), 10_000) + guard response.exitCode == 0 else { + throw SurfaceCatalogError.unsupported(CloudGuestDisplaySnapshot.unavailableMessage) + } let snapshot = try CloudGuestDisplaySnapshot(data: Data(response.stdout.utf8)) guard let self, token == self.generation, !Task.isCancelled else { return } self.snapshot = snapshot - self.isAvailable = response.exitCode == 0 + self.isAvailable = true } catch { guard let self, token == self.generation else { return } self.snapshot = nil diff --git a/Sources/Surfaces/CmuxTuiSurfaceProviders.swift b/Sources/Surfaces/CmuxTuiSurfaceProviders.swift index a95baad4c7b5..fa630da426f1 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProviders.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProviders.swift @@ -130,10 +130,9 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { self.portForwards = portForwards self.portAccessStore = portAccessStore ?? CloudPortAccessStore() self.browserPolicy = browserPolicy - let displayClient = VMClient.shared self.displayCoordinator = displayCoordinator ?? CloudDisplayCoordinator { command, timeout in - guard let displayClient else { throw ProviderError.notSignedIn } - return try await displayClient.exec(id: summary.id, command: command, timeoutMs: timeout) + guard let client = VMClient.shared else { throw ProviderError.notSignedIn } + return try await client.exec(id: summary.id, command: command, timeoutMs: timeout) } info = Self.info(from: summary, linkState: summary.status == "running" ? .connecting : .asleep, linkError: nil, stats: nil) installNotificationSync() From 46ce02999aa58e0cdaeb8acb39a76e0c6b5a58be Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 15:09:11 -0700 Subject: [PATCH 39/64] test: filter untrusted display restore targets --- cmuxTests/CloudDesktopAccessTests.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/cmuxTests/CloudDesktopAccessTests.swift b/cmuxTests/CloudDesktopAccessTests.swift index 4e1e163bf658..3f386cbf80f7 100644 --- a/cmuxTests/CloudDesktopAccessTests.swift +++ b/cmuxTests/CloudDesktopAccessTests.swift @@ -310,6 +310,22 @@ struct CloudDesktopAccessTests { #expect(browser.cloudRestoreURL(on: target).absoluteString == "http://10.0.0.7:8000/projects/123?tab=logs#tail") } + @Test("Display restore ignores untrusted noVNC host and port query items") + func displayRestoreFiltersTransportQuery() throws { + let browser = BrowserPanel(workspaceId: UUID(), websiteDataStore: .nonPersistent()) + defer { browser.close() } + let model = CloudPortAccessModel(target: .init(host: "10.0.0.7", port: 6902), coordinator: nil, + wake: {}, startForward: { _ in 46902 }, stopForward: {}, route: .loopback) + let display = SurfaceResourceID(machine: .cloud("a"), kind: .display, key: "display:2") + browser.cloudAccess.configure(model: model, url: URL(string: "http://10.0.0.7:6902/vnc.html")!, resourceID: display) + browser.pendingCloudRestoreURL = try #require(URL(string: "http://10.0.0.7:6902/vnc.html?host=evil.test&port=9999&path=websockify")) + let target = try #require(URL(string: "http://10.0.0.7:6902/vnc.html?path=websockify")) + let restored = browser.cloudRestoreURL(on: target) + #expect(restored.host == "10.0.0.7" && restored.port == 6902) + #expect(restored.query?.contains("host=") != true && restored.query?.contains("port=") != true) + #expect(restored.query?.contains("path=websockify") == true) + } + @Test("A forwarded /vnc.html URL is not a display when its resource is a browser") func nonDisplayVNCPathDoesNotUseDesktopReadiness() { let state = CloudBrowserAccessState() From 6dba3ecf82f96be9950a1ee9fef55aa7879af7a4 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 15:09:34 -0700 Subject: [PATCH 40/64] fix: fence display restore targets and VM kind --- .../Panels/BrowserPanel+CloudConnection.swift | 26 ++++++++++++------- .../Surfaces/CmuxTuiSurfaceProviders.swift | 8 +++++- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/Sources/Panels/BrowserPanel+CloudConnection.swift b/Sources/Panels/BrowserPanel+CloudConnection.swift index b3d0b00a6036..a8d029c6083d 100644 --- a/Sources/Panels/BrowserPanel+CloudConnection.swift +++ b/Sources/Panels/BrowserPanel+CloudConnection.swift @@ -52,28 +52,36 @@ extension BrowserPanel { switch CloudPortRoutePlan.plan(resource: known, privateAddress: provider.info.privateAddress) { case .privateDirect(let raw): if let url = URL(string: raw) { - let configured = provider.configureBrowser( - self, - url: Self.cloudRestoredURL(pendingCloudRestoreURL, on: url), - resourceID: resource - ) + let configured = provider.configureBrowser(self, + url: Self.cloudRestoredURL(pendingCloudRestoreURL, on: url, isDisplay: resource.kind == .display), + resourceID: resource) if configured { pendingCloudRestoreURL = nil } } case .unsupported(let message): cloudAccess.showUnavailable(message) } } - private static func cloudRestoredURL(_ preferred: URL?, on target: URL) -> URL { + private static func cloudRestoredURL(_ preferred: URL?, on target: URL, isDisplay: Bool = false) -> URL { guard let preferred, var components = URLComponents(url: target, resolvingAgainstBaseURL: false), let saved = URLComponents(url: preferred, resolvingAgainstBaseURL: false) else { return target } components.path = saved.path.isEmpty ? components.path : saved.path - components.query = saved.query - components.fragment = saved.fragment + if isDisplay { + let allowed = Set(["path", "autoconnect", "resize", "reconnect", "reconnect_delay"]) + let safeItems = (saved.queryItems ?? []).filter { allowed.contains($0.name.lowercased()) } + if !safeItems.isEmpty { components.queryItems = safeItems } + } else { + components.query = saved.query + components.fragment = saved.fragment + } return components.url ?? target } func cloudRestoreURL(on target: URL) -> URL { - Self.cloudRestoredURL(pendingCloudRestoreURL ?? currentURLForTabDuplication, on: target) + Self.cloudRestoredURL( + pendingCloudRestoreURL ?? currentURLForTabDuplication, + on: target, + isDisplay: cloudResourceForDuplication?.kind == .display + ) } /// Cloud panes use their own persistent data store so configuring one VM cannot reroute another. diff --git a/Sources/Surfaces/CmuxTuiSurfaceProviders.swift b/Sources/Surfaces/CmuxTuiSurfaceProviders.swift index fa630da426f1..bedb4ba49722 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProviders.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProviders.swift @@ -151,10 +151,16 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { let displayIdentityChanged = self.summary.id != summary.id || self.summary.provider != summary.provider || self.summary.image != summary.image + || self.summary.resolvedKind != summary.resolvedKind let previousPrivateAddress = info.privateAddress refreshGeneration &+= 1 refreshCoordinator.invalidate() - if displayIdentityChanged { displayCoordinator.invalidate() } + if displayIdentityChanged { + displayCoordinator.invalidate() + for resource in catalog.snapshot.resources(on: machine) where resource.kind == .display { + catalog.remove(resource.id, from: self) + } + } self.summary = summary if !supportsPortPreviews { portsCache = nil From 0c0319f4d6534118856d275152f74ae282b54631 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 15:40:40 -0700 Subject: [PATCH 41/64] test: fence browser Cloud service identity --- cmuxTests/CloudDesktopAccessTests.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/cmuxTests/CloudDesktopAccessTests.swift b/cmuxTests/CloudDesktopAccessTests.swift index 3f386cbf80f7..56ae32d4400a 100644 --- a/cmuxTests/CloudDesktopAccessTests.swift +++ b/cmuxTests/CloudDesktopAccessTests.swift @@ -326,6 +326,19 @@ struct CloudDesktopAccessTests { #expect(restored.query?.contains("path=websockify") == true) } + @Test("A browser Cloud resource cannot retain ownership after its service port changes") + func browserResourceOwnsOnlyItsPort() throws { + let endpoint = CloudBrowserProxyEndpoint(host: "127.0.0.1", port: 48000, username: "u", password: "p") + let model = CloudPortAccessModel(target: .init(host: "10.0.0.7", port: 3000), coordinator: nil, + wake: {}, startForward: { _ in 47000 }, stopForward: {}, + startBrowserProxy: { endpoint }) + let state = CloudBrowserAccessState() + state.configure(model: model, url: URL(string: "http://10.0.0.7:3000/")!, + resourceID: SurfaceResourceID(machine: .cloud("a"), kind: .browser, key: "port:3000")) + #expect(state.owns(try #require(URL(string: "http://10.0.0.7:3000/")))) + #expect(!state.owns(try #require(URL(string: "http://10.0.0.7:8000/")))) + } + @Test("A forwarded /vnc.html URL is not a display when its resource is a browser") func nonDisplayVNCPathDoesNotUseDesktopReadiness() { let state = CloudBrowserAccessState() From f58c2c636f51927863a25f563c3a3d41c88c25e6 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 15:40:48 -0700 Subject: [PATCH 42/64] test: exercise recovered display supervision --- tests/test_cloud_display_catalog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_cloud_display_catalog.py b/tests/test_cloud_display_catalog.py index 724ac947da9a..33c49c8e32d6 100644 --- a/tests/test_cloud_display_catalog.py +++ b/tests/test_cloud_display_catalog.py @@ -148,6 +148,7 @@ def terminate(self): old_websockify = Process() new_websockify = Process() service.processes[number] = [x_server] + service.named_processes[number] = {"xvnc": x_server, "dbus": Process()} service.websockify_processes[number] = old_websockify environment = {"DISPLAY": ":2"} runtime = self.root / "runtime" / "2" From fff11962f3d07755d7cce92c277687e4566c7677 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 15:40:58 -0700 Subject: [PATCH 43/64] fix: recover display supervisors and port identity --- Sources/Cloud/CloudGuestDisplayScript.swift | 264 ++++++++++-------- .../PortForward/CloudBrowserAccessState.swift | 17 +- Sources/Panels/BrowserPanel.swift | 11 +- tests/fixtures/cmux-display | 53 +++- 4 files changed, 207 insertions(+), 138 deletions(-) diff --git a/Sources/Cloud/CloudGuestDisplayScript.swift b/Sources/Cloud/CloudGuestDisplayScript.swift index 4fd47b42430c..f1ed48b86f28 100644 --- a/Sources/Cloud/CloudGuestDisplayScript.swift +++ b/Sources/Cloud/CloudGuestDisplayScript.swift @@ -90,129 +90,147 @@ CiAgICAgICAgICAgICAgICByZXR1cm4gVHJ1ZQogICAgICAgICAgICB0aW1lLnNsZWVwKDAuMSkKICAg bWV0aG9kCiAgICBkZWYgcHJvY2Vzc19hbGl2ZShwcm9jZXNzKToKICAgICAgICBwb2xsID0gZ2V0YXR0cihwcm9jZXNzLCAicG9sbCIsIE5vbmUpCiAgICAg ICAgcmV0dXJuIHBvbGwgaXMgTm9uZSBvciBwb2xsKCkgaXMgTm9uZQoKICAgIGRlZiBjb21wb25lbnRzX2FsaXZlKHNlbGYsIG51bWJlcik6CiAgICAgICAg bmFtZWQgPSBzZWxmLm5hbWVkX3Byb2Nlc3Nlcy5nZXQobnVtYmVyKQogICAgICAgIGlmIG5hbWVkIGlzIG5vdCBOb25lOgogICAgICAgICAgICByZXR1cm4g -Ym9vbChuYW1lZCkgYW5kIGFsbChzZWxmLnByb2Nlc3NfYWxpdmUocHJvY2VzcykgZm9yIHByb2Nlc3MgaW4gbmFtZWQudmFsdWVzKCkpCiAgICAgICAgcHJv -Y2Vzc2VzID0gc2VsZi5wcm9jZXNzZXMuZ2V0KG51bWJlciwgW10pCiAgICAgICAgcmV0dXJuIGJvb2wocHJvY2Vzc2VzKSBhbmQgYWxsKHNlbGYucHJvY2Vz -c19hbGl2ZShwcm9jZXNzKSBmb3IgcHJvY2VzcyBpbiBwcm9jZXNzZXMpCgogICAgZGVmIHNlcnZpY2VfcmVhZHkoc2VsZiwgbnVtYmVyKToKICAgICAgICBp -ZiBub3QgcmVhZHkobnVtYmVyKSBvciBub3Qgc2VsZi5jb21wb25lbnRzX2FsaXZlKG51bWJlcik6CiAgICAgICAgICAgIHJldHVybiBGYWxzZQogICAgICAg -IHdlYnNvY2tpZnkgPSBzZWxmLndlYnNvY2tpZnlfcHJvY2Vzc2VzLmdldChudW1iZXIpCiAgICAgICAgcmV0dXJuIHdlYnNvY2tpZnkgaXMgTm9uZSBvciBz -ZWxmLnByb2Nlc3NfYWxpdmUod2Vic29ja2lmeSkKCiAgICBkZWYgdHJhY2tfcHJvY2VzcyhzZWxmLCBudW1iZXIsIG5hbWUsIHByb2Nlc3MpOgogICAgICAg -IHNlbGYucHJvY2Vzc2VzLnNldGRlZmF1bHQobnVtYmVyLCBbXSkuYXBwZW5kKHByb2Nlc3MpCiAgICAgICAgc2VsZi5uYW1lZF9wcm9jZXNzZXMuc2V0ZGVm -YXVsdChudW1iZXIsIHt9KVtuYW1lXSA9IHByb2Nlc3MKCiAgICBkZWYgc3RhcnRfZGJ1cyhzZWxmLCBudW1iZXIsIGVudmlyb25tZW50KToKICAgICAgICBi -dXMgPSBzdWJwcm9jZXNzLnJ1bihbImRidXMtbGF1bmNoIiwgIi0tc2gtc3ludGF4Il0sIGVudj1lbnZpcm9ubWVudCwKICAgICAgICAgICAgICAgICAgICAg -ICAgICAgICBzdGRpbj1zdWJwcm9jZXNzLkRFVk5VTEwsIHN0ZG91dD1zdWJwcm9jZXNzLlBJUEUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3Rk -ZXJyPXN1YnByb2Nlc3MuREVWTlVMTCwgdGV4dD1UcnVlLCB0aW1lb3V0PTEwLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNoZWNrPVRydWUpLnN0 -ZG91dAogICAgICAgIGZvciBsaW5lIGluIGJ1cy5zcGxpdGxpbmVzKCk6CiAgICAgICAgICAgIGlmICI9IiBub3QgaW4gbGluZToKICAgICAgICAgICAgICAg -IGNvbnRpbnVlCiAgICAgICAgICAgIGtleSwgdmFsdWUgPSBsaW5lLnNwbGl0KCI9IiwgMSkKICAgICAgICAgICAgZW52aXJvbm1lbnRba2V5XSA9IHZhbHVl -LnN0cmlwKCkucnN0cmlwKCI7Iikuc3RyaXAoKS5zdHJpcCgiJyIpCiAgICAgICAgdHJ5OgogICAgICAgICAgICBkYnVzX3BpZCA9IGludChlbnZpcm9ubWVu -dC5nZXQoIkRCVVNfU0VTU0lPTl9CVVNfUElEIiwgIjAiKSkKICAgICAgICBleGNlcHQgVmFsdWVFcnJvcjoKICAgICAgICAgICAgZGJ1c19waWQgPSAwCiAg -ICAgICAgaWYgZGJ1c19waWQgPiAxOgogICAgICAgICAgICBzZWxmLnRyYWNrX3Byb2Nlc3MobnVtYmVyLCAiZGJ1cyIsIE93bmVkUHJvY2VzcyhkYnVzX3Bp -ZCkpCgogICAgZGVmIHN0YXJ0X3Nlc3Npb25fY29tcG9uZW50cyhzZWxmLCBudW1iZXIsIGVudmlyb25tZW50LCBydW50aW1lKToKICAgICAgICBmb3IgZXhl -Y3V0YWJsZSwgYXJncyBpbiBbCiAgICAgICAgICAgICgib3BlbmJveCIsIFtdKSwgKCJ0aW50MiIsIFsiLWMiLCAiL2V0Yy9jbXV4L3RpbnQycmMiXSksCiAg -ICAgICAgICAgICgidm5jY29uZmlnIiwgWyItbm93aW4iXSksCiAgICAgICAgXToKICAgICAgICAgICAgZXhpc3RpbmcgPSBzZWxmLm5hbWVkX3Byb2Nlc3Nl -cy5nZXQobnVtYmVyLCB7fSkuZ2V0KGV4ZWN1dGFibGUpCiAgICAgICAgICAgIGlmIGV4aXN0aW5nIGlzIG5vdCBOb25lIGFuZCBzZWxmLnByb2Nlc3NfYWxp -dmUoZXhpc3RpbmcpOgogICAgICAgICAgICAgICAgY29udGludWUKICAgICAgICAgICAgcGF0aCA9IGV4ZWN1dGFibGUgaWYgIi8iIGluIGV4ZWN1dGFibGUg -ZWxzZSBzaHV0aWwud2hpY2goZXhlY3V0YWJsZSkKICAgICAgICAgICAgaWYgbm90IHBhdGg6CiAgICAgICAgICAgICAgICBpZiBleGVjdXRhYmxlID09ICJ2 -bmNjb25maWciOgogICAgICAgICAgICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgICAgICAgICByYWlzZSBSdW50aW1lRXJyb3IoZiJtaXNzaW5nIGRpc3Bs -YXkgY29tcG9uZW50OiB7ZXhlY3V0YWJsZX0iKQogICAgICAgICAgICBwYXRoID0gc2VsZi5zY29wZWRfY29tcG9uZW50X3BhdGgoZXhlY3V0YWJsZSwgcGF0 -aCwgbnVtYmVyLCBydW50aW1lKQogICAgICAgICAgICBwcm9jZXNzID0gc3VicHJvY2Vzcy5Qb3BlbihbcGF0aF0gKyBhcmdzLCBlbnY9ZW52aXJvbm1lbnQs -CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0ZGluPXN1YnByb2Nlc3MuREVWTlVMTCwKICAgICAgICAgICAgICAgICAgICAgICAg -ICAgICAgICAgICAgICAgc3Rkb3V0PXN1YnByb2Nlc3MuREVWTlVMTCwgc3RkZXJyPXN1YnByb2Nlc3MuREVWTlVMTCkKICAgICAgICAgICAgc2VsZi50cmFj -a19wcm9jZXNzKG51bWJlciwgZXhlY3V0YWJsZSwgcHJvY2VzcykKCiAgICBkZWYgc3RhcnRfd2Vic29ja2lmeShzZWxmLCBudW1iZXIsIGVudmlyb25tZW50 -LCBleGVjdXRhYmxlKToKICAgICAgICBwYXRoID0gZXhlY3V0YWJsZSBpZiAiLyIgaW4gZXhlY3V0YWJsZSBlbHNlIHNodXRpbC53aGljaChleGVjdXRhYmxl -KQogICAgICAgIGlmIG5vdCBwYXRoOgogICAgICAgICAgICByYWlzZSBOb1ZOQ1VuYXZhaWxhYmxlKCJtaXNzaW5nIGRpc3BsYXkgY29tcG9uZW50OiB3ZWJz -b2NraWZ5IikKICAgICAgICBzZWxmLnRlcm1pbmF0ZV93ZWJzb2NraWZ5KG51bWJlcikKICAgICAgICByZmJfcG9ydCwgbm92bmNfcG9ydCA9IDU5MDAgKyBu -dW1iZXIsIDY5MDAgKyBudW1iZXIKICAgICAgICBwcm9jZXNzID0gc3VicHJvY2Vzcy5Qb3BlbigKICAgICAgICAgICAgW3BhdGgsICItLXdlYiIsICIvdXNy -L3NoYXJlL25vdm5jIiwgIi0taGVhcnRiZWF0IiwgIjMwIiwKICAgICAgICAgICAgIGYiMTI3LjAuMC4xOntub3ZuY19wb3J0fSIsIGYiMTI3LjAuMC4xOnty -ZmJfcG9ydH0iXSwKICAgICAgICAgICAgZW52PWVudmlyb25tZW50LCBzdGRpbj1zdWJwcm9jZXNzLkRFVk5VTEwsCiAgICAgICAgICAgIHN0ZG91dD1zdWJw -cm9jZXNzLkRFVk5VTEwsIHN0ZGVycj1zdWJwcm9jZXNzLkRFVk5VTEwpCiAgICAgICAgc2VsZi53ZWJzb2NraWZ5X3Byb2Nlc3Nlc1tudW1iZXJdID0gcHJv -Y2VzcwogICAgICAgIGlmIG5vdCBzZWxmLndhaXRfZm9yX3BvcnQobm92bmNfcG9ydCwgdGltZW91dD0xMCk6CiAgICAgICAgICAgIHNlbGYudGVybWluYXRl -X3dlYnNvY2tpZnkobnVtYmVyKQogICAgICAgICAgICByYWlzZSBOb1ZOQ1VuYXZhaWxhYmxlKCJub1ZOQyBkaWQgbm90IGJlY29tZSByZWFkeSIpCgogICAg -ZGVmIHN0YXJ0X2NvbXBvbmVudHMoc2VsZiwgbnVtYmVyLCBlbnZpcm9ubWVudCwgcnVudGltZSk6CiAgICAgICAgaWYgc2VsZi5zZXJ2aWNlX3JlYWR5KG51 -bWJlcik6CiAgICAgICAgICAgIHJldHVybgogICAgICAgIHJmYl9pc19yZWFkeSA9IHJmYl9yZWFkeShudW1iZXIpCiAgICAgICAgdm5jID0gc2h1dGlsLndo -aWNoKCJYdm5jIikgb3Igc2h1dGlsLndoaWNoKCJYdGlnZXJ2bmMiKQogICAgICAgIHdlYnNvY2tpZnkgPSBzaHV0aWwud2hpY2goIndlYnNvY2tpZnkiKQog -ICAgICAgIGlmIG5vdCB3ZWJzb2NraWZ5OgogICAgICAgICAgICByYWlzZSBOb1ZOQ1VuYXZhaWxhYmxlKCJkaXNwbGF5IHJ1bnRpbWUgdW5hdmFpbGFibGUi -KQogICAgICAgIGlmIG5vdCByZmJfaXNfcmVhZHkgYW5kIG5vdCB2bmM6CiAgICAgICAgICAgIHJhaXNlIFJ1bnRpbWVFcnJvcigiZGlzcGxheSBydW50aW1l -IHVuYXZhaWxhYmxlIikKICAgICAgICBpZiBub3QgcmZiX2lzX3JlYWR5OgogICAgICAgICAgICBzZWxmLnRlcm1pbmF0ZShudW1iZXIpCiAgICAgICAgICAg -IHJmYl9wb3J0ID0gNTkwMCArIG51bWJlcgogICAgICAgICAgICBzZWxmLnByb2Nlc3Nlc1tudW1iZXJdID0gW10KICAgICAgICAgICAgc2VsZi5uYW1lZF9w -cm9jZXNzZXNbbnVtYmVyXSA9IHt9CiAgICAgICAgICAgIHNlbGYudHJhY2tfcHJvY2VzcyhudW1iZXIsICJ4dm5jIiwgc3VicHJvY2Vzcy5Qb3BlbigKICAg -ICAgICAgICAgICAgIFt2bmMsIGYiOntudW1iZXJ9IiwgIi1nZW9tZXRyeSIsIG9zLmVudmlyb24uZ2V0KCJDTVVYX1ZOQ19HRU9NRVRSWSIsICIxNDQweDkw -MCIpLAogICAgICAgICAgICAgICAgICItZGVwdGgiLCAiMjQiLCAiLXJmYnBvcnQiLCBzdHIocmZiX3BvcnQpLCAiLWxvY2FsaG9zdCIsCiAgICAgICAgICAg -ICAgICAgIi1TZWN1cml0eVR5cGVzIiwgIk5vbmUiLCAiLUFsd2F5c1NoYXJlZCJdLAogICAgICAgICAgICAgICAgZW52PWVudmlyb25tZW50LCBzdGRpbj1z -dWJwcm9jZXNzLkRFVk5VTEwsCiAgICAgICAgICAgICAgICBzdGRvdXQ9c3VicHJvY2Vzcy5ERVZOVUxMLCBzdGRlcnI9c3VicHJvY2Vzcy5ERVZOVUxMKSkK -ICAgICAgICAgICAgaWYgbm90IHNlbGYud2FpdF9mb3JfcG9ydChyZmJfcG9ydCk6CiAgICAgICAgICAgICAgICByYWlzZSBSdW50aW1lRXJyb3IoIlh2bmMg -ZGlkIG5vdCBiZWNvbWUgcmVhZHkiKQogICAgICAgICAgICBzZWxmLnN0YXJ0X2RidXMobnVtYmVyLCBlbnZpcm9ubWVudCkKICAgICAgICAgICAgc2VsZi5z -dGFydF9zZXNzaW9uX2NvbXBvbmVudHMobnVtYmVyLCBlbnZpcm9ubWVudCwgcnVudGltZSkKICAgICAgICBlbGlmIG51bWJlciBpbiBzZWxmLm5hbWVkX3By -b2Nlc3NlczoKICAgICAgICAgICAgbmFtZWQgPSBzZWxmLm5hbWVkX3Byb2Nlc3Nlc1tudW1iZXJdCiAgICAgICAgICAgIGRidXMgPSBuYW1lZC5nZXQoImRi -dXMiKQogICAgICAgICAgICBpZiBkYnVzIGlzIE5vbmUgb3Igbm90IHNlbGYucHJvY2Vzc19hbGl2ZShkYnVzKToKICAgICAgICAgICAgICAgIGZvciBuYW1l -IGluICgib3BlbmJveCIsICJ0aW50MiIsICJ2bmNjb25maWciKToKICAgICAgICAgICAgICAgICAgICBwcm9jZXNzID0gbmFtZWQucG9wKG5hbWUsIE5vbmUp -CiAgICAgICAgICAgICAgICAgICAgaWYgcHJvY2VzcyBpcyBub3QgTm9uZToKICAgICAgICAgICAgICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAg -ICAgICAgICAgICAgcHJvY2Vzcy50ZXJtaW5hdGUoKQogICAgICAgICAgICAgICAgICAgICAgICBleGNlcHQgT1NFcnJvcjoKICAgICAgICAgICAgICAgICAg -ICAgICAgICAgIHBhc3MKICAgICAgICAgICAgICAgIHNlbGYuc3RhcnRfZGJ1cyhudW1iZXIsIGVudmlyb25tZW50KQogICAgICAgICAgICBzZWxmLnN0YXJ0 -X3Nlc3Npb25fY29tcG9uZW50cyhudW1iZXIsIGVudmlyb25tZW50LCBydW50aW1lKQogICAgICAgIHdlYnNvY2tpZnlfcHJvY2VzcyA9IHNlbGYud2Vic29j -a2lmeV9wcm9jZXNzZXMuZ2V0KG51bWJlcikKICAgICAgICBpZiAod2Vic29ja2lmeV9wcm9jZXNzIGlzIE5vbmUgb3Igbm90IHNlbGYucHJvY2Vzc19hbGl2 -ZSh3ZWJzb2NraWZ5X3Byb2Nlc3MpCiAgICAgICAgICAgICAgICBvciBub3Qgbm92bmNfcmVhZHkobnVtYmVyKSk6CiAgICAgICAgICAgIHNlbGYuc3RhcnRf -d2Vic29ja2lmeShudW1iZXIsIGVudmlyb25tZW50LCB3ZWJzb2NraWZ5KQogICAgICAgIChydW50aW1lIC8gImVudiIpLndyaXRlX3RleHQoZiJleHBvcnQg -RElTUExBWT06e251bWJlcn1cbiIpCgogICAgZGVmIHN1cGVydmlzZShzZWxmLCBudW1iZXIsIGZpbmlzaGVkKToKICAgICAgICBydW50aW1lID0gc2VsZi5y -dW50aW1lIC8gc3RyKG51bWJlcikKICAgICAgICBydW50aW1lLm1rZGlyKG1vZGU9MG83MDAsIHBhcmVudHM9VHJ1ZSwgZXhpc3Rfb2s9VHJ1ZSkKICAgICAg -ICBlbnZpcm9ubWVudCA9IGRpY3Qob3MuZW52aXJvbiwgRElTUExBWT1mIjp7bnVtYmVyfSIsIENNVVhfREVTS1RPUF9SVU5USU1FX0RJUj1zdHIocnVudGlt -ZSkpCiAgICAgICAgZW52aXJvbm1lbnRbIkNIUk9NRV9DT05GSUdfSE9NRSJdID0gc3RyKHJ1bnRpbWUgLyAiY2hyb21lLWNvbmZpZyIpCiAgICAgICAgZW52 -aXJvbm1lbnRbIlhER19DT05GSUdfSE9NRSJdID0gc3RyKHJ1bnRpbWUgLyAieGRnLWNvbmZpZyIpCiAgICAgICAgZm9yIGtleSBpbiAoIk5PVElGWV9TT0NL -RVQiLCAiREJVU19TRVNTSU9OX0JVU19BRERSRVNTIiwgIkRCVVNfU0VTU0lPTl9CVVNfUElEIiwKICAgICAgICAgICAgICAgICAgICAiQVRfU1BJX0JVU19B -RERSRVNTIiwgIkFUX1NQSV9CVVMiKToKICAgICAgICAgICAgZW52aXJvbm1lbnQucG9wKGtleSwgTm9uZSkKICAgICAgICB3aGlsZSBub3Qgc2VsZi5zaHV0 -ZG93bi5pc19zZXQoKToKICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgIyBUaGUgc3VwZXJ2aXNvciBpcyBhIGhlYWx0aCBjaGVjaywgbm90IGEg -dGltZXItZHJpdmVuIHJlc3RhcnQuCiAgICAgICAgICAgICAgICAjIFByZXNlcnZlIGFuIGFjdGl2ZSBSRkIvbm9WTkMgc2Vzc2lvbiB1bnRpbCBpdHMgb3du -ZWQgZW5kcG9pbnRzIGZhaWwuCiAgICAgICAgICAgICAgICBpZiBub3Qgc2VsZi5zZXJ2aWNlX3JlYWR5KG51bWJlcik6CiAgICAgICAgICAgICAgICAgICAg -c2VsZi5zdGFydF9jb21wb25lbnRzKG51bWJlciwgZW52aXJvbm1lbnQsIHJ1bnRpbWUpCiAgICAgICAgICAgICAgICBzdGF0ZSA9ICJydW5uaW5nIiBpZiBz -ZWxmLnNlcnZpY2VfcmVhZHkobnVtYmVyKSBlbHNlICJ1bmF2YWlsYWJsZSIKICAgICAgICAgICAgZXhjZXB0IE5vVk5DVW5hdmFpbGFibGU6CiAgICAgICAg -ICAgICAgICBzZWxmLnRlcm1pbmF0ZV93ZWJzb2NraWZ5KG51bWJlcikKICAgICAgICAgICAgICAgIHN0YXRlID0gInVuYXZhaWxhYmxlIgogICAgICAgICAg -ICBleGNlcHQgKE9TRXJyb3IsIFJ1bnRpbWVFcnJvciwgc3VicHJvY2Vzcy5TdWJwcm9jZXNzRXJyb3IpOgogICAgICAgICAgICAgICAgc2VsZi50ZXJtaW5h -dGUobnVtYmVyKQogICAgICAgICAgICAgICAgc3RhdGUgPSAidW5hdmFpbGFibGUiCiAgICAgICAgICAgIHdpdGggc2VsZi5sb2NrOgogICAgICAgICAgICAg -ICAgc2VsZi5zdGF0ZXNbbnVtYmVyXSA9IHN0YXRlCiAgICAgICAgICAgIGZpbmlzaGVkLnNldCgpCiAgICAgICAgICAgIHNlbGYuc2h1dGRvd24ud2FpdCgz -MCkKICAgICAgICBzZWxmLnRlcm1pbmF0ZShudW1iZXIpCgogICAgZGVmIHNuYXBzaG90KHNlbGYpOgogICAgICAgIHdpdGggc2VsZi5sb2NrOgogICAgICAg -ICAgICByZXR1cm4geyJ2ZXJzaW9uIjogMSwgImNhbkNyZWF0ZSI6IGxlbihzZWxmLmpvYnMpIDwgTUFYX0RJU1BMQVlTIC0gMSwKICAgICAgICAgICAgICAg -ICAgICAiZGlzcGxheXMiOiBbZGVzY3JpcHRvcigxLCAicnVubmluZyIgaWYgcmVhZHkoMSkgZWxzZSAidW5hdmFpbGFibGUiKV0KICAgICAgICAgICAgICAg -ICAgICArIFtkZXNjcmlwdG9yKG4sIHNlbGYuc3RhdGVzLmdldChuLCAibGF1bmNoaW5nIikpIGZvciBuIGluIHNlbGYuY2F0YWxvZy5udW1iZXJzKCldfQoK -ICAgIGRlZiBoYW5kbGUoc2VsZiwgcmVxdWVzdCk6CiAgICAgICAgaWYgcmVxdWVzdC5nZXQoImFjdGlvbiIpID09ICJsaXN0IjoKICAgICAgICAgICAgcmV0 -dXJuIHNlbGYuc25hcHNob3QoKQogICAgICAgIGlmIHJlcXVlc3QuZ2V0KCJhY3Rpb24iKSAhPSAiY3JlYXRlIjoKICAgICAgICAgICAgcmFpc2UgVmFsdWVF -cnJvcigidW5zdXBwb3J0ZWQgZGlzcGxheSBhY3Rpb24iKQogICAgICAgIG51bWJlciA9IHNlbGYuY2F0YWxvZy5hbGxvY2F0ZShyZXF1ZXN0LmdldCgicmVx -dWVzdCIpKQogICAgICAgIHNlbGYuc3RhcnQobnVtYmVyKS53YWl0KDU1KQogICAgICAgIHJlc3VsdCA9IHNlbGYuc25hcHNob3QoKQogICAgICAgIHJlc3Vs -dFsiY3JlYXRlZCJdID0gZiJkaXNwbGF5OntudW1iZXJ9IgogICAgICAgIGlmIHNlbGYuc3RhdGVzLmdldChudW1iZXIpICE9ICJydW5uaW5nIjoKICAgICAg -ICAgICAgcmVzdWx0WyJlcnJvciJdID0gImRpc3BsYXlfc3RhcnRfZmFpbGVkIgogICAgICAgIHJldHVybiByZXN1bHQKCgpkZWYgc2VydmUocnVudGltZSk6 -CiAgICBydW50aW1lID0gUGF0aChydW50aW1lKQogICAgcnVudGltZS5ta2Rpcihtb2RlPTBvNzAwLCBwYXJlbnRzPVRydWUsIGV4aXN0X29rPVRydWUpCiAg -ICB3aXRoIChydW50aW1lIC8gImRpc3BsYXktY29udHJvbC5sb2NrIikub3BlbigidyIpIGFzIGxvY2s6CiAgICAgICAgdHJ5OgogICAgICAgICAgICBmY250 -bC5mbG9jayhsb2NrLCBmY250bC5MT0NLX0VYIHwgZmNudGwuTE9DS19OQikKICAgICAgICBleGNlcHQgQmxvY2tpbmdJT0Vycm9yOgogICAgICAgICAgICBy -ZXR1cm4KICAgICAgICBpZiBub3QgYWxsKHNodXRpbC53aGljaChuYW1lKSBmb3IgbmFtZSBpbiAoIndlYnNvY2tpZnkiLCAib3BlbmJveCIsICJkYnVzLWxh -dW5jaCIpKToKICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJvcigiZGlzcGxheSBydW50aW1lIHVuYXZhaWxhYmxlIikKICAgICAgICBpZiBub3QgKHNodXRp -bC53aGljaCgiWHZuYyIpIG9yIHNodXRpbC53aGljaCgiWHRpZ2Vydm5jIikpOgogICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJkaXNwbGF5IHJ1bnRp -bWUgdW5hdmFpbGFibGUiKQogICAgICAgIHNlcnZpY2UgPSBEaXNwbGF5U2VydmljZShEaXNwbGF5Q2F0YWxvZyhQYXRoLmhvbWUoKSAvICIuY211eCIgLyAi -ZGlzcGxheXMiKSwgcnVudGltZSAvICJkaXNwbGF5cyIpCiAgICAgICAgZm9yIG51bWJlciBpbiBzZXJ2aWNlLmNhdGFsb2cubnVtYmVycygpOgogICAgICAg -ICAgICBzZXJ2aWNlLnN0YXJ0KG51bWJlcikKICAgICAgICBwYXRoID0gcnVudGltZSAvICJkaXNwbGF5LWNvbnRyb2wuc29jayIKICAgICAgICBpZiBwYXRo -LmV4aXN0cygpOgogICAgICAgICAgICBwYXRoLnVubGluaygpCgogICAgICAgIGNsYXNzIEhhbmRsZXIoc29ja2V0c2VydmVyLlN0cmVhbVJlcXVlc3RIYW5k -bGVyKToKICAgICAgICAgICAgZGVmIGhhbmRsZShzZWxmKToKICAgICAgICAgICAgICAgIHNlbGYuY29ubmVjdGlvbi5zZXR0aW1lb3V0KDYwKQogICAgICAg -ICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgIHJhdyA9IHNlbGYucmZpbGUucmVhZGxpbmUoNDA5NykKICAgICAgICAgICAgICAgICAgICBpZiBs -ZW4ocmF3KSA+IDQwOTY6CiAgICAgICAgICAgICAgICAgICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoImRpc3BsYXkgcmVxdWVzdCB0b28gbGFyZ2UiKQogICAg -ICAgICAgICAgICAgICAgIHJlc3VsdCA9IHNlcnZpY2UuaGFuZGxlKGpzb24ubG9hZHMocmF3KSkKICAgICAgICAgICAgICAgIGV4Y2VwdCAoVmFsdWVFcnJv -ciwgS2V5RXJyb3IsIFR5cGVFcnJvciwgT1NFcnJvcik6CiAgICAgICAgICAgICAgICAgICAgcmVzdWx0ID0geyJlcnJvciI6ICJkaXNwbGF5X3JlcXVlc3Rf -ZmFpbGVkIn0KICAgICAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgICAgICBzZWxmLndmaWxlLndyaXRlKGpzb24uZHVtcHMocmVzdWx0KS5lbmNv -ZGUoKSArIGIiXG4iKQogICAgICAgICAgICAgICAgZXhjZXB0IE9TRXJyb3I6CiAgICAgICAgICAgICAgICAgICAgcGFzcyAgIyBUaGUgcGVyc2lzdGVkIHJl -Y2VpcHQgcmVtYWlucyByZXBsYXlhYmxlIGFmdGVyIGRpc2Nvbm5lY3QuCgogICAgICAgIGNsYXNzIFNlcnZlcihzb2NrZXRzZXJ2ZXIuVGhyZWFkaW5nVW5p -eFN0cmVhbVNlcnZlcik6CiAgICAgICAgICAgIGRhZW1vbl90aHJlYWRzID0gVHJ1ZQoKICAgICAgICBwcmV2aW91c191bWFzayA9IG9zLnVtYXNrKDBvMDc3 -KQogICAgICAgIHRyeToKICAgICAgICAgICAgd2l0aCBTZXJ2ZXIoc3RyKHBhdGgpLCBIYW5kbGVyKSBhcyBzZXJ2ZXI6CiAgICAgICAgICAgICAgICBvcy51 -bWFzayhwcmV2aW91c191bWFzaykKICAgICAgICAgICAgICAgIHNlcnZlci5zZXJ2ZV9mb3JldmVyKCkKICAgICAgICBmaW5hbGx5OgogICAgICAgICAgICBv -cy51bWFzayhwcmV2aW91c191bWFzaykKICAgICAgICAgICAgc2VydmljZS5zaHV0ZG93bi5zZXQoKQogICAgICAgICAgICBwYXRoLnVubGluayhtaXNzaW5n -X29rPVRydWUpCgoKZGVmIG1haW4oKToKICAgIHBhcnNlciA9IGFyZ3BhcnNlLkFyZ3VtZW50UGFyc2VyKGRlc2NyaXB0aW9uPV9fZG9jX18pCiAgICBwYXJz -ZXIuYWRkX2FyZ3VtZW50KCJhY3Rpb24iLCBjaG9pY2VzPVsibGlzdCIsICJjcmVhdGUiLCAic2VydmUiXSkKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoIi0t -cmVxdWVzdC1pZCIpCiAgICBhcmdzID0gcGFyc2VyLnBhcnNlX2FyZ3MoKQogICAgcnVudGltZSA9IG9zLmVudmlyb24uZ2V0KCJDTVVYX0RFU0tUT1BfUlVO -VElNRV9ESVIiLCAiL3J1bi9jbXV4LWRlc2t0b3AiKQogICAgaWYgYXJncy5hY3Rpb24gPT0gInNlcnZlIjoKICAgICAgICBzZXJ2ZShydW50aW1lKQogICAg -ICAgIHJldHVybiAwCiAgICBpZiBhcmdzLmFjdGlvbiA9PSAiY3JlYXRlIiBhbmQgbm90IGFyZ3MucmVxdWVzdF9pZDoKICAgICAgICBwYXJzZXIuZXJyb3Io -ImNyZWF0ZSByZXF1aXJlcyAtLXJlcXVlc3QtaWQgKHJldGFpbiBpdCB3aGVuIHJldHJ5aW5nKSIpCiAgICB0cnk6CiAgICAgICAgd2l0aCBzb2NrZXQuc29j -a2V0KHNvY2tldC5BRl9VTklYLCBzb2NrZXQuU09DS19TVFJFQU0pIGFzIGNsaWVudDoKICAgICAgICAgICAgY2xpZW50LnNldHRpbWVvdXQoNjApCiAgICAg -ICAgICAgIGNsaWVudC5jb25uZWN0KHN0cihQYXRoKHJ1bnRpbWUpIC8gImRpc3BsYXktY29udHJvbC5zb2NrIikpCiAgICAgICAgICAgIGNsaWVudC5zZW5k -YWxsKGpzb24uZHVtcHMoeyJhY3Rpb24iOiBhcmdzLmFjdGlvbiwgInJlcXVlc3QiOiBhcmdzLnJlcXVlc3RfaWR9KS5lbmNvZGUoKSArIGIiXG4iKQogICAg -ICAgICAgICB3aXRoIGNsaWVudC5tYWtlZmlsZSgicmIiKSBhcyBzdHJlYW06CiAgICAgICAgICAgICAgICByZXN1bHQgPSBqc29uLmxvYWRzKHN0cmVhbS5y -ZWFkbGluZSg2NTUzNikpCiAgICBleGNlcHQgKE9TRXJyb3IsIFZhbHVlRXJyb3IpOgogICAgICAgIHJlc3VsdCA9IHsiZXJyb3IiOiAiZGlzcGxheV9ydW50 -aW1lX3VuYXZhaWxhYmxlIn0KICAgIHByaW50KGpzb24uZHVtcHMocmVzdWx0KSkKICAgIHJldHVybiAxIGlmICJlcnJvciIgaW4gcmVzdWx0IGVsc2UgMAoK -CmlmIF9fbmFtZV9fID09ICJfX21haW5fXyI6CiAgICByYWlzZSBTeXN0ZW1FeGl0KG1haW4oKSkK +Ym9vbChuYW1lZCkgYW5kIGFsbChzZWxmLnByb2Nlc3NfYWxpdmUocHJvY2VzcykgZm9yIHByb2Nlc3MgaW4gbmFtZWQudmFsdWVzKCkpIFwKICAgICAgICAg +ICAgICAgIGFuZCAoInh2bmMiIGluIG5hbWVkIG9yIHJmYl9yZWFkeShudW1iZXIpKQogICAgICAgIHByb2Nlc3NlcyA9IHNlbGYucHJvY2Vzc2VzLmdldChu +dW1iZXIsIFtdKQogICAgICAgIHJldHVybiBib29sKHByb2Nlc3NlcykgYW5kIGFsbChzZWxmLnByb2Nlc3NfYWxpdmUocHJvY2VzcykgZm9yIHByb2Nlc3Mg +aW4gcHJvY2Vzc2VzKQoKICAgIGRlZiBzZXJ2aWNlX3JlYWR5KHNlbGYsIG51bWJlcik6CiAgICAgICAgaWYgbm90IHJlYWR5KG51bWJlcikgb3Igbm90IHNl +bGYuY29tcG9uZW50c19hbGl2ZShudW1iZXIpOgogICAgICAgICAgICByZXR1cm4gRmFsc2UKICAgICAgICB3ZWJzb2NraWZ5ID0gc2VsZi53ZWJzb2NraWZ5 +X3Byb2Nlc3Nlcy5nZXQobnVtYmVyKQogICAgICAgIHJldHVybiB3ZWJzb2NraWZ5IGlzIE5vbmUgb3Igc2VsZi5wcm9jZXNzX2FsaXZlKHdlYnNvY2tpZnkp +CgogICAgZGVmIHRyYWNrX3Byb2Nlc3Moc2VsZiwgbnVtYmVyLCBuYW1lLCBwcm9jZXNzKToKICAgICAgICBuYW1lZCA9IHNlbGYubmFtZWRfcHJvY2Vzc2Vz +LnNldGRlZmF1bHQobnVtYmVyLCB7fSkKICAgICAgICBwcmV2aW91cyA9IG5hbWVkLmdldChuYW1lKQogICAgICAgIGlmIHByZXZpb3VzIGlzIG5vdCBOb25l +OgogICAgICAgICAgICBzZWxmLnByb2Nlc3Nlc1tudW1iZXJdID0gWwogICAgICAgICAgICAgICAgaXRlbSBmb3IgaXRlbSBpbiBzZWxmLnByb2Nlc3Nlcy5n +ZXQobnVtYmVyLCBbXSkgaWYgaXRlbSBpcyBub3QgcHJldmlvdXMKICAgICAgICAgICAgXQogICAgICAgIHNlbGYucHJvY2Vzc2VzLnNldGRlZmF1bHQobnVt +YmVyLCBbXSkuYXBwZW5kKHByb2Nlc3MpCiAgICAgICAgbmFtZWRbbmFtZV0gPSBwcm9jZXNzCgogICAgZGVmIHJlbW92ZV9wcm9jZXNzKHNlbGYsIG51bWJl +ciwgbmFtZSk6CiAgICAgICAgcHJvY2VzcyA9IHNlbGYubmFtZWRfcHJvY2Vzc2VzLnNldGRlZmF1bHQobnVtYmVyLCB7fSkucG9wKG5hbWUsIE5vbmUpCiAg +ICAgICAgaWYgcHJvY2VzcyBpcyBOb25lOgogICAgICAgICAgICByZXR1cm4KICAgICAgICBzZWxmLnByb2Nlc3Nlc1tudW1iZXJdID0gW2l0ZW0gZm9yIGl0 +ZW0gaW4gc2VsZi5wcm9jZXNzZXMuZ2V0KG51bWJlciwgW10pIGlmIGl0ZW0gaXMgbm90IHByb2Nlc3NdCiAgICAgICAgdHJ5OgogICAgICAgICAgICBwcm9j +ZXNzLnRlcm1pbmF0ZSgpCiAgICAgICAgZXhjZXB0IE9TRXJyb3I6CiAgICAgICAgICAgIHBhc3MKCiAgICBkZWYgcmVjb3Zlcl9wcm9jZXNzZXMoc2VsZiwg +bnVtYmVyKToKICAgICAgICBzZWxmLnByb2Nlc3Nlc1tudW1iZXJdID0gW10KICAgICAgICBzZWxmLm5hbWVkX3Byb2Nlc3Nlc1tudW1iZXJdID0ge30KICAg +ICAgICB1aWQgPSBzdHIob3MuZ2V0dWlkKCkpCiAgICAgICAgZm9yIG5hbWUgaW4gKCJvcGVuYm94IiwgInRpbnQyIiwgInZuY2NvbmZpZyIpOgogICAgICAg +ICAgICBtYXJrZXIgPSBmImNtdXgtZGlzcGxheS17bnVtYmVyfS17bmFtZX0iCiAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgIG91dHB1dCA9IHN1 +YnByb2Nlc3MuY2hlY2tfb3V0cHV0KFsicGdyZXAiLCAiLXUiLCB1aWQsICIteCIsIG1hcmtlcl0sIHRleHQ9VHJ1ZSkKICAgICAgICAgICAgICAgIHBpZCA9 +IGludChvdXRwdXQuc3BsaXRsaW5lcygpWzBdKQogICAgICAgICAgICBleGNlcHQgKE9TRXJyb3IsIFZhbHVlRXJyb3IsIEluZGV4RXJyb3IsIHN1YnByb2Nl +c3MuQ2FsbGVkUHJvY2Vzc0Vycm9yKToKICAgICAgICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgICAgIHNlbGYudHJhY2tfcHJvY2VzcyhudW1iZXIsIG5h +bWUsIE93bmVkUHJvY2VzcyhwaWQpKQogICAgICAgIHRyeToKICAgICAgICAgICAgb3V0cHV0ID0gc3VicHJvY2Vzcy5jaGVja19vdXRwdXQoCiAgICAgICAg +ICAgICAgICBbInBncmVwIiwgIi11IiwgdWlkLCAiLWYiLCByZiIoPzpYdm5jfFh0aWdlcnZuYykuKjp7bnVtYmVyfSg/OlxzfCQpIl0sCiAgICAgICAgICAg +ICAgICB0ZXh0PVRydWUKICAgICAgICAgICAgKQogICAgICAgICAgICBwaWQgPSBpbnQob3V0cHV0LnNwbGl0bGluZXMoKVswXSkKICAgICAgICBleGNlcHQg +KE9TRXJyb3IsIFZhbHVlRXJyb3IsIEluZGV4RXJyb3IsIHN1YnByb2Nlc3MuQ2FsbGVkUHJvY2Vzc0Vycm9yKToKICAgICAgICAgICAgcGlkID0gMAogICAg +ICAgIGlmIHBpZCA+IDE6CiAgICAgICAgICAgIHNlbGYudHJhY2tfcHJvY2VzcyhudW1iZXIsICJ4dm5jIiwgT3duZWRQcm9jZXNzKHBpZCkpCgogICAgZGVm +IHN0YXJ0X2RidXMoc2VsZiwgbnVtYmVyLCBlbnZpcm9ubWVudCk6CiAgICAgICAgYnVzID0gc3VicHJvY2Vzcy5ydW4oWyJkYnVzLWxhdW5jaCIsICItLXNo +LXN5bnRheCJdLCBlbnY9ZW52aXJvbm1lbnQsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3RkaW49c3VicHJvY2Vzcy5ERVZOVUxMLCBzdGRvdXQ9 +c3VicHJvY2Vzcy5QSVBFLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0ZGVycj1zdWJwcm9jZXNzLkRFVk5VTEwsIHRleHQ9VHJ1ZSwgdGltZW91 +dD0xMCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjaGVjaz1UcnVlKS5zdGRvdXQKICAgICAgICBmb3IgbGluZSBpbiBidXMuc3BsaXRsaW5lcygp +OgogICAgICAgICAgICBpZiAiPSIgbm90IGluIGxpbmU6CiAgICAgICAgICAgICAgICBjb250aW51ZQogICAgICAgICAgICBrZXksIHZhbHVlID0gbGluZS5z +cGxpdCgiPSIsIDEpCiAgICAgICAgICAgIGVudmlyb25tZW50W2tleV0gPSB2YWx1ZS5zdHJpcCgpLnJzdHJpcCgiOyIpLnN0cmlwKCkuc3RyaXAoIiciKQog +ICAgICAgIHRyeToKICAgICAgICAgICAgZGJ1c19waWQgPSBpbnQoZW52aXJvbm1lbnQuZ2V0KCJEQlVTX1NFU1NJT05fQlVTX1BJRCIsICIwIikpCiAgICAg +ICAgZXhjZXB0IFZhbHVlRXJyb3I6CiAgICAgICAgICAgIGRidXNfcGlkID0gMAogICAgICAgIGlmIGRidXNfcGlkID4gMToKICAgICAgICAgICAgc2VsZi50 +cmFja19wcm9jZXNzKG51bWJlciwgImRidXMiLCBPd25lZFByb2Nlc3MoZGJ1c19waWQpKQoKICAgIGRlZiBzdGFydF9zZXNzaW9uX2NvbXBvbmVudHMoc2Vs +ZiwgbnVtYmVyLCBlbnZpcm9ubWVudCwgcnVudGltZSk6CiAgICAgICAgZm9yIGV4ZWN1dGFibGUsIGFyZ3MgaW4gWwogICAgICAgICAgICAoIm9wZW5ib3gi +LCBbXSksICgidGludDIiLCBbIi1jIiwgIi9ldGMvY211eC90aW50MnJjIl0pLAogICAgICAgICAgICAoInZuY2NvbmZpZyIsIFsiLW5vd2luIl0pLAogICAg +ICAgIF06CiAgICAgICAgICAgIGV4aXN0aW5nID0gc2VsZi5uYW1lZF9wcm9jZXNzZXMuZ2V0KG51bWJlciwge30pLmdldChleGVjdXRhYmxlKQogICAgICAg +ICAgICBpZiBleGlzdGluZyBpcyBub3QgTm9uZSBhbmQgc2VsZi5wcm9jZXNzX2FsaXZlKGV4aXN0aW5nKToKICAgICAgICAgICAgICAgIGNvbnRpbnVlCiAg +ICAgICAgICAgIHBhdGggPSBleGVjdXRhYmxlIGlmICIvIiBpbiBleGVjdXRhYmxlIGVsc2Ugc2h1dGlsLndoaWNoKGV4ZWN1dGFibGUpCiAgICAgICAgICAg +IGlmIG5vdCBwYXRoOgogICAgICAgICAgICAgICAgaWYgZXhlY3V0YWJsZSA9PSAidm5jY29uZmlnIjoKICAgICAgICAgICAgICAgICAgICBjb250aW51ZQog +ICAgICAgICAgICAgICAgcmFpc2UgUnVudGltZUVycm9yKGYibWlzc2luZyBkaXNwbGF5IGNvbXBvbmVudDoge2V4ZWN1dGFibGV9IikKICAgICAgICAgICAg +cGF0aCA9IHNlbGYuc2NvcGVkX2NvbXBvbmVudF9wYXRoKGV4ZWN1dGFibGUsIHBhdGgsIG51bWJlciwgcnVudGltZSkKICAgICAgICAgICAgcHJvY2VzcyA9 +IHN1YnByb2Nlc3MuUG9wZW4oW3BhdGhdICsgYXJncywgZW52PWVudmlyb25tZW50LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBz +dGRpbj1zdWJwcm9jZXNzLkRFVk5VTEwsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0ZG91dD1zdWJwcm9jZXNzLkRFVk5VTEws +IHN0ZGVycj1zdWJwcm9jZXNzLkRFVk5VTEwpCiAgICAgICAgICAgIHNlbGYudHJhY2tfcHJvY2VzcyhudW1iZXIsIGV4ZWN1dGFibGUsIHByb2Nlc3MpCgog +ICAgZGVmIHN0YXJ0X3dlYnNvY2tpZnkoc2VsZiwgbnVtYmVyLCBlbnZpcm9ubWVudCwgZXhlY3V0YWJsZSk6CiAgICAgICAgcGF0aCA9IGV4ZWN1dGFibGUg +aWYgIi8iIGluIGV4ZWN1dGFibGUgZWxzZSBzaHV0aWwud2hpY2goZXhlY3V0YWJsZSkKICAgICAgICBpZiBub3QgcGF0aDoKICAgICAgICAgICAgcmFpc2Ug +Tm9WTkNVbmF2YWlsYWJsZSgibWlzc2luZyBkaXNwbGF5IGNvbXBvbmVudDogd2Vic29ja2lmeSIpCiAgICAgICAgc2VsZi50ZXJtaW5hdGVfd2Vic29ja2lm +eShudW1iZXIpCiAgICAgICAgcmZiX3BvcnQsIG5vdm5jX3BvcnQgPSA1OTAwICsgbnVtYmVyLCA2OTAwICsgbnVtYmVyCiAgICAgICAgcHJvY2VzcyA9IHN1 +YnByb2Nlc3MuUG9wZW4oCiAgICAgICAgICAgIFtwYXRoLCAiLS13ZWIiLCAiL3Vzci9zaGFyZS9ub3ZuYyIsICItLWhlYXJ0YmVhdCIsICIzMCIsCiAgICAg +ICAgICAgICBmIjEyNy4wLjAuMTp7bm92bmNfcG9ydH0iLCBmIjEyNy4wLjAuMTp7cmZiX3BvcnR9Il0sCiAgICAgICAgICAgIGVudj1lbnZpcm9ubWVudCwg +c3RkaW49c3VicHJvY2Vzcy5ERVZOVUxMLAogICAgICAgICAgICBzdGRvdXQ9c3VicHJvY2Vzcy5ERVZOVUxMLCBzdGRlcnI9c3VicHJvY2Vzcy5ERVZOVUxM +KQogICAgICAgIHNlbGYud2Vic29ja2lmeV9wcm9jZXNzZXNbbnVtYmVyXSA9IHByb2Nlc3MKICAgICAgICBpZiBub3Qgc2VsZi53YWl0X2Zvcl9wb3J0KG5v +dm5jX3BvcnQsIHRpbWVvdXQ9MTApOgogICAgICAgICAgICBzZWxmLnRlcm1pbmF0ZV93ZWJzb2NraWZ5KG51bWJlcikKICAgICAgICAgICAgcmFpc2UgTm9W +TkNVbmF2YWlsYWJsZSgibm9WTkMgZGlkIG5vdCBiZWNvbWUgcmVhZHkiKQoKICAgIGRlZiBzdGFydF9jb21wb25lbnRzKHNlbGYsIG51bWJlciwgZW52aXJv +bm1lbnQsIHJ1bnRpbWUpOgogICAgICAgIGlmIHNlbGYuc2VydmljZV9yZWFkeShudW1iZXIpOgogICAgICAgICAgICByZXR1cm4KICAgICAgICByZmJfaXNf +cmVhZHkgPSByZmJfcmVhZHkobnVtYmVyKQogICAgICAgIHZuYyA9IHNodXRpbC53aGljaCgiWHZuYyIpIG9yIHNodXRpbC53aGljaCgiWHRpZ2Vydm5jIikK +ICAgICAgICB3ZWJzb2NraWZ5ID0gc2h1dGlsLndoaWNoKCJ3ZWJzb2NraWZ5IikKICAgICAgICBpZiBub3Qgd2Vic29ja2lmeToKICAgICAgICAgICAgcmFp +c2UgTm9WTkNVbmF2YWlsYWJsZSgiZGlzcGxheSBydW50aW1lIHVuYXZhaWxhYmxlIikKICAgICAgICBpZiBub3QgcmZiX2lzX3JlYWR5IGFuZCBub3Qgdm5j +OgogICAgICAgICAgICByYWlzZSBSdW50aW1lRXJyb3IoImRpc3BsYXkgcnVudGltZSB1bmF2YWlsYWJsZSIpCiAgICAgICAgaWYgcmZiX2lzX3JlYWR5IGFu +ZCBudW1iZXIgbm90IGluIHNlbGYubmFtZWRfcHJvY2Vzc2VzOgogICAgICAgICAgICBzZWxmLnJlY292ZXJfcHJvY2Vzc2VzKG51bWJlcikKICAgICAgICBp +ZiBub3QgcmZiX2lzX3JlYWR5OgogICAgICAgICAgICBzZWxmLnRlcm1pbmF0ZShudW1iZXIpCiAgICAgICAgICAgIHJmYl9wb3J0ID0gNTkwMCArIG51bWJl +cgogICAgICAgICAgICBzZWxmLnByb2Nlc3Nlc1tudW1iZXJdID0gW10KICAgICAgICAgICAgc2VsZi5uYW1lZF9wcm9jZXNzZXNbbnVtYmVyXSA9IHt9CiAg +ICAgICAgICAgIHNlbGYudHJhY2tfcHJvY2VzcyhudW1iZXIsICJ4dm5jIiwgc3VicHJvY2Vzcy5Qb3BlbigKICAgICAgICAgICAgICAgIFt2bmMsIGYiOntu +dW1iZXJ9IiwgIi1nZW9tZXRyeSIsIG9zLmVudmlyb24uZ2V0KCJDTVVYX1ZOQ19HRU9NRVRSWSIsICIxNDQweDkwMCIpLAogICAgICAgICAgICAgICAgICIt +ZGVwdGgiLCAiMjQiLCAiLXJmYnBvcnQiLCBzdHIocmZiX3BvcnQpLCAiLWxvY2FsaG9zdCIsCiAgICAgICAgICAgICAgICAgIi1TZWN1cml0eVR5cGVzIiwg +Ik5vbmUiLCAiLUFsd2F5c1NoYXJlZCJdLAogICAgICAgICAgICAgICAgZW52PWVudmlyb25tZW50LCBzdGRpbj1zdWJwcm9jZXNzLkRFVk5VTEwsCiAgICAg +ICAgICAgICAgICBzdGRvdXQ9c3VicHJvY2Vzcy5ERVZOVUxMLCBzdGRlcnI9c3VicHJvY2Vzcy5ERVZOVUxMKSkKICAgICAgICAgICAgaWYgbm90IHNlbGYu +d2FpdF9mb3JfcG9ydChyZmJfcG9ydCk6CiAgICAgICAgICAgICAgICByYWlzZSBSdW50aW1lRXJyb3IoIlh2bmMgZGlkIG5vdCBiZWNvbWUgcmVhZHkiKQog +ICAgICAgICAgICBzZWxmLnN0YXJ0X2RidXMobnVtYmVyLCBlbnZpcm9ubWVudCkKICAgICAgICAgICAgc2VsZi5zdGFydF9zZXNzaW9uX2NvbXBvbmVudHMo +bnVtYmVyLCBlbnZpcm9ubWVudCwgcnVudGltZSkKICAgICAgICBlbGlmIG51bWJlciBpbiBzZWxmLm5hbWVkX3Byb2Nlc3NlczoKICAgICAgICAgICAgbmFt +ZWQgPSBzZWxmLm5hbWVkX3Byb2Nlc3Nlc1tudW1iZXJdCiAgICAgICAgICAgIGRidXMgPSBuYW1lZC5nZXQoImRidXMiKQogICAgICAgICAgICBpZiBkYnVz +IGlzIE5vbmUgb3Igbm90IHNlbGYucHJvY2Vzc19hbGl2ZShkYnVzKToKICAgICAgICAgICAgICAgIGZvciBuYW1lIGluICgib3BlbmJveCIsICJ0aW50MiIs +ICJ2bmNjb25maWciKToKICAgICAgICAgICAgICAgICAgICBzZWxmLnJlbW92ZV9wcm9jZXNzKG51bWJlciwgbmFtZSkKICAgICAgICAgICAgICAgIHNlbGYu +c3RhcnRfZGJ1cyhudW1iZXIsIGVudmlyb25tZW50KQogICAgICAgICAgICBzZWxmLnN0YXJ0X3Nlc3Npb25fY29tcG9uZW50cyhudW1iZXIsIGVudmlyb25t +ZW50LCBydW50aW1lKQogICAgICAgIHdlYnNvY2tpZnlfcHJvY2VzcyA9IHNlbGYud2Vic29ja2lmeV9wcm9jZXNzZXMuZ2V0KG51bWJlcikKICAgICAgICBp +ZiAod2Vic29ja2lmeV9wcm9jZXNzIGlzIE5vbmUgb3Igbm90IHNlbGYucHJvY2Vzc19hbGl2ZSh3ZWJzb2NraWZ5X3Byb2Nlc3MpCiAgICAgICAgICAgICAg +ICBvciBub3Qgbm92bmNfcmVhZHkobnVtYmVyKSk6CiAgICAgICAgICAgIHNlbGYuc3RhcnRfd2Vic29ja2lmeShudW1iZXIsIGVudmlyb25tZW50LCB3ZWJz +b2NraWZ5KQogICAgICAgIChydW50aW1lIC8gImVudiIpLndyaXRlX3RleHQoZiJleHBvcnQgRElTUExBWT06e251bWJlcn1cbiIpCgogICAgZGVmIHN1cGVy +dmlzZShzZWxmLCBudW1iZXIsIGZpbmlzaGVkKToKICAgICAgICBydW50aW1lID0gc2VsZi5ydW50aW1lIC8gc3RyKG51bWJlcikKICAgICAgICBydW50aW1l +Lm1rZGlyKG1vZGU9MG83MDAsIHBhcmVudHM9VHJ1ZSwgZXhpc3Rfb2s9VHJ1ZSkKICAgICAgICBlbnZpcm9ubWVudCA9IGRpY3Qob3MuZW52aXJvbiwgRElT +UExBWT1mIjp7bnVtYmVyfSIsIENNVVhfREVTS1RPUF9SVU5USU1FX0RJUj1zdHIocnVudGltZSkpCiAgICAgICAgZW52aXJvbm1lbnRbIkNIUk9NRV9DT05G +SUdfSE9NRSJdID0gc3RyKHJ1bnRpbWUgLyAiY2hyb21lLWNvbmZpZyIpCiAgICAgICAgZW52aXJvbm1lbnRbIlhER19DT05GSUdfSE9NRSJdID0gc3RyKHJ1 +bnRpbWUgLyAieGRnLWNvbmZpZyIpCiAgICAgICAgZm9yIGtleSBpbiAoIk5PVElGWV9TT0NLRVQiLCAiREJVU19TRVNTSU9OX0JVU19BRERSRVNTIiwgIkRC +VVNfU0VTU0lPTl9CVVNfUElEIiwKICAgICAgICAgICAgICAgICAgICAiQVRfU1BJX0JVU19BRERSRVNTIiwgIkFUX1NQSV9CVVMiKToKICAgICAgICAgICAg +ZW52aXJvbm1lbnQucG9wKGtleSwgTm9uZSkKICAgICAgICB3aGlsZSBub3Qgc2VsZi5zaHV0ZG93bi5pc19zZXQoKToKICAgICAgICAgICAgdHJ5OgogICAg +ICAgICAgICAgICAgIyBUaGUgc3VwZXJ2aXNvciBpcyBhIGhlYWx0aCBjaGVjaywgbm90IGEgdGltZXItZHJpdmVuIHJlc3RhcnQuCiAgICAgICAgICAgICAg +ICAjIFByZXNlcnZlIGFuIGFjdGl2ZSBSRkIvbm9WTkMgc2Vzc2lvbiB1bnRpbCBpdHMgb3duZWQgZW5kcG9pbnRzIGZhaWwuCiAgICAgICAgICAgICAgICBp +ZiBub3Qgc2VsZi5zZXJ2aWNlX3JlYWR5KG51bWJlcik6CiAgICAgICAgICAgICAgICAgICAgc2VsZi5zdGFydF9jb21wb25lbnRzKG51bWJlciwgZW52aXJv +bm1lbnQsIHJ1bnRpbWUpCiAgICAgICAgICAgICAgICBzdGF0ZSA9ICJydW5uaW5nIiBpZiBzZWxmLnNlcnZpY2VfcmVhZHkobnVtYmVyKSBlbHNlICJ1bmF2 +YWlsYWJsZSIKICAgICAgICAgICAgZXhjZXB0IE5vVk5DVW5hdmFpbGFibGU6CiAgICAgICAgICAgICAgICBzZWxmLnRlcm1pbmF0ZV93ZWJzb2NraWZ5KG51 +bWJlcikKICAgICAgICAgICAgICAgIHN0YXRlID0gInVuYXZhaWxhYmxlIgogICAgICAgICAgICBleGNlcHQgKE9TRXJyb3IsIFJ1bnRpbWVFcnJvciwgc3Vi +cHJvY2Vzcy5TdWJwcm9jZXNzRXJyb3IpOgogICAgICAgICAgICAgICAgc2VsZi50ZXJtaW5hdGUobnVtYmVyKQogICAgICAgICAgICAgICAgc3RhdGUgPSAi +dW5hdmFpbGFibGUiCiAgICAgICAgICAgIHdpdGggc2VsZi5sb2NrOgogICAgICAgICAgICAgICAgc2VsZi5zdGF0ZXNbbnVtYmVyXSA9IHN0YXRlCiAgICAg +ICAgICAgIGZpbmlzaGVkLnNldCgpCiAgICAgICAgICAgIHNlbGYuc2h1dGRvd24ud2FpdCgzMCkKICAgICAgICBzZWxmLnRlcm1pbmF0ZShudW1iZXIpCgog +ICAgZGVmIHNuYXBzaG90KHNlbGYpOgogICAgICAgIHdpdGggc2VsZi5sb2NrOgogICAgICAgICAgICByZXR1cm4geyJ2ZXJzaW9uIjogMSwgImNhbkNyZWF0 +ZSI6IGxlbihzZWxmLmpvYnMpIDwgTUFYX0RJU1BMQVlTIC0gMSwKICAgICAgICAgICAgICAgICAgICAiZGlzcGxheXMiOiBbZGVzY3JpcHRvcigxLCAicnVu +bmluZyIgaWYgcmVhZHkoMSkgZWxzZSAidW5hdmFpbGFibGUiKV0KICAgICAgICAgICAgICAgICAgICArIFtkZXNjcmlwdG9yKG4sIHNlbGYuc3RhdGVzLmdl +dChuLCAibGF1bmNoaW5nIikpIGZvciBuIGluIHNlbGYuY2F0YWxvZy5udW1iZXJzKCldfQoKICAgIGRlZiBoYW5kbGUoc2VsZiwgcmVxdWVzdCk6CiAgICAg +ICAgaWYgcmVxdWVzdC5nZXQoImFjdGlvbiIpID09ICJsaXN0IjoKICAgICAgICAgICAgcmV0dXJuIHNlbGYuc25hcHNob3QoKQogICAgICAgIGlmIHJlcXVl +c3QuZ2V0KCJhY3Rpb24iKSAhPSAiY3JlYXRlIjoKICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJvcigidW5zdXBwb3J0ZWQgZGlzcGxheSBhY3Rpb24iKQog +ICAgICAgIG51bWJlciA9IHNlbGYuY2F0YWxvZy5hbGxvY2F0ZShyZXF1ZXN0LmdldCgicmVxdWVzdCIpKQogICAgICAgIHNlbGYuc3RhcnQobnVtYmVyKS53 +YWl0KDU1KQogICAgICAgIHJlc3VsdCA9IHNlbGYuc25hcHNob3QoKQogICAgICAgIHJlc3VsdFsiY3JlYXRlZCJdID0gZiJkaXNwbGF5OntudW1iZXJ9Igog +ICAgICAgIGlmIHNlbGYuc3RhdGVzLmdldChudW1iZXIpICE9ICJydW5uaW5nIjoKICAgICAgICAgICAgcmVzdWx0WyJlcnJvciJdID0gImRpc3BsYXlfc3Rh +cnRfZmFpbGVkIgogICAgICAgIHJldHVybiByZXN1bHQKCgpkZWYgc2VydmUocnVudGltZSk6CiAgICBydW50aW1lID0gUGF0aChydW50aW1lKQogICAgcnVu +dGltZS5ta2Rpcihtb2RlPTBvNzAwLCBwYXJlbnRzPVRydWUsIGV4aXN0X29rPVRydWUpCiAgICB3aXRoIChydW50aW1lIC8gImRpc3BsYXktY29udHJvbC5s +b2NrIikub3BlbigidyIpIGFzIGxvY2s6CiAgICAgICAgdHJ5OgogICAgICAgICAgICBmY250bC5mbG9jayhsb2NrLCBmY250bC5MT0NLX0VYIHwgZmNudGwu +TE9DS19OQikKICAgICAgICBleGNlcHQgQmxvY2tpbmdJT0Vycm9yOgogICAgICAgICAgICByZXR1cm4KICAgICAgICBpZiBub3QgYWxsKHNodXRpbC53aGlj +aChuYW1lKSBmb3IgbmFtZSBpbiAoIndlYnNvY2tpZnkiLCAib3BlbmJveCIsICJkYnVzLWxhdW5jaCIpKToKICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJv +cigiZGlzcGxheSBydW50aW1lIHVuYXZhaWxhYmxlIikKICAgICAgICBpZiBub3QgKHNodXRpbC53aGljaCgiWHZuYyIpIG9yIHNodXRpbC53aGljaCgiWHRp +Z2Vydm5jIikpOgogICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJkaXNwbGF5IHJ1bnRpbWUgdW5hdmFpbGFibGUiKQogICAgICAgIHNlcnZpY2UgPSBE +aXNwbGF5U2VydmljZShEaXNwbGF5Q2F0YWxvZyhQYXRoLmhvbWUoKSAvICIuY211eCIgLyAiZGlzcGxheXMiKSwgcnVudGltZSAvICJkaXNwbGF5cyIpCiAg +ICAgICAgZm9yIG51bWJlciBpbiBzZXJ2aWNlLmNhdGFsb2cubnVtYmVycygpOgogICAgICAgICAgICBzZXJ2aWNlLnN0YXJ0KG51bWJlcikKICAgICAgICBw +YXRoID0gcnVudGltZSAvICJkaXNwbGF5LWNvbnRyb2wuc29jayIKICAgICAgICBpZiBwYXRoLmV4aXN0cygpOgogICAgICAgICAgICBwYXRoLnVubGluaygp +CgogICAgICAgIGNsYXNzIEhhbmRsZXIoc29ja2V0c2VydmVyLlN0cmVhbVJlcXVlc3RIYW5kbGVyKToKICAgICAgICAgICAgZGVmIGhhbmRsZShzZWxmKToK +ICAgICAgICAgICAgICAgIHNlbGYuY29ubmVjdGlvbi5zZXR0aW1lb3V0KDYwKQogICAgICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgIHJh +dyA9IHNlbGYucmZpbGUucmVhZGxpbmUoNDA5NykKICAgICAgICAgICAgICAgICAgICBpZiBsZW4ocmF3KSA+IDQwOTY6CiAgICAgICAgICAgICAgICAgICAg +ICAgIHJhaXNlIFZhbHVlRXJyb3IoImRpc3BsYXkgcmVxdWVzdCB0b28gbGFyZ2UiKQogICAgICAgICAgICAgICAgICAgIHJlc3VsdCA9IHNlcnZpY2UuaGFu +ZGxlKGpzb24ubG9hZHMocmF3KSkKICAgICAgICAgICAgICAgIGV4Y2VwdCAoVmFsdWVFcnJvciwgS2V5RXJyb3IsIFR5cGVFcnJvciwgT1NFcnJvcik6CiAg +ICAgICAgICAgICAgICAgICAgcmVzdWx0ID0geyJlcnJvciI6ICJkaXNwbGF5X3JlcXVlc3RfZmFpbGVkIn0KICAgICAgICAgICAgICAgIHRyeToKICAgICAg +ICAgICAgICAgICAgICBzZWxmLndmaWxlLndyaXRlKGpzb24uZHVtcHMocmVzdWx0KS5lbmNvZGUoKSArIGIiXG4iKQogICAgICAgICAgICAgICAgZXhjZXB0 +IE9TRXJyb3I6CiAgICAgICAgICAgICAgICAgICAgcGFzcyAgIyBUaGUgcGVyc2lzdGVkIHJlY2VpcHQgcmVtYWlucyByZXBsYXlhYmxlIGFmdGVyIGRpc2Nv +bm5lY3QuCgogICAgICAgIGNsYXNzIFNlcnZlcihzb2NrZXRzZXJ2ZXIuVGhyZWFkaW5nVW5peFN0cmVhbVNlcnZlcik6CiAgICAgICAgICAgIGRhZW1vbl90 +aHJlYWRzID0gVHJ1ZQoKICAgICAgICBwcmV2aW91c191bWFzayA9IG9zLnVtYXNrKDBvMDc3KQogICAgICAgIHRyeToKICAgICAgICAgICAgd2l0aCBTZXJ2 +ZXIoc3RyKHBhdGgpLCBIYW5kbGVyKSBhcyBzZXJ2ZXI6CiAgICAgICAgICAgICAgICBvcy51bWFzayhwcmV2aW91c191bWFzaykKICAgICAgICAgICAgICAg +IHNlcnZlci5zZXJ2ZV9mb3JldmVyKCkKICAgICAgICBmaW5hbGx5OgogICAgICAgICAgICBvcy51bWFzayhwcmV2aW91c191bWFzaykKICAgICAgICAgICAg +c2VydmljZS5zaHV0ZG93bi5zZXQoKQogICAgICAgICAgICBwYXRoLnVubGluayhtaXNzaW5nX29rPVRydWUpCgoKZGVmIG1haW4oKToKICAgIHBhcnNlciA9 +IGFyZ3BhcnNlLkFyZ3VtZW50UGFyc2VyKGRlc2NyaXB0aW9uPV9fZG9jX18pCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50KCJhY3Rpb24iLCBjaG9pY2VzPVsi +bGlzdCIsICJjcmVhdGUiLCAic2VydmUiXSkKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoIi0tcmVxdWVzdC1pZCIpCiAgICBhcmdzID0gcGFyc2VyLnBhcnNl +X2FyZ3MoKQogICAgcnVudGltZSA9IG9zLmVudmlyb24uZ2V0KCJDTVVYX0RFU0tUT1BfUlVOVElNRV9ESVIiLCAiL3J1bi9jbXV4LWRlc2t0b3AiKQogICAg +aWYgYXJncy5hY3Rpb24gPT0gInNlcnZlIjoKICAgICAgICBzZXJ2ZShydW50aW1lKQogICAgICAgIHJldHVybiAwCiAgICBpZiBhcmdzLmFjdGlvbiA9PSAi +Y3JlYXRlIiBhbmQgbm90IGFyZ3MucmVxdWVzdF9pZDoKICAgICAgICBwYXJzZXIuZXJyb3IoImNyZWF0ZSByZXF1aXJlcyAtLXJlcXVlc3QtaWQgKHJldGFp +biBpdCB3aGVuIHJldHJ5aW5nKSIpCiAgICB0cnk6CiAgICAgICAgd2l0aCBzb2NrZXQuc29ja2V0KHNvY2tldC5BRl9VTklYLCBzb2NrZXQuU09DS19TVFJF +QU0pIGFzIGNsaWVudDoKICAgICAgICAgICAgY2xpZW50LnNldHRpbWVvdXQoNjApCiAgICAgICAgICAgIGNsaWVudC5jb25uZWN0KHN0cihQYXRoKHJ1bnRp +bWUpIC8gImRpc3BsYXktY29udHJvbC5zb2NrIikpCiAgICAgICAgICAgIGNsaWVudC5zZW5kYWxsKGpzb24uZHVtcHMoeyJhY3Rpb24iOiBhcmdzLmFjdGlv +biwgInJlcXVlc3QiOiBhcmdzLnJlcXVlc3RfaWR9KS5lbmNvZGUoKSArIGIiXG4iKQogICAgICAgICAgICB3aXRoIGNsaWVudC5tYWtlZmlsZSgicmIiKSBh +cyBzdHJlYW06CiAgICAgICAgICAgICAgICByZXN1bHQgPSBqc29uLmxvYWRzKHN0cmVhbS5yZWFkbGluZSg2NTUzNikpCiAgICBleGNlcHQgKE9TRXJyb3Is +IFZhbHVlRXJyb3IpOgogICAgICAgIHJlc3VsdCA9IHsiZXJyb3IiOiAiZGlzcGxheV9ydW50aW1lX3VuYXZhaWxhYmxlIn0KICAgIHByaW50KGpzb24uZHVt +cHMocmVzdWx0KSkKICAgIHJldHVybiAxIGlmICJlcnJvciIgaW4gcmVzdWx0IGVsc2UgMAoKCmlmIF9fbmFtZV9fID09ICJfX21haW5fXyI6CiAgICByYWlz +ZSBTeXN0ZW1FeGl0KG1haW4oKSkK """ private static let path = "$HOME/.cmux/cmux-display" diff --git a/Sources/Cloud/PortForward/CloudBrowserAccessState.swift b/Sources/Cloud/PortForward/CloudBrowserAccessState.swift index f1554c8d5741..d1ffe141a77c 100644 --- a/Sources/Cloud/PortForward/CloudBrowserAccessState.swift +++ b/Sources/Cloud/PortForward/CloudBrowserAccessState.swift @@ -290,7 +290,7 @@ final class CloudBrowserAccessState { if model?.usesBrowserProxy == true { guard ["http", "https"].contains(url.scheme?.lowercased() ?? ""), url.host?.lowercased() == remoteURL.host?.lowercased() else { return false } - if let resourceID, let expectedPort = Self.displayPort(for: resourceID) { + if let resourceID, let expectedPort = Self.resourcePort(for: resourceID) { let requestedPort = url.port ?? (url.scheme?.lowercased() == "https" ? 443 : 80) return requestedPort == expectedPort } @@ -330,10 +330,15 @@ final class CloudBrowserAccessState { && (a.port ?? (a.scheme == "https" ? 443 : 80)) == (b.port ?? (b.scheme == "https" ? 443 : 80)) } - private static func displayPort(for resource: SurfaceResourceID) -> Int? { - guard resource.kind == .display, - let number = Int(resource.key.split(separator: ":").last ?? ""), - (1...16).contains(number) else { return nil } - return 6900 + number + private static func resourcePort(for resource: SurfaceResourceID) -> Int? { + if resource.kind == .display, + let number = Int(resource.key.split(separator: ":").last ?? ""), + (1...16).contains(number) { + return 6900 + number + } + if resource.kind == .browser, resource.key.hasPrefix("port:") { + return Int(resource.key.dropFirst("port:".count)) + } + return nil } } diff --git a/Sources/Panels/BrowserPanel.swift b/Sources/Panels/BrowserPanel.swift index 6e81b0356088..e5190fddfde4 100644 --- a/Sources/Panels/BrowserPanel.swift +++ b/Sources/Panels/BrowserPanel.swift @@ -3238,7 +3238,16 @@ final class BrowserPanel: Panel, ObservableObject { instanceID: boundWebViewInstanceID, navigationID: navigation.map { ObjectIdentifier($0) } ) - self.cloudAccess.didCommit(url: webView.url, navigationID: navigation.map(ObjectIdentifier.init)) + let leavingCloudRoute = self.retainsCloudResourceForDuplication + && webView.url.map { + ["http", "https"].contains($0.scheme?.lowercased() ?? "") + && !self.cloudAccess.owns($0) + } == true + if leavingCloudRoute { + self.leaveCloudResourceForLocalNavigation() + } else { + self.cloudAccess.didCommit(url: webView.url, navigationID: navigation.map(ObjectIdentifier.init)) + } // An about:blank placeholder leaves the restore-stall detector armed. if !Self.isAboutBlankURL(webView.url) { self.hasCommittedDocumentSinceWebViewReplacement = true diff --git a/tests/fixtures/cmux-display b/tests/fixtures/cmux-display index 2aea81296256..fb57431ebb5c 100755 --- a/tests/fixtures/cmux-display +++ b/tests/fixtures/cmux-display @@ -224,7 +224,8 @@ class DisplayService: def components_alive(self, number): named = self.named_processes.get(number) if named is not None: - return bool(named) and all(self.process_alive(process) for process in named.values()) + return bool(named) and all(self.process_alive(process) for process in named.values()) \ + and ("xvnc" in named or rfb_ready(number)) processes = self.processes.get(number, []) return bool(processes) and all(self.process_alive(process) for process in processes) @@ -235,8 +236,47 @@ class DisplayService: return websockify is None or self.process_alive(websockify) def track_process(self, number, name, process): + named = self.named_processes.setdefault(number, {}) + previous = named.get(name) + if previous is not None: + self.processes[number] = [ + item for item in self.processes.get(number, []) if item is not previous + ] self.processes.setdefault(number, []).append(process) - self.named_processes.setdefault(number, {})[name] = process + named[name] = process + + def remove_process(self, number, name): + process = self.named_processes.setdefault(number, {}).pop(name, None) + if process is None: + return + self.processes[number] = [item for item in self.processes.get(number, []) if item is not process] + try: + process.terminate() + except OSError: + pass + + def recover_processes(self, number): + self.processes[number] = [] + self.named_processes[number] = {} + uid = str(os.getuid()) + for name in ("openbox", "tint2", "vncconfig"): + marker = f"cmux-display-{number}-{name}" + try: + output = subprocess.check_output(["pgrep", "-u", uid, "-x", marker], text=True) + pid = int(output.splitlines()[0]) + except (OSError, ValueError, IndexError, subprocess.CalledProcessError): + continue + self.track_process(number, name, OwnedProcess(pid)) + try: + output = subprocess.check_output( + ["pgrep", "-u", uid, "-f", rf"(?:Xvnc|Xtigervnc).*:{number}(?:\s|$)"], + text=True + ) + pid = int(output.splitlines()[0]) + except (OSError, ValueError, IndexError, subprocess.CalledProcessError): + pid = 0 + if pid > 1: + self.track_process(number, "xvnc", OwnedProcess(pid)) def start_dbus(self, number, environment): bus = subprocess.run(["dbus-launch", "--sh-syntax"], env=environment, @@ -300,6 +340,8 @@ class DisplayService: raise NoVNCUnavailable("display runtime unavailable") if not rfb_is_ready and not vnc: raise RuntimeError("display runtime unavailable") + if rfb_is_ready and number not in self.named_processes: + self.recover_processes(number) if not rfb_is_ready: self.terminate(number) rfb_port = 5900 + number @@ -320,12 +362,7 @@ class DisplayService: dbus = named.get("dbus") if dbus is None or not self.process_alive(dbus): for name in ("openbox", "tint2", "vncconfig"): - process = named.pop(name, None) - if process is not None: - try: - process.terminate() - except OSError: - pass + self.remove_process(number, name) self.start_dbus(number, environment) self.start_session_components(number, environment, runtime) websockify_process = self.websockify_processes.get(number) From f96bad4d87a825f919d6bb2b08111bba8529a716 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 15:51:59 -0700 Subject: [PATCH 44/64] test: recover scoped display process commands --- tests/test_cloud_display_catalog.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_cloud_display_catalog.py b/tests/test_cloud_display_catalog.py index 33c49c8e32d6..33d9d7724d62 100644 --- a/tests/test_cloud_display_catalog.py +++ b/tests/test_cloud_display_catalog.py @@ -204,6 +204,18 @@ def poll(self): self.assertIs(service.named_processes[number]["openbox"], replacement) service.shutdown.set() + def test_recovery_adopts_display_scoped_processes_by_full_command(self): + service = display.DisplayService(self.catalog(), self.root / "runtime") + + def pgrep(command, **_options): + if "cmux-display-2-openbox" in command[-1]: + return "12345\n" + raise display.subprocess.CalledProcessError(1, command) + + with mock.patch.object(display.subprocess, "check_output", side_effect=pgrep): + service.recover_processes(2) + self.assertIn("openbox", service.named_processes[2]) + def test_additional_desktop_clients_use_display_scoped_process_names(self): service = display.DisplayService(self.catalog(), self.root / "runtime") source = self.root / "openbox" From e2fb4e3465efac18b4f670d1693205f9edc99f4f Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 15:52:09 -0700 Subject: [PATCH 45/64] fix: recover scoped display processes by command --- Sources/Cloud/CloudGuestDisplayScript.swift | 248 ++++++++++---------- tests/fixtures/cmux-display | 3 +- 2 files changed, 126 insertions(+), 125 deletions(-) diff --git a/Sources/Cloud/CloudGuestDisplayScript.swift b/Sources/Cloud/CloudGuestDisplayScript.swift index f1ed48b86f28..6cdb02f4b02a 100644 --- a/Sources/Cloud/CloudGuestDisplayScript.swift +++ b/Sources/Cloud/CloudGuestDisplayScript.swift @@ -107,130 +107,130 @@ ZW0gaW4gc2VsZi5wcm9jZXNzZXMuZ2V0KG51bWJlciwgW10pIGlmIGl0ZW0gaXMgbm90IHByb2Nlc3Nd ZXNzLnRlcm1pbmF0ZSgpCiAgICAgICAgZXhjZXB0IE9TRXJyb3I6CiAgICAgICAgICAgIHBhc3MKCiAgICBkZWYgcmVjb3Zlcl9wcm9jZXNzZXMoc2VsZiwg bnVtYmVyKToKICAgICAgICBzZWxmLnByb2Nlc3Nlc1tudW1iZXJdID0gW10KICAgICAgICBzZWxmLm5hbWVkX3Byb2Nlc3Nlc1tudW1iZXJdID0ge30KICAg ICAgICB1aWQgPSBzdHIob3MuZ2V0dWlkKCkpCiAgICAgICAgZm9yIG5hbWUgaW4gKCJvcGVuYm94IiwgInRpbnQyIiwgInZuY2NvbmZpZyIpOgogICAgICAg -ICAgICBtYXJrZXIgPSBmImNtdXgtZGlzcGxheS17bnVtYmVyfS17bmFtZX0iCiAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgIG91dHB1dCA9IHN1 -YnByb2Nlc3MuY2hlY2tfb3V0cHV0KFsicGdyZXAiLCAiLXUiLCB1aWQsICIteCIsIG1hcmtlcl0sIHRleHQ9VHJ1ZSkKICAgICAgICAgICAgICAgIHBpZCA9 -IGludChvdXRwdXQuc3BsaXRsaW5lcygpWzBdKQogICAgICAgICAgICBleGNlcHQgKE9TRXJyb3IsIFZhbHVlRXJyb3IsIEluZGV4RXJyb3IsIHN1YnByb2Nl -c3MuQ2FsbGVkUHJvY2Vzc0Vycm9yKToKICAgICAgICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgICAgIHNlbGYudHJhY2tfcHJvY2VzcyhudW1iZXIsIG5h -bWUsIE93bmVkUHJvY2VzcyhwaWQpKQogICAgICAgIHRyeToKICAgICAgICAgICAgb3V0cHV0ID0gc3VicHJvY2Vzcy5jaGVja19vdXRwdXQoCiAgICAgICAg -ICAgICAgICBbInBncmVwIiwgIi11IiwgdWlkLCAiLWYiLCByZiIoPzpYdm5jfFh0aWdlcnZuYykuKjp7bnVtYmVyfSg/OlxzfCQpIl0sCiAgICAgICAgICAg -ICAgICB0ZXh0PVRydWUKICAgICAgICAgICAgKQogICAgICAgICAgICBwaWQgPSBpbnQob3V0cHV0LnNwbGl0bGluZXMoKVswXSkKICAgICAgICBleGNlcHQg -KE9TRXJyb3IsIFZhbHVlRXJyb3IsIEluZGV4RXJyb3IsIHN1YnByb2Nlc3MuQ2FsbGVkUHJvY2Vzc0Vycm9yKToKICAgICAgICAgICAgcGlkID0gMAogICAg -ICAgIGlmIHBpZCA+IDE6CiAgICAgICAgICAgIHNlbGYudHJhY2tfcHJvY2VzcyhudW1iZXIsICJ4dm5jIiwgT3duZWRQcm9jZXNzKHBpZCkpCgogICAgZGVm -IHN0YXJ0X2RidXMoc2VsZiwgbnVtYmVyLCBlbnZpcm9ubWVudCk6CiAgICAgICAgYnVzID0gc3VicHJvY2Vzcy5ydW4oWyJkYnVzLWxhdW5jaCIsICItLXNo -LXN5bnRheCJdLCBlbnY9ZW52aXJvbm1lbnQsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3RkaW49c3VicHJvY2Vzcy5ERVZOVUxMLCBzdGRvdXQ9 -c3VicHJvY2Vzcy5QSVBFLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0ZGVycj1zdWJwcm9jZXNzLkRFVk5VTEwsIHRleHQ9VHJ1ZSwgdGltZW91 -dD0xMCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjaGVjaz1UcnVlKS5zdGRvdXQKICAgICAgICBmb3IgbGluZSBpbiBidXMuc3BsaXRsaW5lcygp -OgogICAgICAgICAgICBpZiAiPSIgbm90IGluIGxpbmU6CiAgICAgICAgICAgICAgICBjb250aW51ZQogICAgICAgICAgICBrZXksIHZhbHVlID0gbGluZS5z -cGxpdCgiPSIsIDEpCiAgICAgICAgICAgIGVudmlyb25tZW50W2tleV0gPSB2YWx1ZS5zdHJpcCgpLnJzdHJpcCgiOyIpLnN0cmlwKCkuc3RyaXAoIiciKQog -ICAgICAgIHRyeToKICAgICAgICAgICAgZGJ1c19waWQgPSBpbnQoZW52aXJvbm1lbnQuZ2V0KCJEQlVTX1NFU1NJT05fQlVTX1BJRCIsICIwIikpCiAgICAg -ICAgZXhjZXB0IFZhbHVlRXJyb3I6CiAgICAgICAgICAgIGRidXNfcGlkID0gMAogICAgICAgIGlmIGRidXNfcGlkID4gMToKICAgICAgICAgICAgc2VsZi50 -cmFja19wcm9jZXNzKG51bWJlciwgImRidXMiLCBPd25lZFByb2Nlc3MoZGJ1c19waWQpKQoKICAgIGRlZiBzdGFydF9zZXNzaW9uX2NvbXBvbmVudHMoc2Vs -ZiwgbnVtYmVyLCBlbnZpcm9ubWVudCwgcnVudGltZSk6CiAgICAgICAgZm9yIGV4ZWN1dGFibGUsIGFyZ3MgaW4gWwogICAgICAgICAgICAoIm9wZW5ib3gi -LCBbXSksICgidGludDIiLCBbIi1jIiwgIi9ldGMvY211eC90aW50MnJjIl0pLAogICAgICAgICAgICAoInZuY2NvbmZpZyIsIFsiLW5vd2luIl0pLAogICAg -ICAgIF06CiAgICAgICAgICAgIGV4aXN0aW5nID0gc2VsZi5uYW1lZF9wcm9jZXNzZXMuZ2V0KG51bWJlciwge30pLmdldChleGVjdXRhYmxlKQogICAgICAg -ICAgICBpZiBleGlzdGluZyBpcyBub3QgTm9uZSBhbmQgc2VsZi5wcm9jZXNzX2FsaXZlKGV4aXN0aW5nKToKICAgICAgICAgICAgICAgIGNvbnRpbnVlCiAg -ICAgICAgICAgIHBhdGggPSBleGVjdXRhYmxlIGlmICIvIiBpbiBleGVjdXRhYmxlIGVsc2Ugc2h1dGlsLndoaWNoKGV4ZWN1dGFibGUpCiAgICAgICAgICAg -IGlmIG5vdCBwYXRoOgogICAgICAgICAgICAgICAgaWYgZXhlY3V0YWJsZSA9PSAidm5jY29uZmlnIjoKICAgICAgICAgICAgICAgICAgICBjb250aW51ZQog -ICAgICAgICAgICAgICAgcmFpc2UgUnVudGltZUVycm9yKGYibWlzc2luZyBkaXNwbGF5IGNvbXBvbmVudDoge2V4ZWN1dGFibGV9IikKICAgICAgICAgICAg -cGF0aCA9IHNlbGYuc2NvcGVkX2NvbXBvbmVudF9wYXRoKGV4ZWN1dGFibGUsIHBhdGgsIG51bWJlciwgcnVudGltZSkKICAgICAgICAgICAgcHJvY2VzcyA9 -IHN1YnByb2Nlc3MuUG9wZW4oW3BhdGhdICsgYXJncywgZW52PWVudmlyb25tZW50LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBz -dGRpbj1zdWJwcm9jZXNzLkRFVk5VTEwsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0ZG91dD1zdWJwcm9jZXNzLkRFVk5VTEws -IHN0ZGVycj1zdWJwcm9jZXNzLkRFVk5VTEwpCiAgICAgICAgICAgIHNlbGYudHJhY2tfcHJvY2VzcyhudW1iZXIsIGV4ZWN1dGFibGUsIHByb2Nlc3MpCgog -ICAgZGVmIHN0YXJ0X3dlYnNvY2tpZnkoc2VsZiwgbnVtYmVyLCBlbnZpcm9ubWVudCwgZXhlY3V0YWJsZSk6CiAgICAgICAgcGF0aCA9IGV4ZWN1dGFibGUg -aWYgIi8iIGluIGV4ZWN1dGFibGUgZWxzZSBzaHV0aWwud2hpY2goZXhlY3V0YWJsZSkKICAgICAgICBpZiBub3QgcGF0aDoKICAgICAgICAgICAgcmFpc2Ug -Tm9WTkNVbmF2YWlsYWJsZSgibWlzc2luZyBkaXNwbGF5IGNvbXBvbmVudDogd2Vic29ja2lmeSIpCiAgICAgICAgc2VsZi50ZXJtaW5hdGVfd2Vic29ja2lm -eShudW1iZXIpCiAgICAgICAgcmZiX3BvcnQsIG5vdm5jX3BvcnQgPSA1OTAwICsgbnVtYmVyLCA2OTAwICsgbnVtYmVyCiAgICAgICAgcHJvY2VzcyA9IHN1 -YnByb2Nlc3MuUG9wZW4oCiAgICAgICAgICAgIFtwYXRoLCAiLS13ZWIiLCAiL3Vzci9zaGFyZS9ub3ZuYyIsICItLWhlYXJ0YmVhdCIsICIzMCIsCiAgICAg -ICAgICAgICBmIjEyNy4wLjAuMTp7bm92bmNfcG9ydH0iLCBmIjEyNy4wLjAuMTp7cmZiX3BvcnR9Il0sCiAgICAgICAgICAgIGVudj1lbnZpcm9ubWVudCwg -c3RkaW49c3VicHJvY2Vzcy5ERVZOVUxMLAogICAgICAgICAgICBzdGRvdXQ9c3VicHJvY2Vzcy5ERVZOVUxMLCBzdGRlcnI9c3VicHJvY2Vzcy5ERVZOVUxM -KQogICAgICAgIHNlbGYud2Vic29ja2lmeV9wcm9jZXNzZXNbbnVtYmVyXSA9IHByb2Nlc3MKICAgICAgICBpZiBub3Qgc2VsZi53YWl0X2Zvcl9wb3J0KG5v -dm5jX3BvcnQsIHRpbWVvdXQ9MTApOgogICAgICAgICAgICBzZWxmLnRlcm1pbmF0ZV93ZWJzb2NraWZ5KG51bWJlcikKICAgICAgICAgICAgcmFpc2UgTm9W -TkNVbmF2YWlsYWJsZSgibm9WTkMgZGlkIG5vdCBiZWNvbWUgcmVhZHkiKQoKICAgIGRlZiBzdGFydF9jb21wb25lbnRzKHNlbGYsIG51bWJlciwgZW52aXJv -bm1lbnQsIHJ1bnRpbWUpOgogICAgICAgIGlmIHNlbGYuc2VydmljZV9yZWFkeShudW1iZXIpOgogICAgICAgICAgICByZXR1cm4KICAgICAgICByZmJfaXNf -cmVhZHkgPSByZmJfcmVhZHkobnVtYmVyKQogICAgICAgIHZuYyA9IHNodXRpbC53aGljaCgiWHZuYyIpIG9yIHNodXRpbC53aGljaCgiWHRpZ2Vydm5jIikK -ICAgICAgICB3ZWJzb2NraWZ5ID0gc2h1dGlsLndoaWNoKCJ3ZWJzb2NraWZ5IikKICAgICAgICBpZiBub3Qgd2Vic29ja2lmeToKICAgICAgICAgICAgcmFp -c2UgTm9WTkNVbmF2YWlsYWJsZSgiZGlzcGxheSBydW50aW1lIHVuYXZhaWxhYmxlIikKICAgICAgICBpZiBub3QgcmZiX2lzX3JlYWR5IGFuZCBub3Qgdm5j -OgogICAgICAgICAgICByYWlzZSBSdW50aW1lRXJyb3IoImRpc3BsYXkgcnVudGltZSB1bmF2YWlsYWJsZSIpCiAgICAgICAgaWYgcmZiX2lzX3JlYWR5IGFu -ZCBudW1iZXIgbm90IGluIHNlbGYubmFtZWRfcHJvY2Vzc2VzOgogICAgICAgICAgICBzZWxmLnJlY292ZXJfcHJvY2Vzc2VzKG51bWJlcikKICAgICAgICBp -ZiBub3QgcmZiX2lzX3JlYWR5OgogICAgICAgICAgICBzZWxmLnRlcm1pbmF0ZShudW1iZXIpCiAgICAgICAgICAgIHJmYl9wb3J0ID0gNTkwMCArIG51bWJl -cgogICAgICAgICAgICBzZWxmLnByb2Nlc3Nlc1tudW1iZXJdID0gW10KICAgICAgICAgICAgc2VsZi5uYW1lZF9wcm9jZXNzZXNbbnVtYmVyXSA9IHt9CiAg -ICAgICAgICAgIHNlbGYudHJhY2tfcHJvY2VzcyhudW1iZXIsICJ4dm5jIiwgc3VicHJvY2Vzcy5Qb3BlbigKICAgICAgICAgICAgICAgIFt2bmMsIGYiOntu -dW1iZXJ9IiwgIi1nZW9tZXRyeSIsIG9zLmVudmlyb24uZ2V0KCJDTVVYX1ZOQ19HRU9NRVRSWSIsICIxNDQweDkwMCIpLAogICAgICAgICAgICAgICAgICIt -ZGVwdGgiLCAiMjQiLCAiLXJmYnBvcnQiLCBzdHIocmZiX3BvcnQpLCAiLWxvY2FsaG9zdCIsCiAgICAgICAgICAgICAgICAgIi1TZWN1cml0eVR5cGVzIiwg -Ik5vbmUiLCAiLUFsd2F5c1NoYXJlZCJdLAogICAgICAgICAgICAgICAgZW52PWVudmlyb25tZW50LCBzdGRpbj1zdWJwcm9jZXNzLkRFVk5VTEwsCiAgICAg -ICAgICAgICAgICBzdGRvdXQ9c3VicHJvY2Vzcy5ERVZOVUxMLCBzdGRlcnI9c3VicHJvY2Vzcy5ERVZOVUxMKSkKICAgICAgICAgICAgaWYgbm90IHNlbGYu -d2FpdF9mb3JfcG9ydChyZmJfcG9ydCk6CiAgICAgICAgICAgICAgICByYWlzZSBSdW50aW1lRXJyb3IoIlh2bmMgZGlkIG5vdCBiZWNvbWUgcmVhZHkiKQog -ICAgICAgICAgICBzZWxmLnN0YXJ0X2RidXMobnVtYmVyLCBlbnZpcm9ubWVudCkKICAgICAgICAgICAgc2VsZi5zdGFydF9zZXNzaW9uX2NvbXBvbmVudHMo -bnVtYmVyLCBlbnZpcm9ubWVudCwgcnVudGltZSkKICAgICAgICBlbGlmIG51bWJlciBpbiBzZWxmLm5hbWVkX3Byb2Nlc3NlczoKICAgICAgICAgICAgbmFt -ZWQgPSBzZWxmLm5hbWVkX3Byb2Nlc3Nlc1tudW1iZXJdCiAgICAgICAgICAgIGRidXMgPSBuYW1lZC5nZXQoImRidXMiKQogICAgICAgICAgICBpZiBkYnVz -IGlzIE5vbmUgb3Igbm90IHNlbGYucHJvY2Vzc19hbGl2ZShkYnVzKToKICAgICAgICAgICAgICAgIGZvciBuYW1lIGluICgib3BlbmJveCIsICJ0aW50MiIs -ICJ2bmNjb25maWciKToKICAgICAgICAgICAgICAgICAgICBzZWxmLnJlbW92ZV9wcm9jZXNzKG51bWJlciwgbmFtZSkKICAgICAgICAgICAgICAgIHNlbGYu -c3RhcnRfZGJ1cyhudW1iZXIsIGVudmlyb25tZW50KQogICAgICAgICAgICBzZWxmLnN0YXJ0X3Nlc3Npb25fY29tcG9uZW50cyhudW1iZXIsIGVudmlyb25t -ZW50LCBydW50aW1lKQogICAgICAgIHdlYnNvY2tpZnlfcHJvY2VzcyA9IHNlbGYud2Vic29ja2lmeV9wcm9jZXNzZXMuZ2V0KG51bWJlcikKICAgICAgICBp -ZiAod2Vic29ja2lmeV9wcm9jZXNzIGlzIE5vbmUgb3Igbm90IHNlbGYucHJvY2Vzc19hbGl2ZSh3ZWJzb2NraWZ5X3Byb2Nlc3MpCiAgICAgICAgICAgICAg -ICBvciBub3Qgbm92bmNfcmVhZHkobnVtYmVyKSk6CiAgICAgICAgICAgIHNlbGYuc3RhcnRfd2Vic29ja2lmeShudW1iZXIsIGVudmlyb25tZW50LCB3ZWJz -b2NraWZ5KQogICAgICAgIChydW50aW1lIC8gImVudiIpLndyaXRlX3RleHQoZiJleHBvcnQgRElTUExBWT06e251bWJlcn1cbiIpCgogICAgZGVmIHN1cGVy -dmlzZShzZWxmLCBudW1iZXIsIGZpbmlzaGVkKToKICAgICAgICBydW50aW1lID0gc2VsZi5ydW50aW1lIC8gc3RyKG51bWJlcikKICAgICAgICBydW50aW1l -Lm1rZGlyKG1vZGU9MG83MDAsIHBhcmVudHM9VHJ1ZSwgZXhpc3Rfb2s9VHJ1ZSkKICAgICAgICBlbnZpcm9ubWVudCA9IGRpY3Qob3MuZW52aXJvbiwgRElT -UExBWT1mIjp7bnVtYmVyfSIsIENNVVhfREVTS1RPUF9SVU5USU1FX0RJUj1zdHIocnVudGltZSkpCiAgICAgICAgZW52aXJvbm1lbnRbIkNIUk9NRV9DT05G -SUdfSE9NRSJdID0gc3RyKHJ1bnRpbWUgLyAiY2hyb21lLWNvbmZpZyIpCiAgICAgICAgZW52aXJvbm1lbnRbIlhER19DT05GSUdfSE9NRSJdID0gc3RyKHJ1 -bnRpbWUgLyAieGRnLWNvbmZpZyIpCiAgICAgICAgZm9yIGtleSBpbiAoIk5PVElGWV9TT0NLRVQiLCAiREJVU19TRVNTSU9OX0JVU19BRERSRVNTIiwgIkRC -VVNfU0VTU0lPTl9CVVNfUElEIiwKICAgICAgICAgICAgICAgICAgICAiQVRfU1BJX0JVU19BRERSRVNTIiwgIkFUX1NQSV9CVVMiKToKICAgICAgICAgICAg -ZW52aXJvbm1lbnQucG9wKGtleSwgTm9uZSkKICAgICAgICB3aGlsZSBub3Qgc2VsZi5zaHV0ZG93bi5pc19zZXQoKToKICAgICAgICAgICAgdHJ5OgogICAg -ICAgICAgICAgICAgIyBUaGUgc3VwZXJ2aXNvciBpcyBhIGhlYWx0aCBjaGVjaywgbm90IGEgdGltZXItZHJpdmVuIHJlc3RhcnQuCiAgICAgICAgICAgICAg -ICAjIFByZXNlcnZlIGFuIGFjdGl2ZSBSRkIvbm9WTkMgc2Vzc2lvbiB1bnRpbCBpdHMgb3duZWQgZW5kcG9pbnRzIGZhaWwuCiAgICAgICAgICAgICAgICBp -ZiBub3Qgc2VsZi5zZXJ2aWNlX3JlYWR5KG51bWJlcik6CiAgICAgICAgICAgICAgICAgICAgc2VsZi5zdGFydF9jb21wb25lbnRzKG51bWJlciwgZW52aXJv -bm1lbnQsIHJ1bnRpbWUpCiAgICAgICAgICAgICAgICBzdGF0ZSA9ICJydW5uaW5nIiBpZiBzZWxmLnNlcnZpY2VfcmVhZHkobnVtYmVyKSBlbHNlICJ1bmF2 -YWlsYWJsZSIKICAgICAgICAgICAgZXhjZXB0IE5vVk5DVW5hdmFpbGFibGU6CiAgICAgICAgICAgICAgICBzZWxmLnRlcm1pbmF0ZV93ZWJzb2NraWZ5KG51 -bWJlcikKICAgICAgICAgICAgICAgIHN0YXRlID0gInVuYXZhaWxhYmxlIgogICAgICAgICAgICBleGNlcHQgKE9TRXJyb3IsIFJ1bnRpbWVFcnJvciwgc3Vi -cHJvY2Vzcy5TdWJwcm9jZXNzRXJyb3IpOgogICAgICAgICAgICAgICAgc2VsZi50ZXJtaW5hdGUobnVtYmVyKQogICAgICAgICAgICAgICAgc3RhdGUgPSAi -dW5hdmFpbGFibGUiCiAgICAgICAgICAgIHdpdGggc2VsZi5sb2NrOgogICAgICAgICAgICAgICAgc2VsZi5zdGF0ZXNbbnVtYmVyXSA9IHN0YXRlCiAgICAg -ICAgICAgIGZpbmlzaGVkLnNldCgpCiAgICAgICAgICAgIHNlbGYuc2h1dGRvd24ud2FpdCgzMCkKICAgICAgICBzZWxmLnRlcm1pbmF0ZShudW1iZXIpCgog -ICAgZGVmIHNuYXBzaG90KHNlbGYpOgogICAgICAgIHdpdGggc2VsZi5sb2NrOgogICAgICAgICAgICByZXR1cm4geyJ2ZXJzaW9uIjogMSwgImNhbkNyZWF0 -ZSI6IGxlbihzZWxmLmpvYnMpIDwgTUFYX0RJU1BMQVlTIC0gMSwKICAgICAgICAgICAgICAgICAgICAiZGlzcGxheXMiOiBbZGVzY3JpcHRvcigxLCAicnVu -bmluZyIgaWYgcmVhZHkoMSkgZWxzZSAidW5hdmFpbGFibGUiKV0KICAgICAgICAgICAgICAgICAgICArIFtkZXNjcmlwdG9yKG4sIHNlbGYuc3RhdGVzLmdl -dChuLCAibGF1bmNoaW5nIikpIGZvciBuIGluIHNlbGYuY2F0YWxvZy5udW1iZXJzKCldfQoKICAgIGRlZiBoYW5kbGUoc2VsZiwgcmVxdWVzdCk6CiAgICAg -ICAgaWYgcmVxdWVzdC5nZXQoImFjdGlvbiIpID09ICJsaXN0IjoKICAgICAgICAgICAgcmV0dXJuIHNlbGYuc25hcHNob3QoKQogICAgICAgIGlmIHJlcXVl -c3QuZ2V0KCJhY3Rpb24iKSAhPSAiY3JlYXRlIjoKICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJvcigidW5zdXBwb3J0ZWQgZGlzcGxheSBhY3Rpb24iKQog -ICAgICAgIG51bWJlciA9IHNlbGYuY2F0YWxvZy5hbGxvY2F0ZShyZXF1ZXN0LmdldCgicmVxdWVzdCIpKQogICAgICAgIHNlbGYuc3RhcnQobnVtYmVyKS53 -YWl0KDU1KQogICAgICAgIHJlc3VsdCA9IHNlbGYuc25hcHNob3QoKQogICAgICAgIHJlc3VsdFsiY3JlYXRlZCJdID0gZiJkaXNwbGF5OntudW1iZXJ9Igog -ICAgICAgIGlmIHNlbGYuc3RhdGVzLmdldChudW1iZXIpICE9ICJydW5uaW5nIjoKICAgICAgICAgICAgcmVzdWx0WyJlcnJvciJdID0gImRpc3BsYXlfc3Rh -cnRfZmFpbGVkIgogICAgICAgIHJldHVybiByZXN1bHQKCgpkZWYgc2VydmUocnVudGltZSk6CiAgICBydW50aW1lID0gUGF0aChydW50aW1lKQogICAgcnVu -dGltZS5ta2Rpcihtb2RlPTBvNzAwLCBwYXJlbnRzPVRydWUsIGV4aXN0X29rPVRydWUpCiAgICB3aXRoIChydW50aW1lIC8gImRpc3BsYXktY29udHJvbC5s -b2NrIikub3BlbigidyIpIGFzIGxvY2s6CiAgICAgICAgdHJ5OgogICAgICAgICAgICBmY250bC5mbG9jayhsb2NrLCBmY250bC5MT0NLX0VYIHwgZmNudGwu -TE9DS19OQikKICAgICAgICBleGNlcHQgQmxvY2tpbmdJT0Vycm9yOgogICAgICAgICAgICByZXR1cm4KICAgICAgICBpZiBub3QgYWxsKHNodXRpbC53aGlj -aChuYW1lKSBmb3IgbmFtZSBpbiAoIndlYnNvY2tpZnkiLCAib3BlbmJveCIsICJkYnVzLWxhdW5jaCIpKToKICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJv -cigiZGlzcGxheSBydW50aW1lIHVuYXZhaWxhYmxlIikKICAgICAgICBpZiBub3QgKHNodXRpbC53aGljaCgiWHZuYyIpIG9yIHNodXRpbC53aGljaCgiWHRp -Z2Vydm5jIikpOgogICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJkaXNwbGF5IHJ1bnRpbWUgdW5hdmFpbGFibGUiKQogICAgICAgIHNlcnZpY2UgPSBE -aXNwbGF5U2VydmljZShEaXNwbGF5Q2F0YWxvZyhQYXRoLmhvbWUoKSAvICIuY211eCIgLyAiZGlzcGxheXMiKSwgcnVudGltZSAvICJkaXNwbGF5cyIpCiAg -ICAgICAgZm9yIG51bWJlciBpbiBzZXJ2aWNlLmNhdGFsb2cubnVtYmVycygpOgogICAgICAgICAgICBzZXJ2aWNlLnN0YXJ0KG51bWJlcikKICAgICAgICBw -YXRoID0gcnVudGltZSAvICJkaXNwbGF5LWNvbnRyb2wuc29jayIKICAgICAgICBpZiBwYXRoLmV4aXN0cygpOgogICAgICAgICAgICBwYXRoLnVubGluaygp -CgogICAgICAgIGNsYXNzIEhhbmRsZXIoc29ja2V0c2VydmVyLlN0cmVhbVJlcXVlc3RIYW5kbGVyKToKICAgICAgICAgICAgZGVmIGhhbmRsZShzZWxmKToK -ICAgICAgICAgICAgICAgIHNlbGYuY29ubmVjdGlvbi5zZXR0aW1lb3V0KDYwKQogICAgICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgIHJh -dyA9IHNlbGYucmZpbGUucmVhZGxpbmUoNDA5NykKICAgICAgICAgICAgICAgICAgICBpZiBsZW4ocmF3KSA+IDQwOTY6CiAgICAgICAgICAgICAgICAgICAg -ICAgIHJhaXNlIFZhbHVlRXJyb3IoImRpc3BsYXkgcmVxdWVzdCB0b28gbGFyZ2UiKQogICAgICAgICAgICAgICAgICAgIHJlc3VsdCA9IHNlcnZpY2UuaGFu -ZGxlKGpzb24ubG9hZHMocmF3KSkKICAgICAgICAgICAgICAgIGV4Y2VwdCAoVmFsdWVFcnJvciwgS2V5RXJyb3IsIFR5cGVFcnJvciwgT1NFcnJvcik6CiAg -ICAgICAgICAgICAgICAgICAgcmVzdWx0ID0geyJlcnJvciI6ICJkaXNwbGF5X3JlcXVlc3RfZmFpbGVkIn0KICAgICAgICAgICAgICAgIHRyeToKICAgICAg -ICAgICAgICAgICAgICBzZWxmLndmaWxlLndyaXRlKGpzb24uZHVtcHMocmVzdWx0KS5lbmNvZGUoKSArIGIiXG4iKQogICAgICAgICAgICAgICAgZXhjZXB0 -IE9TRXJyb3I6CiAgICAgICAgICAgICAgICAgICAgcGFzcyAgIyBUaGUgcGVyc2lzdGVkIHJlY2VpcHQgcmVtYWlucyByZXBsYXlhYmxlIGFmdGVyIGRpc2Nv -bm5lY3QuCgogICAgICAgIGNsYXNzIFNlcnZlcihzb2NrZXRzZXJ2ZXIuVGhyZWFkaW5nVW5peFN0cmVhbVNlcnZlcik6CiAgICAgICAgICAgIGRhZW1vbl90 -aHJlYWRzID0gVHJ1ZQoKICAgICAgICBwcmV2aW91c191bWFzayA9IG9zLnVtYXNrKDBvMDc3KQogICAgICAgIHRyeToKICAgICAgICAgICAgd2l0aCBTZXJ2 -ZXIoc3RyKHBhdGgpLCBIYW5kbGVyKSBhcyBzZXJ2ZXI6CiAgICAgICAgICAgICAgICBvcy51bWFzayhwcmV2aW91c191bWFzaykKICAgICAgICAgICAgICAg -IHNlcnZlci5zZXJ2ZV9mb3JldmVyKCkKICAgICAgICBmaW5hbGx5OgogICAgICAgICAgICBvcy51bWFzayhwcmV2aW91c191bWFzaykKICAgICAgICAgICAg -c2VydmljZS5zaHV0ZG93bi5zZXQoKQogICAgICAgICAgICBwYXRoLnVubGluayhtaXNzaW5nX29rPVRydWUpCgoKZGVmIG1haW4oKToKICAgIHBhcnNlciA9 -IGFyZ3BhcnNlLkFyZ3VtZW50UGFyc2VyKGRlc2NyaXB0aW9uPV9fZG9jX18pCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50KCJhY3Rpb24iLCBjaG9pY2VzPVsi -bGlzdCIsICJjcmVhdGUiLCAic2VydmUiXSkKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoIi0tcmVxdWVzdC1pZCIpCiAgICBhcmdzID0gcGFyc2VyLnBhcnNl -X2FyZ3MoKQogICAgcnVudGltZSA9IG9zLmVudmlyb24uZ2V0KCJDTVVYX0RFU0tUT1BfUlVOVElNRV9ESVIiLCAiL3J1bi9jbXV4LWRlc2t0b3AiKQogICAg -aWYgYXJncy5hY3Rpb24gPT0gInNlcnZlIjoKICAgICAgICBzZXJ2ZShydW50aW1lKQogICAgICAgIHJldHVybiAwCiAgICBpZiBhcmdzLmFjdGlvbiA9PSAi -Y3JlYXRlIiBhbmQgbm90IGFyZ3MucmVxdWVzdF9pZDoKICAgICAgICBwYXJzZXIuZXJyb3IoImNyZWF0ZSByZXF1aXJlcyAtLXJlcXVlc3QtaWQgKHJldGFp -biBpdCB3aGVuIHJldHJ5aW5nKSIpCiAgICB0cnk6CiAgICAgICAgd2l0aCBzb2NrZXQuc29ja2V0KHNvY2tldC5BRl9VTklYLCBzb2NrZXQuU09DS19TVFJF -QU0pIGFzIGNsaWVudDoKICAgICAgICAgICAgY2xpZW50LnNldHRpbWVvdXQoNjApCiAgICAgICAgICAgIGNsaWVudC5jb25uZWN0KHN0cihQYXRoKHJ1bnRp -bWUpIC8gImRpc3BsYXktY29udHJvbC5zb2NrIikpCiAgICAgICAgICAgIGNsaWVudC5zZW5kYWxsKGpzb24uZHVtcHMoeyJhY3Rpb24iOiBhcmdzLmFjdGlv -biwgInJlcXVlc3QiOiBhcmdzLnJlcXVlc3RfaWR9KS5lbmNvZGUoKSArIGIiXG4iKQogICAgICAgICAgICB3aXRoIGNsaWVudC5tYWtlZmlsZSgicmIiKSBh -cyBzdHJlYW06CiAgICAgICAgICAgICAgICByZXN1bHQgPSBqc29uLmxvYWRzKHN0cmVhbS5yZWFkbGluZSg2NTUzNikpCiAgICBleGNlcHQgKE9TRXJyb3Is -IFZhbHVlRXJyb3IpOgogICAgICAgIHJlc3VsdCA9IHsiZXJyb3IiOiAiZGlzcGxheV9ydW50aW1lX3VuYXZhaWxhYmxlIn0KICAgIHByaW50KGpzb24uZHVt -cHMocmVzdWx0KSkKICAgIHJldHVybiAxIGlmICJlcnJvciIgaW4gcmVzdWx0IGVsc2UgMAoKCmlmIF9fbmFtZV9fID09ICJfX21haW5fXyI6CiAgICByYWlz -ZSBTeXN0ZW1FeGl0KG1haW4oKSkK +ICAgICBtYXJrZXIgPSBmImNtdXgtZGlzcGxheS17bnVtYmVyfS17bmFtZX0iCiAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgIHBhdHRlcm4gPSBy +ZiIoXnwvfCApe21hcmtlcn0oW1s6c3BhY2U6XV18JCkiCiAgICAgICAgICAgICAgICBvdXRwdXQgPSBzdWJwcm9jZXNzLmNoZWNrX291dHB1dChbInBncmVw +IiwgIi11IiwgdWlkLCAiLWYiLCBwYXR0ZXJuXSwgdGV4dD1UcnVlKQogICAgICAgICAgICAgICAgcGlkID0gaW50KG91dHB1dC5zcGxpdGxpbmVzKClbMF0p +CiAgICAgICAgICAgIGV4Y2VwdCAoT1NFcnJvciwgVmFsdWVFcnJvciwgSW5kZXhFcnJvciwgc3VicHJvY2Vzcy5DYWxsZWRQcm9jZXNzRXJyb3IpOgogICAg +ICAgICAgICAgICAgY29udGludWUKICAgICAgICAgICAgc2VsZi50cmFja19wcm9jZXNzKG51bWJlciwgbmFtZSwgT3duZWRQcm9jZXNzKHBpZCkpCiAgICAg +ICAgdHJ5OgogICAgICAgICAgICBvdXRwdXQgPSBzdWJwcm9jZXNzLmNoZWNrX291dHB1dCgKICAgICAgICAgICAgICAgIFsicGdyZXAiLCAiLXUiLCB1aWQs +ICItZiIsIHJmIig/Olh2bmN8WHRpZ2Vydm5jKS4qOntudW1iZXJ9KD86XHN8JCkiXSwKICAgICAgICAgICAgICAgIHRleHQ9VHJ1ZQogICAgICAgICAgICAp +CiAgICAgICAgICAgIHBpZCA9IGludChvdXRwdXQuc3BsaXRsaW5lcygpWzBdKQogICAgICAgIGV4Y2VwdCAoT1NFcnJvciwgVmFsdWVFcnJvciwgSW5kZXhF +cnJvciwgc3VicHJvY2Vzcy5DYWxsZWRQcm9jZXNzRXJyb3IpOgogICAgICAgICAgICBwaWQgPSAwCiAgICAgICAgaWYgcGlkID4gMToKICAgICAgICAgICAg +c2VsZi50cmFja19wcm9jZXNzKG51bWJlciwgInh2bmMiLCBPd25lZFByb2Nlc3MocGlkKSkKCiAgICBkZWYgc3RhcnRfZGJ1cyhzZWxmLCBudW1iZXIsIGVu +dmlyb25tZW50KToKICAgICAgICBidXMgPSBzdWJwcm9jZXNzLnJ1bihbImRidXMtbGF1bmNoIiwgIi0tc2gtc3ludGF4Il0sIGVudj1lbnZpcm9ubWVudCwK +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdGRpbj1zdWJwcm9jZXNzLkRFVk5VTEwsIHN0ZG91dD1zdWJwcm9jZXNzLlBJUEUsCiAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgc3RkZXJyPXN1YnByb2Nlc3MuREVWTlVMTCwgdGV4dD1UcnVlLCB0aW1lb3V0PTEwLAogICAgICAgICAgICAgICAgICAgICAg +ICAgICAgIGNoZWNrPVRydWUpLnN0ZG91dAogICAgICAgIGZvciBsaW5lIGluIGJ1cy5zcGxpdGxpbmVzKCk6CiAgICAgICAgICAgIGlmICI9IiBub3QgaW4g +bGluZToKICAgICAgICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgICAgIGtleSwgdmFsdWUgPSBsaW5lLnNwbGl0KCI9IiwgMSkKICAgICAgICAgICAgZW52 +aXJvbm1lbnRba2V5XSA9IHZhbHVlLnN0cmlwKCkucnN0cmlwKCI7Iikuc3RyaXAoKS5zdHJpcCgiJyIpCiAgICAgICAgdHJ5OgogICAgICAgICAgICBkYnVz +X3BpZCA9IGludChlbnZpcm9ubWVudC5nZXQoIkRCVVNfU0VTU0lPTl9CVVNfUElEIiwgIjAiKSkKICAgICAgICBleGNlcHQgVmFsdWVFcnJvcjoKICAgICAg +ICAgICAgZGJ1c19waWQgPSAwCiAgICAgICAgaWYgZGJ1c19waWQgPiAxOgogICAgICAgICAgICBzZWxmLnRyYWNrX3Byb2Nlc3MobnVtYmVyLCAiZGJ1cyIs +IE93bmVkUHJvY2VzcyhkYnVzX3BpZCkpCgogICAgZGVmIHN0YXJ0X3Nlc3Npb25fY29tcG9uZW50cyhzZWxmLCBudW1iZXIsIGVudmlyb25tZW50LCBydW50 +aW1lKToKICAgICAgICBmb3IgZXhlY3V0YWJsZSwgYXJncyBpbiBbCiAgICAgICAgICAgICgib3BlbmJveCIsIFtdKSwgKCJ0aW50MiIsIFsiLWMiLCAiL2V0 +Yy9jbXV4L3RpbnQycmMiXSksCiAgICAgICAgICAgICgidm5jY29uZmlnIiwgWyItbm93aW4iXSksCiAgICAgICAgXToKICAgICAgICAgICAgZXhpc3Rpbmcg +PSBzZWxmLm5hbWVkX3Byb2Nlc3Nlcy5nZXQobnVtYmVyLCB7fSkuZ2V0KGV4ZWN1dGFibGUpCiAgICAgICAgICAgIGlmIGV4aXN0aW5nIGlzIG5vdCBOb25l +IGFuZCBzZWxmLnByb2Nlc3NfYWxpdmUoZXhpc3RpbmcpOgogICAgICAgICAgICAgICAgY29udGludWUKICAgICAgICAgICAgcGF0aCA9IGV4ZWN1dGFibGUg +aWYgIi8iIGluIGV4ZWN1dGFibGUgZWxzZSBzaHV0aWwud2hpY2goZXhlY3V0YWJsZSkKICAgICAgICAgICAgaWYgbm90IHBhdGg6CiAgICAgICAgICAgICAg +ICBpZiBleGVjdXRhYmxlID09ICJ2bmNjb25maWciOgogICAgICAgICAgICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgICAgICAgICByYWlzZSBSdW50aW1l +RXJyb3IoZiJtaXNzaW5nIGRpc3BsYXkgY29tcG9uZW50OiB7ZXhlY3V0YWJsZX0iKQogICAgICAgICAgICBwYXRoID0gc2VsZi5zY29wZWRfY29tcG9uZW50 +X3BhdGgoZXhlY3V0YWJsZSwgcGF0aCwgbnVtYmVyLCBydW50aW1lKQogICAgICAgICAgICBwcm9jZXNzID0gc3VicHJvY2Vzcy5Qb3BlbihbcGF0aF0gKyBh +cmdzLCBlbnY9ZW52aXJvbm1lbnQsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0ZGluPXN1YnByb2Nlc3MuREVWTlVMTCwKICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3Rkb3V0PXN1YnByb2Nlc3MuREVWTlVMTCwgc3RkZXJyPXN1YnByb2Nlc3MuREVWTlVMTCkK +ICAgICAgICAgICAgc2VsZi50cmFja19wcm9jZXNzKG51bWJlciwgZXhlY3V0YWJsZSwgcHJvY2VzcykKCiAgICBkZWYgc3RhcnRfd2Vic29ja2lmeShzZWxm +LCBudW1iZXIsIGVudmlyb25tZW50LCBleGVjdXRhYmxlKToKICAgICAgICBwYXRoID0gZXhlY3V0YWJsZSBpZiAiLyIgaW4gZXhlY3V0YWJsZSBlbHNlIHNo +dXRpbC53aGljaChleGVjdXRhYmxlKQogICAgICAgIGlmIG5vdCBwYXRoOgogICAgICAgICAgICByYWlzZSBOb1ZOQ1VuYXZhaWxhYmxlKCJtaXNzaW5nIGRp +c3BsYXkgY29tcG9uZW50OiB3ZWJzb2NraWZ5IikKICAgICAgICBzZWxmLnRlcm1pbmF0ZV93ZWJzb2NraWZ5KG51bWJlcikKICAgICAgICByZmJfcG9ydCwg +bm92bmNfcG9ydCA9IDU5MDAgKyBudW1iZXIsIDY5MDAgKyBudW1iZXIKICAgICAgICBwcm9jZXNzID0gc3VicHJvY2Vzcy5Qb3BlbigKICAgICAgICAgICAg +W3BhdGgsICItLXdlYiIsICIvdXNyL3NoYXJlL25vdm5jIiwgIi0taGVhcnRiZWF0IiwgIjMwIiwKICAgICAgICAgICAgIGYiMTI3LjAuMC4xOntub3ZuY19w +b3J0fSIsIGYiMTI3LjAuMC4xOntyZmJfcG9ydH0iXSwKICAgICAgICAgICAgZW52PWVudmlyb25tZW50LCBzdGRpbj1zdWJwcm9jZXNzLkRFVk5VTEwsCiAg +ICAgICAgICAgIHN0ZG91dD1zdWJwcm9jZXNzLkRFVk5VTEwsIHN0ZGVycj1zdWJwcm9jZXNzLkRFVk5VTEwpCiAgICAgICAgc2VsZi53ZWJzb2NraWZ5X3By +b2Nlc3Nlc1tudW1iZXJdID0gcHJvY2VzcwogICAgICAgIGlmIG5vdCBzZWxmLndhaXRfZm9yX3BvcnQobm92bmNfcG9ydCwgdGltZW91dD0xMCk6CiAgICAg +ICAgICAgIHNlbGYudGVybWluYXRlX3dlYnNvY2tpZnkobnVtYmVyKQogICAgICAgICAgICByYWlzZSBOb1ZOQ1VuYXZhaWxhYmxlKCJub1ZOQyBkaWQgbm90 +IGJlY29tZSByZWFkeSIpCgogICAgZGVmIHN0YXJ0X2NvbXBvbmVudHMoc2VsZiwgbnVtYmVyLCBlbnZpcm9ubWVudCwgcnVudGltZSk6CiAgICAgICAgaWYg +c2VsZi5zZXJ2aWNlX3JlYWR5KG51bWJlcik6CiAgICAgICAgICAgIHJldHVybgogICAgICAgIHJmYl9pc19yZWFkeSA9IHJmYl9yZWFkeShudW1iZXIpCiAg +ICAgICAgdm5jID0gc2h1dGlsLndoaWNoKCJYdm5jIikgb3Igc2h1dGlsLndoaWNoKCJYdGlnZXJ2bmMiKQogICAgICAgIHdlYnNvY2tpZnkgPSBzaHV0aWwu +d2hpY2goIndlYnNvY2tpZnkiKQogICAgICAgIGlmIG5vdCB3ZWJzb2NraWZ5OgogICAgICAgICAgICByYWlzZSBOb1ZOQ1VuYXZhaWxhYmxlKCJkaXNwbGF5 +IHJ1bnRpbWUgdW5hdmFpbGFibGUiKQogICAgICAgIGlmIG5vdCByZmJfaXNfcmVhZHkgYW5kIG5vdCB2bmM6CiAgICAgICAgICAgIHJhaXNlIFJ1bnRpbWVF +cnJvcigiZGlzcGxheSBydW50aW1lIHVuYXZhaWxhYmxlIikKICAgICAgICBpZiByZmJfaXNfcmVhZHkgYW5kIG51bWJlciBub3QgaW4gc2VsZi5uYW1lZF9w +cm9jZXNzZXM6CiAgICAgICAgICAgIHNlbGYucmVjb3Zlcl9wcm9jZXNzZXMobnVtYmVyKQogICAgICAgIGlmIG5vdCByZmJfaXNfcmVhZHk6CiAgICAgICAg +ICAgIHNlbGYudGVybWluYXRlKG51bWJlcikKICAgICAgICAgICAgcmZiX3BvcnQgPSA1OTAwICsgbnVtYmVyCiAgICAgICAgICAgIHNlbGYucHJvY2Vzc2Vz +W251bWJlcl0gPSBbXQogICAgICAgICAgICBzZWxmLm5hbWVkX3Byb2Nlc3Nlc1tudW1iZXJdID0ge30KICAgICAgICAgICAgc2VsZi50cmFja19wcm9jZXNz +KG51bWJlciwgInh2bmMiLCBzdWJwcm9jZXNzLlBvcGVuKAogICAgICAgICAgICAgICAgW3ZuYywgZiI6e251bWJlcn0iLCAiLWdlb21ldHJ5Iiwgb3MuZW52 +aXJvbi5nZXQoIkNNVVhfVk5DX0dFT01FVFJZIiwgIjE0NDB4OTAwIiksCiAgICAgICAgICAgICAgICAgIi1kZXB0aCIsICIyNCIsICItcmZicG9ydCIsIHN0 +cihyZmJfcG9ydCksICItbG9jYWxob3N0IiwKICAgICAgICAgICAgICAgICAiLVNlY3VyaXR5VHlwZXMiLCAiTm9uZSIsICItQWx3YXlzU2hhcmVkIl0sCiAg +ICAgICAgICAgICAgICBlbnY9ZW52aXJvbm1lbnQsIHN0ZGluPXN1YnByb2Nlc3MuREVWTlVMTCwKICAgICAgICAgICAgICAgIHN0ZG91dD1zdWJwcm9jZXNz +LkRFVk5VTEwsIHN0ZGVycj1zdWJwcm9jZXNzLkRFVk5VTEwpKQogICAgICAgICAgICBpZiBub3Qgc2VsZi53YWl0X2Zvcl9wb3J0KHJmYl9wb3J0KToKICAg +ICAgICAgICAgICAgIHJhaXNlIFJ1bnRpbWVFcnJvcigiWHZuYyBkaWQgbm90IGJlY29tZSByZWFkeSIpCiAgICAgICAgICAgIHNlbGYuc3RhcnRfZGJ1cyhu +dW1iZXIsIGVudmlyb25tZW50KQogICAgICAgICAgICBzZWxmLnN0YXJ0X3Nlc3Npb25fY29tcG9uZW50cyhudW1iZXIsIGVudmlyb25tZW50LCBydW50aW1l +KQogICAgICAgIGVsaWYgbnVtYmVyIGluIHNlbGYubmFtZWRfcHJvY2Vzc2VzOgogICAgICAgICAgICBuYW1lZCA9IHNlbGYubmFtZWRfcHJvY2Vzc2VzW251 +bWJlcl0KICAgICAgICAgICAgZGJ1cyA9IG5hbWVkLmdldCgiZGJ1cyIpCiAgICAgICAgICAgIGlmIGRidXMgaXMgTm9uZSBvciBub3Qgc2VsZi5wcm9jZXNz +X2FsaXZlKGRidXMpOgogICAgICAgICAgICAgICAgZm9yIG5hbWUgaW4gKCJvcGVuYm94IiwgInRpbnQyIiwgInZuY2NvbmZpZyIpOgogICAgICAgICAgICAg +ICAgICAgIHNlbGYucmVtb3ZlX3Byb2Nlc3MobnVtYmVyLCBuYW1lKQogICAgICAgICAgICAgICAgc2VsZi5zdGFydF9kYnVzKG51bWJlciwgZW52aXJvbm1l +bnQpCiAgICAgICAgICAgIHNlbGYuc3RhcnRfc2Vzc2lvbl9jb21wb25lbnRzKG51bWJlciwgZW52aXJvbm1lbnQsIHJ1bnRpbWUpCiAgICAgICAgd2Vic29j +a2lmeV9wcm9jZXNzID0gc2VsZi53ZWJzb2NraWZ5X3Byb2Nlc3Nlcy5nZXQobnVtYmVyKQogICAgICAgIGlmICh3ZWJzb2NraWZ5X3Byb2Nlc3MgaXMgTm9u +ZSBvciBub3Qgc2VsZi5wcm9jZXNzX2FsaXZlKHdlYnNvY2tpZnlfcHJvY2VzcykKICAgICAgICAgICAgICAgIG9yIG5vdCBub3ZuY19yZWFkeShudW1iZXIp +KToKICAgICAgICAgICAgc2VsZi5zdGFydF93ZWJzb2NraWZ5KG51bWJlciwgZW52aXJvbm1lbnQsIHdlYnNvY2tpZnkpCiAgICAgICAgKHJ1bnRpbWUgLyAi +ZW52Iikud3JpdGVfdGV4dChmImV4cG9ydCBESVNQTEFZPTp7bnVtYmVyfVxuIikKCiAgICBkZWYgc3VwZXJ2aXNlKHNlbGYsIG51bWJlciwgZmluaXNoZWQp +OgogICAgICAgIHJ1bnRpbWUgPSBzZWxmLnJ1bnRpbWUgLyBzdHIobnVtYmVyKQogICAgICAgIHJ1bnRpbWUubWtkaXIobW9kZT0wbzcwMCwgcGFyZW50cz1U +cnVlLCBleGlzdF9vaz1UcnVlKQogICAgICAgIGVudmlyb25tZW50ID0gZGljdChvcy5lbnZpcm9uLCBESVNQTEFZPWYiOntudW1iZXJ9IiwgQ01VWF9ERVNL +VE9QX1JVTlRJTUVfRElSPXN0cihydW50aW1lKSkKICAgICAgICBlbnZpcm9ubWVudFsiQ0hST01FX0NPTkZJR19IT01FIl0gPSBzdHIocnVudGltZSAvICJj +aHJvbWUtY29uZmlnIikKICAgICAgICBlbnZpcm9ubWVudFsiWERHX0NPTkZJR19IT01FIl0gPSBzdHIocnVudGltZSAvICJ4ZGctY29uZmlnIikKICAgICAg +ICBmb3Iga2V5IGluICgiTk9USUZZX1NPQ0tFVCIsICJEQlVTX1NFU1NJT05fQlVTX0FERFJFU1MiLCAiREJVU19TRVNTSU9OX0JVU19QSUQiLAogICAgICAg +ICAgICAgICAgICAgICJBVF9TUElfQlVTX0FERFJFU1MiLCAiQVRfU1BJX0JVUyIpOgogICAgICAgICAgICBlbnZpcm9ubWVudC5wb3Aoa2V5LCBOb25lKQog +ICAgICAgIHdoaWxlIG5vdCBzZWxmLnNodXRkb3duLmlzX3NldCgpOgogICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICAjIFRoZSBzdXBlcnZpc29y +IGlzIGEgaGVhbHRoIGNoZWNrLCBub3QgYSB0aW1lci1kcml2ZW4gcmVzdGFydC4KICAgICAgICAgICAgICAgICMgUHJlc2VydmUgYW4gYWN0aXZlIFJGQi9u +b1ZOQyBzZXNzaW9uIHVudGlsIGl0cyBvd25lZCBlbmRwb2ludHMgZmFpbC4KICAgICAgICAgICAgICAgIGlmIG5vdCBzZWxmLnNlcnZpY2VfcmVhZHkobnVt +YmVyKToKICAgICAgICAgICAgICAgICAgICBzZWxmLnN0YXJ0X2NvbXBvbmVudHMobnVtYmVyLCBlbnZpcm9ubWVudCwgcnVudGltZSkKICAgICAgICAgICAg +ICAgIHN0YXRlID0gInJ1bm5pbmciIGlmIHNlbGYuc2VydmljZV9yZWFkeShudW1iZXIpIGVsc2UgInVuYXZhaWxhYmxlIgogICAgICAgICAgICBleGNlcHQg +Tm9WTkNVbmF2YWlsYWJsZToKICAgICAgICAgICAgICAgIHNlbGYudGVybWluYXRlX3dlYnNvY2tpZnkobnVtYmVyKQogICAgICAgICAgICAgICAgc3RhdGUg +PSAidW5hdmFpbGFibGUiCiAgICAgICAgICAgIGV4Y2VwdCAoT1NFcnJvciwgUnVudGltZUVycm9yLCBzdWJwcm9jZXNzLlN1YnByb2Nlc3NFcnJvcik6CiAg +ICAgICAgICAgICAgICBzZWxmLnRlcm1pbmF0ZShudW1iZXIpCiAgICAgICAgICAgICAgICBzdGF0ZSA9ICJ1bmF2YWlsYWJsZSIKICAgICAgICAgICAgd2l0 +aCBzZWxmLmxvY2s6CiAgICAgICAgICAgICAgICBzZWxmLnN0YXRlc1tudW1iZXJdID0gc3RhdGUKICAgICAgICAgICAgZmluaXNoZWQuc2V0KCkKICAgICAg +ICAgICAgc2VsZi5zaHV0ZG93bi53YWl0KDMwKQogICAgICAgIHNlbGYudGVybWluYXRlKG51bWJlcikKCiAgICBkZWYgc25hcHNob3Qoc2VsZik6CiAgICAg +ICAgd2l0aCBzZWxmLmxvY2s6CiAgICAgICAgICAgIHJldHVybiB7InZlcnNpb24iOiAxLCAiY2FuQ3JlYXRlIjogbGVuKHNlbGYuam9icykgPCBNQVhfRElT +UExBWVMgLSAxLAogICAgICAgICAgICAgICAgICAgICJkaXNwbGF5cyI6IFtkZXNjcmlwdG9yKDEsICJydW5uaW5nIiBpZiByZWFkeSgxKSBlbHNlICJ1bmF2 +YWlsYWJsZSIpXQogICAgICAgICAgICAgICAgICAgICsgW2Rlc2NyaXB0b3Iobiwgc2VsZi5zdGF0ZXMuZ2V0KG4sICJsYXVuY2hpbmciKSkgZm9yIG4gaW4g +c2VsZi5jYXRhbG9nLm51bWJlcnMoKV19CgogICAgZGVmIGhhbmRsZShzZWxmLCByZXF1ZXN0KToKICAgICAgICBpZiByZXF1ZXN0LmdldCgiYWN0aW9uIikg +PT0gImxpc3QiOgogICAgICAgICAgICByZXR1cm4gc2VsZi5zbmFwc2hvdCgpCiAgICAgICAgaWYgcmVxdWVzdC5nZXQoImFjdGlvbiIpICE9ICJjcmVhdGUi +OgogICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJ1bnN1cHBvcnRlZCBkaXNwbGF5IGFjdGlvbiIpCiAgICAgICAgbnVtYmVyID0gc2VsZi5jYXRhbG9n +LmFsbG9jYXRlKHJlcXVlc3QuZ2V0KCJyZXF1ZXN0IikpCiAgICAgICAgc2VsZi5zdGFydChudW1iZXIpLndhaXQoNTUpCiAgICAgICAgcmVzdWx0ID0gc2Vs +Zi5zbmFwc2hvdCgpCiAgICAgICAgcmVzdWx0WyJjcmVhdGVkIl0gPSBmImRpc3BsYXk6e251bWJlcn0iCiAgICAgICAgaWYgc2VsZi5zdGF0ZXMuZ2V0KG51 +bWJlcikgIT0gInJ1bm5pbmciOgogICAgICAgICAgICByZXN1bHRbImVycm9yIl0gPSAiZGlzcGxheV9zdGFydF9mYWlsZWQiCiAgICAgICAgcmV0dXJuIHJl +c3VsdAoKCmRlZiBzZXJ2ZShydW50aW1lKToKICAgIHJ1bnRpbWUgPSBQYXRoKHJ1bnRpbWUpCiAgICBydW50aW1lLm1rZGlyKG1vZGU9MG83MDAsIHBhcmVu +dHM9VHJ1ZSwgZXhpc3Rfb2s9VHJ1ZSkKICAgIHdpdGggKHJ1bnRpbWUgLyAiZGlzcGxheS1jb250cm9sLmxvY2siKS5vcGVuKCJ3IikgYXMgbG9jazoKICAg +ICAgICB0cnk6CiAgICAgICAgICAgIGZjbnRsLmZsb2NrKGxvY2ssIGZjbnRsLkxPQ0tfRVggfCBmY250bC5MT0NLX05CKQogICAgICAgIGV4Y2VwdCBCbG9j +a2luZ0lPRXJyb3I6CiAgICAgICAgICAgIHJldHVybgogICAgICAgIGlmIG5vdCBhbGwoc2h1dGlsLndoaWNoKG5hbWUpIGZvciBuYW1lIGluICgid2Vic29j +a2lmeSIsICJvcGVuYm94IiwgImRidXMtbGF1bmNoIikpOgogICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJkaXNwbGF5IHJ1bnRpbWUgdW5hdmFpbGFi +bGUiKQogICAgICAgIGlmIG5vdCAoc2h1dGlsLndoaWNoKCJYdm5jIikgb3Igc2h1dGlsLndoaWNoKCJYdGlnZXJ2bmMiKSk6CiAgICAgICAgICAgIHJhaXNl +IFZhbHVlRXJyb3IoImRpc3BsYXkgcnVudGltZSB1bmF2YWlsYWJsZSIpCiAgICAgICAgc2VydmljZSA9IERpc3BsYXlTZXJ2aWNlKERpc3BsYXlDYXRhbG9n +KFBhdGguaG9tZSgpIC8gIi5jbXV4IiAvICJkaXNwbGF5cyIpLCBydW50aW1lIC8gImRpc3BsYXlzIikKICAgICAgICBmb3IgbnVtYmVyIGluIHNlcnZpY2Uu +Y2F0YWxvZy5udW1iZXJzKCk6CiAgICAgICAgICAgIHNlcnZpY2Uuc3RhcnQobnVtYmVyKQogICAgICAgIHBhdGggPSBydW50aW1lIC8gImRpc3BsYXktY29u +dHJvbC5zb2NrIgogICAgICAgIGlmIHBhdGguZXhpc3RzKCk6CiAgICAgICAgICAgIHBhdGgudW5saW5rKCkKCiAgICAgICAgY2xhc3MgSGFuZGxlcihzb2Nr +ZXRzZXJ2ZXIuU3RyZWFtUmVxdWVzdEhhbmRsZXIpOgogICAgICAgICAgICBkZWYgaGFuZGxlKHNlbGYpOgogICAgICAgICAgICAgICAgc2VsZi5jb25uZWN0 +aW9uLnNldHRpbWVvdXQoNjApCiAgICAgICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICAgICAgcmF3ID0gc2VsZi5yZmlsZS5yZWFkbGluZSg0MDk3 +KQogICAgICAgICAgICAgICAgICAgIGlmIGxlbihyYXcpID4gNDA5NjoKICAgICAgICAgICAgICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJvcigiZGlzcGxh +eSByZXF1ZXN0IHRvbyBsYXJnZSIpCiAgICAgICAgICAgICAgICAgICAgcmVzdWx0ID0gc2VydmljZS5oYW5kbGUoanNvbi5sb2FkcyhyYXcpKQogICAgICAg +ICAgICAgICAgZXhjZXB0IChWYWx1ZUVycm9yLCBLZXlFcnJvciwgVHlwZUVycm9yLCBPU0Vycm9yKToKICAgICAgICAgICAgICAgICAgICByZXN1bHQgPSB7 +ImVycm9yIjogImRpc3BsYXlfcmVxdWVzdF9mYWlsZWQifQogICAgICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgIHNlbGYud2ZpbGUud3Jp +dGUoanNvbi5kdW1wcyhyZXN1bHQpLmVuY29kZSgpICsgYiJcbiIpCiAgICAgICAgICAgICAgICBleGNlcHQgT1NFcnJvcjoKICAgICAgICAgICAgICAgICAg +ICBwYXNzICAjIFRoZSBwZXJzaXN0ZWQgcmVjZWlwdCByZW1haW5zIHJlcGxheWFibGUgYWZ0ZXIgZGlzY29ubmVjdC4KCiAgICAgICAgY2xhc3MgU2VydmVy +KHNvY2tldHNlcnZlci5UaHJlYWRpbmdVbml4U3RyZWFtU2VydmVyKToKICAgICAgICAgICAgZGFlbW9uX3RocmVhZHMgPSBUcnVlCgogICAgICAgIHByZXZp +b3VzX3VtYXNrID0gb3MudW1hc2soMG8wNzcpCiAgICAgICAgdHJ5OgogICAgICAgICAgICB3aXRoIFNlcnZlcihzdHIocGF0aCksIEhhbmRsZXIpIGFzIHNl +cnZlcjoKICAgICAgICAgICAgICAgIG9zLnVtYXNrKHByZXZpb3VzX3VtYXNrKQogICAgICAgICAgICAgICAgc2VydmVyLnNlcnZlX2ZvcmV2ZXIoKQogICAg +ICAgIGZpbmFsbHk6CiAgICAgICAgICAgIG9zLnVtYXNrKHByZXZpb3VzX3VtYXNrKQogICAgICAgICAgICBzZXJ2aWNlLnNodXRkb3duLnNldCgpCiAgICAg +ICAgICAgIHBhdGgudW5saW5rKG1pc3Npbmdfb2s9VHJ1ZSkKCgpkZWYgbWFpbigpOgogICAgcGFyc2VyID0gYXJncGFyc2UuQXJndW1lbnRQYXJzZXIoZGVz +Y3JpcHRpb249X19kb2NfXykKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoImFjdGlvbiIsIGNob2ljZXM9WyJsaXN0IiwgImNyZWF0ZSIsICJzZXJ2ZSJdKQog +ICAgcGFyc2VyLmFkZF9hcmd1bWVudCgiLS1yZXF1ZXN0LWlkIikKICAgIGFyZ3MgPSBwYXJzZXIucGFyc2VfYXJncygpCiAgICBydW50aW1lID0gb3MuZW52 +aXJvbi5nZXQoIkNNVVhfREVTS1RPUF9SVU5USU1FX0RJUiIsICIvcnVuL2NtdXgtZGVza3RvcCIpCiAgICBpZiBhcmdzLmFjdGlvbiA9PSAic2VydmUiOgog +ICAgICAgIHNlcnZlKHJ1bnRpbWUpCiAgICAgICAgcmV0dXJuIDAKICAgIGlmIGFyZ3MuYWN0aW9uID09ICJjcmVhdGUiIGFuZCBub3QgYXJncy5yZXF1ZXN0 +X2lkOgogICAgICAgIHBhcnNlci5lcnJvcigiY3JlYXRlIHJlcXVpcmVzIC0tcmVxdWVzdC1pZCAocmV0YWluIGl0IHdoZW4gcmV0cnlpbmcpIikKICAgIHRy +eToKICAgICAgICB3aXRoIHNvY2tldC5zb2NrZXQoc29ja2V0LkFGX1VOSVgsIHNvY2tldC5TT0NLX1NUUkVBTSkgYXMgY2xpZW50OgogICAgICAgICAgICBj +bGllbnQuc2V0dGltZW91dCg2MCkKICAgICAgICAgICAgY2xpZW50LmNvbm5lY3Qoc3RyKFBhdGgocnVudGltZSkgLyAiZGlzcGxheS1jb250cm9sLnNvY2si +KSkKICAgICAgICAgICAgY2xpZW50LnNlbmRhbGwoanNvbi5kdW1wcyh7ImFjdGlvbiI6IGFyZ3MuYWN0aW9uLCAicmVxdWVzdCI6IGFyZ3MucmVxdWVzdF9p +ZH0pLmVuY29kZSgpICsgYiJcbiIpCiAgICAgICAgICAgIHdpdGggY2xpZW50Lm1ha2VmaWxlKCJyYiIpIGFzIHN0cmVhbToKICAgICAgICAgICAgICAgIHJl +c3VsdCA9IGpzb24ubG9hZHMoc3RyZWFtLnJlYWRsaW5lKDY1NTM2KSkKICAgIGV4Y2VwdCAoT1NFcnJvciwgVmFsdWVFcnJvcik6CiAgICAgICAgcmVzdWx0 +ID0geyJlcnJvciI6ICJkaXNwbGF5X3J1bnRpbWVfdW5hdmFpbGFibGUifQogICAgcHJpbnQoanNvbi5kdW1wcyhyZXN1bHQpKQogICAgcmV0dXJuIDEgaWYg +ImVycm9yIiBpbiByZXN1bHQgZWxzZSAwCgoKaWYgX19uYW1lX18gPT0gIl9fbWFpbl9fIjoKICAgIHJhaXNlIFN5c3RlbUV4aXQobWFpbigpKQo= """ private static let path = "$HOME/.cmux/cmux-display" diff --git a/tests/fixtures/cmux-display b/tests/fixtures/cmux-display index fb57431ebb5c..afb2b1ecf9ed 100755 --- a/tests/fixtures/cmux-display +++ b/tests/fixtures/cmux-display @@ -262,7 +262,8 @@ class DisplayService: for name in ("openbox", "tint2", "vncconfig"): marker = f"cmux-display-{number}-{name}" try: - output = subprocess.check_output(["pgrep", "-u", uid, "-x", marker], text=True) + pattern = rf"(^|/| ){marker}([[:space:]]|$)" + output = subprocess.check_output(["pgrep", "-u", uid, "-f", pattern], text=True) pid = int(output.splitlines()[0]) except (OSError, ValueError, IndexError, subprocess.CalledProcessError): continue From c1cb5d9426f37e0c5c409c53bfe56b2c3cb67457 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 18:01:01 -0700 Subject: [PATCH 46/64] test: cover Cloud restore and destination comment fixes --- cmuxTests/CloudDisplayCatalogTests.swift | 2 ++ cmuxTests/CloudSurfaceOwnershipTests.swift | 12 ++++++++++++ tests/test_cloud_display_catalog.py | 4 ++-- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/cmuxTests/CloudDisplayCatalogTests.swift b/cmuxTests/CloudDisplayCatalogTests.swift index 8883d62f5c87..fa298a8e8968 100644 --- a/cmuxTests/CloudDisplayCatalogTests.swift +++ b/cmuxTests/CloudDisplayCatalogTests.swift @@ -77,6 +77,7 @@ struct CloudDisplayCatalogTests { failed = true await service.refresh() #expect(service.snapshot == nil && !service.canCreate) + #expect(service.displaySnapshot?.displays.count == 1) } @Test("A failed refresh invalidates the cached guest catalog") @@ -92,6 +93,7 @@ struct CloudDisplayCatalogTests { shouldFail = true await service.refresh() #expect(service.snapshot == nil && !service.canCreate) + #expect(service.displaySnapshot?.displays.count == 1) } @Test("Cancelling display creation cancels the guest exec") diff --git a/cmuxTests/CloudSurfaceOwnershipTests.swift b/cmuxTests/CloudSurfaceOwnershipTests.swift index c3f5ca1f9472..4958d4510999 100644 --- a/cmuxTests/CloudSurfaceOwnershipTests.swift +++ b/cmuxTests/CloudSurfaceOwnershipTests.swift @@ -14,6 +14,18 @@ import Testing struct CloudSurfaceOwnershipTests { private let machine = SurfaceMachineID.cloud("ownership-b") + @Test("A retired destination fails closed before ownership policy lookup") + func missingDestinationRejectsValidation() { + let catalog = SurfaceCatalog() + let destination = SurfaceDestination.workspace(id: UUID(), placement: .tab) + #expect(throws: SurfaceCatalogError.self) { + try catalog.validateOwnership( + of: [SurfaceResourceID(machine: machine, kind: .display, key: "display:1")], + at: destination + ) + } + } + @Test("Saved display identities are checked before catalog mutation") func rejectsForeignCatalogRestore() throws { let workspace = cloudWorkspace() diff --git a/tests/test_cloud_display_catalog.py b/tests/test_cloud_display_catalog.py index 33d9d7724d62..42fa9f1850bd 100644 --- a/tests/test_cloud_display_catalog.py +++ b/tests/test_cloud_display_catalog.py @@ -208,12 +208,12 @@ def test_recovery_adopts_display_scoped_processes_by_full_command(self): service = display.DisplayService(self.catalog(), self.root / "runtime") def pgrep(command, **_options): - if "cmux-display-2-openbox" in command[-1]: + if "cmux\\-display\\-2\\-openbox" in command[-1]: return "12345\n" raise display.subprocess.CalledProcessError(1, command) with mock.patch.object(display.subprocess, "check_output", side_effect=pgrep): - service.recover_processes(2) + service.recover_processes(2, self.root / "runtime" / "2") self.assertIn("openbox", service.named_processes[2]) def test_additional_desktop_clients_use_display_scoped_process_names(self): From 178d35e5da031a257e9ece58a2dd547b5c867f99 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 18:01:38 -0700 Subject: [PATCH 47/64] fix: address Cloud display review comments --- Sources/Cloud/CloudDisplayCoordinator.swift | 5 + Sources/Cloud/CloudGuestDisplayScript.swift | 450 +++++++++--------- .../PortForward/CloudBrowserAccessState.swift | 1 + .../Panels/BrowserPanel+CloudConnection.swift | 17 +- Sources/Panels/BrowserPanel.swift | 10 +- Sources/Surfaces/CmuxTuiSnapshotParser.swift | 3 +- .../CmuxTuiSurfaceProvider+Displays.swift | 2 +- .../CmuxTuiSurfaceProvider+PortForward.swift | 4 + .../Surfaces/CmuxTuiSurfaceProviders.swift | 9 +- .../DockSplitStore+SurfaceOwnership.swift | 5 +- .../Surfaces/SurfaceCatalog+CloudPorts.swift | 5 +- .../Surfaces/SurfaceCatalog+Ownership.swift | 11 +- tests/fixtures/cmux-display | 15 +- 13 files changed, 290 insertions(+), 247 deletions(-) diff --git a/Sources/Cloud/CloudDisplayCoordinator.swift b/Sources/Cloud/CloudDisplayCoordinator.swift index ff27ee89dda8..857dbeeebb5a 100644 --- a/Sources/Cloud/CloudDisplayCoordinator.swift +++ b/Sources/Cloud/CloudDisplayCoordinator.swift @@ -8,6 +8,7 @@ import Observation final class CloudDisplayCoordinator { private let execute: @MainActor (String, Int) async throws -> VMExecResult private(set) var snapshot: CloudGuestDisplaySnapshot? + private(set) var lastValidatedSnapshot: CloudGuestDisplaySnapshot? private(set) var isAvailable = false private var generation: UInt64 = 0 private var requestID: UUID? @@ -19,6 +20,7 @@ final class CloudDisplayCoordinator { } var canCreate: Bool { isAvailable && (snapshot?.canCreate == true || requestID != nil) && creation == nil } + var displaySnapshot: CloudGuestDisplaySnapshot? { snapshot ?? lastValidatedSnapshot } func refresh() async { guard creation == nil else { return } @@ -34,6 +36,7 @@ final class CloudDisplayCoordinator { let snapshot = try CloudGuestDisplaySnapshot(data: Data(response.stdout.utf8)) guard let self, token == self.generation, !Task.isCancelled else { return } self.snapshot = snapshot + self.lastValidatedSnapshot = snapshot self.isAvailable = true } catch { guard let self, token == self.generation else { return } @@ -70,6 +73,7 @@ final class CloudDisplayCoordinator { try Task.checkCancellation() guard let self, self.generation == token else { throw CancellationError() } self.snapshot = snapshot + self.lastValidatedSnapshot = snapshot guard response.exitCode == 0, snapshot.error == nil, snapshot.created != nil else { throw SurfaceCatalogError.unsupported(String(localized: "cloud.display.creationFailed", defaultValue: "The new display could not start. Refresh Displays, then retry. Existing displays are unchanged.")) } @@ -97,6 +101,7 @@ final class CloudDisplayCoordinator { creation?.cancel() creation = nil snapshot = nil + lastValidatedSnapshot = nil requestID = nil isAvailable = false } diff --git a/Sources/Cloud/CloudGuestDisplayScript.swift b/Sources/Cloud/CloudGuestDisplayScript.swift index 6cdb02f4b02a..ebbcfe3455ca 100644 --- a/Sources/Cloud/CloudGuestDisplayScript.swift +++ b/Sources/Cloud/CloudGuestDisplayScript.swift @@ -8,229 +8,231 @@ IyEvdXNyL2Jpbi9lbnYgcHl0aG9uMwoiIiJHdWVzdC1vd25lZCBkaXNwbGF5IGNhdGFsb2cgYW5kIHN1 b3Agc2VydmljZS4KClRoZSBleGlzdGluZyA6MSBkZXNrdG9wIHJlbWFpbnMgb3duZWQgYnkgc3RhcnQtdm5jLnNoLiBBZGRpdGlvbmFsIHJlY29yZHMgZWFj aApvd24gYW4gWCBzZXJ2ZXIsIHNlc3Npb24gYnVzLCB3aW5kb3cgbWFuYWdlciBhbmQgbm9WTkMgbGlzdGVuZXIuIFRoZSBjb250cm9sCnNvY2tldCBpcyBs b2NhbCB0byB0aGlzIGd1ZXN0IGFuZCBvbmx5IGFjY2Vzc2libGUgdG8gaXRzIHdvcmsgdXNlci4KIiIiCmltcG9ydCBhcmdwYXJzZQppbXBvcnQgZmNudGwK -aW1wb3J0IGh0dHAuY2xpZW50CmltcG9ydCBqc29uCmltcG9ydCBvcwpmcm9tIHBhdGhsaWIgaW1wb3J0IFBhdGgKaW1wb3J0IHNodXRpbAppbXBvcnQgc29j -a2V0CmltcG9ydCBzb2NrZXRzZXJ2ZXIKaW1wb3J0IHN1YnByb2Nlc3MKaW1wb3J0IHRlbXBmaWxlCmltcG9ydCB0aHJlYWRpbmcKaW1wb3J0IHRpbWUKaW1w -b3J0IHV1aWQKCk1BWF9ESVNQTEFZUyA9IDE2ClNUQVJURVIgPSAiL3Vzci9sb2NhbC9iaW4vc3RhcnQtdm5jLnNoIgoKCmRlZiBkZXNjcmlwdG9yKG51bWJl -ciwgc3RhdGU9InJ1bm5pbmciKToKICAgIHJldHVybiB7ImlkIjogZiJkaXNwbGF5OntudW1iZXJ9IiwgIm51bWJlciI6IG51bWJlciwKICAgICAgICAgICAg -InBvcnQiOiA2OTAwICsgbnVtYmVyLCAic3RhdGUiOiBzdGF0ZX0KCgpkZWYgbGlzdGVuaW5nKHBvcnQpOgogICAgdHJ5OgogICAgICAgIHdpdGggc29ja2V0 -LmNyZWF0ZV9jb25uZWN0aW9uKCgiMTI3LjAuMC4xIiwgcG9ydCksIHRpbWVvdXQ9MC4zKToKICAgICAgICAgICAgcmV0dXJuIFRydWUKICAgIGV4Y2VwdCBP -U0Vycm9yOgogICAgICAgIHJldHVybiBGYWxzZQoKCmRlZiByZmJfcmVhZHkobnVtYmVyKToKICAgICIiIkEgYm91bmQgSFRUUCBwb3J0IGFsb25lIGRvZXMg -bm90IHByb3ZlIHRoYXQgYW4gWC9SRkIgc2VydmVyIGV4aXN0cy4iIiIKICAgIHRyeToKICAgICAgICB3aXRoIHNvY2tldC5jcmVhdGVfY29ubmVjdGlvbigo -IjEyNy4wLjAuMSIsIDU5MDAgKyBudW1iZXIpLCB0aW1lb3V0PTAuNSkgYXMgcmZiOgogICAgICAgICAgICB2ZXJzaW9uID0gYiIiCiAgICAgICAgICAgIHdo -aWxlIGxlbih2ZXJzaW9uKSA8IDEyOgogICAgICAgICAgICAgICAgcGFydCA9IHJmYi5yZWN2KDEyIC0gbGVuKHZlcnNpb24pKQogICAgICAgICAgICAgICAg -aWYgbm90IHBhcnQ6CiAgICAgICAgICAgICAgICAgICAgcmV0dXJuIEZhbHNlCiAgICAgICAgICAgICAgICB2ZXJzaW9uICs9IHBhcnQKICAgICAgICAgICAg -aWYgbm90IHZlcnNpb24uc3RhcnRzd2l0aChiIlJGQiAiKToKICAgICAgICAgICAgICAgIHJldHVybiBGYWxzZQogICAgZXhjZXB0IChPU0Vycm9yLCBodHRw -LmNsaWVudC5IVFRQRXhjZXB0aW9uKToKICAgICAgICByZXR1cm4gRmFsc2UKICAgIHJldHVybiBUcnVlCgoKZGVmIG5vdm5jX3JlYWR5KG51bWJlcik6CiAg -ICB0cnk6CiAgICAgICAgY29ubmVjdGlvbiA9IGh0dHAuY2xpZW50LkhUVFBDb25uZWN0aW9uKCIxMjcuMC4wLjEiLCA2OTAwICsgbnVtYmVyLCB0aW1lb3V0 -PTAuNSkKICAgICAgICB0cnk6CiAgICAgICAgICAgIGNvbm5lY3Rpb24ucmVxdWVzdCgiSEVBRCIsICIvdm5jLmh0bWwiKQogICAgICAgICAgICByZXR1cm4g -Y29ubmVjdGlvbi5nZXRyZXNwb25zZSgpLnN0YXR1cyA9PSAyMDAKICAgICAgICBmaW5hbGx5OgogICAgICAgICAgICBjb25uZWN0aW9uLmNsb3NlKCkKICAg -IGV4Y2VwdCAoT1NFcnJvciwgaHR0cC5jbGllbnQuSFRUUEV4Y2VwdGlvbik6CiAgICAgICAgcmV0dXJuIEZhbHNlCgoKZGVmIHJlYWR5KG51bWJlcik6CiAg -ICAiIiJBIGJvdW5kIGVuZHBvaW50IGFsb25lIGRvZXMgbm90IHByb3ZlIHRoYXQgdGhlIGNvbXBsZXRlIGRlc2t0b3AgZXhpc3RzLiIiIgogICAgcmV0dXJu -IHJmYl9yZWFkeShudW1iZXIpIGFuZCBub3ZuY19yZWFkeShudW1iZXIpCgoKY2xhc3MgRGlzcGxheUNhdGFsb2c6CiAgICAiIiJTZXJpYWxpemVzIGFsbG9j -YXRpb24gYW5kIHBlcnNpc3RzIHJlcXVlc3QgcmVjZWlwdHMgYmVmb3JlIHN0YXJ0aW5nIHdvcmsuIiIiCgogICAgZGVmIF9faW5pdF9fKHNlbGYsIGRpcmVj -dG9yeSwgb2NjdXBpZWQ9Tm9uZSk6CiAgICAgICAgc2VsZi5kaXJlY3RvcnkgPSBQYXRoKGRpcmVjdG9yeSkKICAgICAgICBzZWxmLmRpcmVjdG9yeS5ta2Rp -cihtb2RlPTBvNzAwLCBwYXJlbnRzPVRydWUsIGV4aXN0X29rPVRydWUpCiAgICAgICAgc2VsZi5wYXRoID0gc2VsZi5kaXJlY3RvcnkgLyAiY2F0YWxvZy5q -c29uIgogICAgICAgIHNlbGYubG9jayA9IHRocmVhZGluZy5STG9jaygpCiAgICAgICAgc2VsZi5vY2N1cGllZCA9IG9jY3VwaWVkIG9yIChsYW1iZGEgbjog -UGF0aChmIi90bXAvLlgxMS11bml4L1h7bn0iKS5leGlzdHMoKQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgb3IgbGlzdGVuaW5nKDU5 -MDAgKyBuKSBvciBsaXN0ZW5pbmcoNjkwMCArIG4pKQogICAgICAgIHNlbGYucmVjb3JkcyA9IFtdCiAgICAgICAgaWYgc2VsZi5wYXRoLmV4aXN0cygpOgog -ICAgICAgICAgICBkYXRhID0ganNvbi5sb2FkcyhzZWxmLnBhdGgucmVhZF90ZXh0KCkpCiAgICAgICAgICAgIGlmIGRhdGEuZ2V0KCJ2ZXJzaW9uIikgIT0g -MToKICAgICAgICAgICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoInVuc3VwcG9ydGVkIGRpc3BsYXkgY2F0YWxvZyB2ZXJzaW9uIikKICAgICAgICAgICAgc2Vl -bl9udW1iZXJzLCBzZWVuX3JlcXVlc3RzID0gc2V0KCksIHNldCgpCiAgICAgICAgICAgIGZvciByZWNvcmQgaW4gZGF0YVsiZGlzcGxheXMiXToKICAgICAg -ICAgICAgICAgIG51bWJlciwgcmVxdWVzdCA9IHJlY29yZFsibnVtYmVyIl0sIHJlY29yZFsicmVxdWVzdCJdCiAgICAgICAgICAgICAgICBpZiB0eXBlKG51 -bWJlcikgaXMgbm90IGludCBvciBub3QgMiA8PSBudW1iZXIgPD0gTUFYX0RJU1BMQVlTOgogICAgICAgICAgICAgICAgICAgIHJhaXNlIFZhbHVlRXJyb3Io -ImludmFsaWQgZGlzcGxheSBudW1iZXIiKQogICAgICAgICAgICAgICAgaWYgc3RyKHV1aWQuVVVJRChyZXF1ZXN0KSkgIT0gcmVxdWVzdCBvciBudW1iZXIg -aW4gc2Vlbl9udW1iZXJzIG9yIHJlcXVlc3QgaW4gc2Vlbl9yZXF1ZXN0czoKICAgICAgICAgICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJpbnZhbGlk -IGRpc3BsYXkgaWRlbnRpdHkiKQogICAgICAgICAgICAgICAgc2Vlbl9udW1iZXJzLmFkZChudW1iZXIpCiAgICAgICAgICAgICAgICBzZWVuX3JlcXVlc3Rz -LmFkZChyZXF1ZXN0KQogICAgICAgICAgICAgICAgc2VsZi5yZWNvcmRzLmFwcGVuZCh7Im51bWJlciI6IG51bWJlciwgInJlcXVlc3QiOiByZXF1ZXN0fSkK -CiAgICBkZWYgbnVtYmVycyhzZWxmKToKICAgICAgICB3aXRoIHNlbGYubG9jazoKICAgICAgICAgICAgcmV0dXJuIHNvcnRlZChyZWNvcmRbIm51bWJlciJd -IGZvciByZWNvcmQgaW4gc2VsZi5yZWNvcmRzKQoKICAgIGRlZiBhbGxvY2F0ZShzZWxmLCByZXF1ZXN0KToKICAgICAgICBpZiBub3QgaXNpbnN0YW5jZShy -ZXF1ZXN0LCBzdHIpIG9yIHN0cih1dWlkLlVVSUQocmVxdWVzdCkpICE9IHJlcXVlc3Q6CiAgICAgICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoImludmFsaWQg -cmVxdWVzdCBpZGVudGl0eSIpCiAgICAgICAgd2l0aCBzZWxmLmxvY2s6CiAgICAgICAgICAgIGZvciByZWNvcmQgaW4gc2VsZi5yZWNvcmRzOgogICAgICAg -ICAgICAgICAgaWYgcmVjb3JkWyJyZXF1ZXN0Il0gPT0gcmVxdWVzdDoKICAgICAgICAgICAgICAgICAgICByZXR1cm4gcmVjb3JkWyJudW1iZXIiXQogICAg -ICAgICAgICB1c2VkID0gc2V0KHNlbGYubnVtYmVycygpKQogICAgICAgICAgICBudW1iZXIgPSBuZXh0KChuIGZvciBuIGluIHJhbmdlKDIsIE1BWF9ESVNQ -TEFZUyArIDEpCiAgICAgICAgICAgICAgICAgICAgICAgICAgIGlmIG4gbm90IGluIHVzZWQgYW5kIG5vdCBzZWxmLm9jY3VwaWVkKG4pKSwgTm9uZSkKICAg -ICAgICAgICAgaWYgbnVtYmVyIGlzIE5vbmU6CiAgICAgICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJkaXNwbGF5IGNhcGFjaXR5IGV4aGF1c3RlZCIp -CiAgICAgICAgICAgIHJlY29yZHMgPSBzZWxmLnJlY29yZHMgKyBbeyJudW1iZXIiOiBudW1iZXIsICJyZXF1ZXN0IjogcmVxdWVzdH1dCiAgICAgICAgICAg -IGZkLCB0ZW1wb3JhcnkgPSB0ZW1wZmlsZS5ta3N0ZW1wKHByZWZpeD0iY2F0YWxvZy0iLCBkaXI9c2VsZi5kaXJlY3RvcnkpCiAgICAgICAgICAgIHRyeToK -ICAgICAgICAgICAgICAgIHdpdGggb3MuZmRvcGVuKGZkLCAidyIpIGFzIG91dHB1dDoKICAgICAgICAgICAgICAgICAgICBqc29uLmR1bXAoeyJ2ZXJzaW9u -IjogMSwgImRpc3BsYXlzIjogcmVjb3Jkc30sIG91dHB1dCkKICAgICAgICAgICAgICAgICAgICBvdXRwdXQuZmx1c2goKQogICAgICAgICAgICAgICAgICAg -IG9zLmZzeW5jKG91dHB1dC5maWxlbm8oKSkKICAgICAgICAgICAgICAgIG9zLnJlcGxhY2UodGVtcG9yYXJ5LCBzZWxmLnBhdGgpCiAgICAgICAgICAgIGZp -bmFsbHk6CiAgICAgICAgICAgICAgICBpZiBvcy5wYXRoLmV4aXN0cyh0ZW1wb3JhcnkpOgogICAgICAgICAgICAgICAgICAgIG9zLnVubGluayh0ZW1wb3Jh -cnkpCiAgICAgICAgICAgIHNlbGYucmVjb3JkcyA9IHJlY29yZHMKICAgICAgICAgICAgcmV0dXJuIG51bWJlcgoKCmNsYXNzIE93bmVkUHJvY2VzczoKICAg -ICIiIkEgaGFuZGxlIGZvciBhIGRhZW1vbiBzdGFydGVkIGJ5IGRidXMtbGF1bmNoIHJhdGhlciB0aGFuIGJ5IFBvcGVuLiIiIgoKICAgIGRlZiBfX2luaXRf -XyhzZWxmLCBwaWQpOgogICAgICAgIHNlbGYucGlkID0gcGlkCgogICAgZGVmIHRlcm1pbmF0ZShzZWxmKToKICAgICAgICB0cnk6CiAgICAgICAgICAgIG9z -LmtpbGwoc2VsZi5waWQsIDE1KQogICAgICAgIGV4Y2VwdCBPU0Vycm9yOgogICAgICAgICAgICBwYXNzCgogICAgZGVmIHBvbGwoc2VsZik6CiAgICAgICAg -dHJ5OgogICAgICAgICAgICBvcy5raWxsKHNlbGYucGlkLCAwKQogICAgICAgICAgICByZXR1cm4gTm9uZQogICAgICAgIGV4Y2VwdCBPU0Vycm9yOgogICAg -ICAgICAgICByZXR1cm4gMAoKCmNsYXNzIE5vVk5DVW5hdmFpbGFibGUoUnVudGltZUVycm9yKToKICAgICIiIlRoZSB2aWV3aW5nIHRyYW5zcG9ydCBmYWls -ZWQgd2hpbGUgdGhlIGd1ZXN0IGRlc2t0b3AgbWF5IGJlIGhlYWx0aHkuIiIiCgoKY2xhc3MgRGlzcGxheVNlcnZpY2U6CiAgICBkZWYgX19pbml0X18oc2Vs -ZiwgY2F0YWxvZywgcnVudGltZSwgc3RhcnRlcj1TVEFSVEVSKToKICAgICAgICBzZWxmLmNhdGFsb2cgPSBjYXRhbG9nCiAgICAgICAgc2VsZi5ydW50aW1l -ID0gUGF0aChydW50aW1lKQogICAgICAgIHNlbGYuc3RhcnRlciA9IHN0YXJ0ZXIKICAgICAgICBzZWxmLmxvY2sgPSB0aHJlYWRpbmcuTG9jaygpCiAgICAg -ICAgc2VsZi5qb2JzID0ge30KICAgICAgICBzZWxmLnN0YXRlcyA9IHt9CiAgICAgICAgc2VsZi5wcm9jZXNzZXMgPSB7fQogICAgICAgIHNlbGYubmFtZWRf -cHJvY2Vzc2VzID0ge30KICAgICAgICBzZWxmLndlYnNvY2tpZnlfcHJvY2Vzc2VzID0ge30KICAgICAgICBzZWxmLnNodXRkb3duID0gdGhyZWFkaW5nLkV2 -ZW50KCkKCiAgICBkZWYgc3RhcnQoc2VsZiwgbnVtYmVyKToKICAgICAgICB3aXRoIHNlbGYubG9jazoKICAgICAgICAgICAgaWYgbnVtYmVyIGluIHNlbGYu -am9iczoKICAgICAgICAgICAgICAgIHJldHVybiBzZWxmLmpvYnNbbnVtYmVyXQogICAgICAgICAgICBmaW5pc2hlZCA9IHRocmVhZGluZy5FdmVudCgpCiAg -ICAgICAgICAgIHNlbGYuam9ic1tudW1iZXJdID0gZmluaXNoZWQKICAgICAgICAgICAgc2VsZi5zdGF0ZXNbbnVtYmVyXSA9ICJsYXVuY2hpbmciCiAgICAg -ICAgICAgIHRocmVhZGluZy5UaHJlYWQodGFyZ2V0PXNlbGYuc3VwZXJ2aXNlLCBhcmdzPShudW1iZXIsIGZpbmlzaGVkKSwgZGFlbW9uPVRydWUpLnN0YXJ0 -KCkKICAgICAgICAgICAgcmV0dXJuIGZpbmlzaGVkCgogICAgZGVmIHRlcm1pbmF0ZShzZWxmLCBudW1iZXIpOgogICAgICAgIGZvciBwcm9jZXNzIGluIHNl -bGYucHJvY2Vzc2VzLnBvcChudW1iZXIsIFtdKToKICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgcHJvY2Vzcy50ZXJtaW5hdGUoKQogICAgICAg -ICAgICBleGNlcHQgT1NFcnJvcjoKICAgICAgICAgICAgICAgIHBhc3MKICAgICAgICBzZWxmLm5hbWVkX3Byb2Nlc3Nlcy5wb3AobnVtYmVyLCBOb25lKQog -ICAgICAgIHNlbGYudGVybWluYXRlX3dlYnNvY2tpZnkobnVtYmVyKQoKICAgIGRlZiB0ZXJtaW5hdGVfd2Vic29ja2lmeShzZWxmLCBudW1iZXIpOgogICAg -ICAgIHByb2Nlc3MgPSBzZWxmLndlYnNvY2tpZnlfcHJvY2Vzc2VzLnBvcChudW1iZXIsIE5vbmUpCiAgICAgICAgaWYgcHJvY2VzcyBpcyBOb25lOgogICAg -ICAgICAgICByZXR1cm4KICAgICAgICB0cnk6CiAgICAgICAgICAgIHByb2Nlc3MudGVybWluYXRlKCkKICAgICAgICBleGNlcHQgT1NFcnJvcjoKICAgICAg -ICAgICAgcGFzcwoKICAgIEBzdGF0aWNtZXRob2QKICAgIGRlZiBzY29wZWRfY29tcG9uZW50X3BhdGgoZXhlY3V0YWJsZSwgcGF0aCwgbnVtYmVyLCBydW50 -aW1lKToKICAgICAgICAiIiJHaXZlIHBlci1kaXNwbGF5IGRlc2t0b3AgY2xpZW50cyBuYW1lcyB0aGUgOjEgc3VwZXJ2aXNvciBjYW5ub3QgbWF0Y2guIiIi -CiAgICAgICAgc291cmNlID0gUGF0aChwYXRoKQogICAgICAgIGlmIG5vdCBzb3VyY2UuaXNfZmlsZSgpOgogICAgICAgICAgICByZXR1cm4gcGF0aAogICAg -ICAgIGRpcmVjdG9yeSA9IFBhdGgocnVudGltZSkgLyAiYmluIgogICAgICAgIGRpcmVjdG9yeS5ta2Rpcihtb2RlPTBvNzAwLCBwYXJlbnRzPVRydWUsIGV4 -aXN0X29rPVRydWUpCiAgICAgICAgdGFyZ2V0ID0gZGlyZWN0b3J5IC8gZiJjbXV4LWRpc3BsYXkte251bWJlcn0te3NvdXJjZS5uYW1lfSIKICAgICAgICBp -ZiBub3QgdGFyZ2V0LmV4aXN0cygpIG9yIHRhcmdldC5zdGF0KCkuc3RfbXRpbWVfbnMgPCBzb3VyY2Uuc3RhdCgpLnN0X210aW1lX25zOgogICAgICAgICAg -ICBzaHV0aWwuY29weTIoc291cmNlLCB0YXJnZXQpCiAgICAgICAgICAgIHRhcmdldC5jaG1vZCgwbzcwMCkKICAgICAgICByZXR1cm4gc3RyKHRhcmdldCkK -CiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgd2FpdF9mb3JfcG9ydChwb3J0LCB0aW1lb3V0PTIwKToKICAgICAgICBkZWFkbGluZSA9IHRpbWUubW9ub3Rv -bmljKCkgKyB0aW1lb3V0CiAgICAgICAgd2hpbGUgdGltZS5tb25vdG9uaWMoKSA8IGRlYWRsaW5lOgogICAgICAgICAgICBpZiBsaXN0ZW5pbmcocG9ydCk6 -CiAgICAgICAgICAgICAgICByZXR1cm4gVHJ1ZQogICAgICAgICAgICB0aW1lLnNsZWVwKDAuMSkKICAgICAgICByZXR1cm4gRmFsc2UKCiAgICBAc3RhdGlj -bWV0aG9kCiAgICBkZWYgcHJvY2Vzc19hbGl2ZShwcm9jZXNzKToKICAgICAgICBwb2xsID0gZ2V0YXR0cihwcm9jZXNzLCAicG9sbCIsIE5vbmUpCiAgICAg -ICAgcmV0dXJuIHBvbGwgaXMgTm9uZSBvciBwb2xsKCkgaXMgTm9uZQoKICAgIGRlZiBjb21wb25lbnRzX2FsaXZlKHNlbGYsIG51bWJlcik6CiAgICAgICAg -bmFtZWQgPSBzZWxmLm5hbWVkX3Byb2Nlc3Nlcy5nZXQobnVtYmVyKQogICAgICAgIGlmIG5hbWVkIGlzIG5vdCBOb25lOgogICAgICAgICAgICByZXR1cm4g -Ym9vbChuYW1lZCkgYW5kIGFsbChzZWxmLnByb2Nlc3NfYWxpdmUocHJvY2VzcykgZm9yIHByb2Nlc3MgaW4gbmFtZWQudmFsdWVzKCkpIFwKICAgICAgICAg -ICAgICAgIGFuZCAoInh2bmMiIGluIG5hbWVkIG9yIHJmYl9yZWFkeShudW1iZXIpKQogICAgICAgIHByb2Nlc3NlcyA9IHNlbGYucHJvY2Vzc2VzLmdldChu -dW1iZXIsIFtdKQogICAgICAgIHJldHVybiBib29sKHByb2Nlc3NlcykgYW5kIGFsbChzZWxmLnByb2Nlc3NfYWxpdmUocHJvY2VzcykgZm9yIHByb2Nlc3Mg -aW4gcHJvY2Vzc2VzKQoKICAgIGRlZiBzZXJ2aWNlX3JlYWR5KHNlbGYsIG51bWJlcik6CiAgICAgICAgaWYgbm90IHJlYWR5KG51bWJlcikgb3Igbm90IHNl -bGYuY29tcG9uZW50c19hbGl2ZShudW1iZXIpOgogICAgICAgICAgICByZXR1cm4gRmFsc2UKICAgICAgICB3ZWJzb2NraWZ5ID0gc2VsZi53ZWJzb2NraWZ5 -X3Byb2Nlc3Nlcy5nZXQobnVtYmVyKQogICAgICAgIHJldHVybiB3ZWJzb2NraWZ5IGlzIE5vbmUgb3Igc2VsZi5wcm9jZXNzX2FsaXZlKHdlYnNvY2tpZnkp -CgogICAgZGVmIHRyYWNrX3Byb2Nlc3Moc2VsZiwgbnVtYmVyLCBuYW1lLCBwcm9jZXNzKToKICAgICAgICBuYW1lZCA9IHNlbGYubmFtZWRfcHJvY2Vzc2Vz -LnNldGRlZmF1bHQobnVtYmVyLCB7fSkKICAgICAgICBwcmV2aW91cyA9IG5hbWVkLmdldChuYW1lKQogICAgICAgIGlmIHByZXZpb3VzIGlzIG5vdCBOb25l -OgogICAgICAgICAgICBzZWxmLnByb2Nlc3Nlc1tudW1iZXJdID0gWwogICAgICAgICAgICAgICAgaXRlbSBmb3IgaXRlbSBpbiBzZWxmLnByb2Nlc3Nlcy5n -ZXQobnVtYmVyLCBbXSkgaWYgaXRlbSBpcyBub3QgcHJldmlvdXMKICAgICAgICAgICAgXQogICAgICAgIHNlbGYucHJvY2Vzc2VzLnNldGRlZmF1bHQobnVt -YmVyLCBbXSkuYXBwZW5kKHByb2Nlc3MpCiAgICAgICAgbmFtZWRbbmFtZV0gPSBwcm9jZXNzCgogICAgZGVmIHJlbW92ZV9wcm9jZXNzKHNlbGYsIG51bWJl -ciwgbmFtZSk6CiAgICAgICAgcHJvY2VzcyA9IHNlbGYubmFtZWRfcHJvY2Vzc2VzLnNldGRlZmF1bHQobnVtYmVyLCB7fSkucG9wKG5hbWUsIE5vbmUpCiAg -ICAgICAgaWYgcHJvY2VzcyBpcyBOb25lOgogICAgICAgICAgICByZXR1cm4KICAgICAgICBzZWxmLnByb2Nlc3Nlc1tudW1iZXJdID0gW2l0ZW0gZm9yIGl0 -ZW0gaW4gc2VsZi5wcm9jZXNzZXMuZ2V0KG51bWJlciwgW10pIGlmIGl0ZW0gaXMgbm90IHByb2Nlc3NdCiAgICAgICAgdHJ5OgogICAgICAgICAgICBwcm9j -ZXNzLnRlcm1pbmF0ZSgpCiAgICAgICAgZXhjZXB0IE9TRXJyb3I6CiAgICAgICAgICAgIHBhc3MKCiAgICBkZWYgcmVjb3Zlcl9wcm9jZXNzZXMoc2VsZiwg -bnVtYmVyKToKICAgICAgICBzZWxmLnByb2Nlc3Nlc1tudW1iZXJdID0gW10KICAgICAgICBzZWxmLm5hbWVkX3Byb2Nlc3Nlc1tudW1iZXJdID0ge30KICAg -ICAgICB1aWQgPSBzdHIob3MuZ2V0dWlkKCkpCiAgICAgICAgZm9yIG5hbWUgaW4gKCJvcGVuYm94IiwgInRpbnQyIiwgInZuY2NvbmZpZyIpOgogICAgICAg -ICAgICBtYXJrZXIgPSBmImNtdXgtZGlzcGxheS17bnVtYmVyfS17bmFtZX0iCiAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgIHBhdHRlcm4gPSBy -ZiIoXnwvfCApe21hcmtlcn0oW1s6c3BhY2U6XV18JCkiCiAgICAgICAgICAgICAgICBvdXRwdXQgPSBzdWJwcm9jZXNzLmNoZWNrX291dHB1dChbInBncmVw -IiwgIi11IiwgdWlkLCAiLWYiLCBwYXR0ZXJuXSwgdGV4dD1UcnVlKQogICAgICAgICAgICAgICAgcGlkID0gaW50KG91dHB1dC5zcGxpdGxpbmVzKClbMF0p -CiAgICAgICAgICAgIGV4Y2VwdCAoT1NFcnJvciwgVmFsdWVFcnJvciwgSW5kZXhFcnJvciwgc3VicHJvY2Vzcy5DYWxsZWRQcm9jZXNzRXJyb3IpOgogICAg -ICAgICAgICAgICAgY29udGludWUKICAgICAgICAgICAgc2VsZi50cmFja19wcm9jZXNzKG51bWJlciwgbmFtZSwgT3duZWRQcm9jZXNzKHBpZCkpCiAgICAg -ICAgdHJ5OgogICAgICAgICAgICBvdXRwdXQgPSBzdWJwcm9jZXNzLmNoZWNrX291dHB1dCgKICAgICAgICAgICAgICAgIFsicGdyZXAiLCAiLXUiLCB1aWQs -ICItZiIsIHJmIig/Olh2bmN8WHRpZ2Vydm5jKS4qOntudW1iZXJ9KD86XHN8JCkiXSwKICAgICAgICAgICAgICAgIHRleHQ9VHJ1ZQogICAgICAgICAgICAp -CiAgICAgICAgICAgIHBpZCA9IGludChvdXRwdXQuc3BsaXRsaW5lcygpWzBdKQogICAgICAgIGV4Y2VwdCAoT1NFcnJvciwgVmFsdWVFcnJvciwgSW5kZXhF -cnJvciwgc3VicHJvY2Vzcy5DYWxsZWRQcm9jZXNzRXJyb3IpOgogICAgICAgICAgICBwaWQgPSAwCiAgICAgICAgaWYgcGlkID4gMToKICAgICAgICAgICAg -c2VsZi50cmFja19wcm9jZXNzKG51bWJlciwgInh2bmMiLCBPd25lZFByb2Nlc3MocGlkKSkKCiAgICBkZWYgc3RhcnRfZGJ1cyhzZWxmLCBudW1iZXIsIGVu -dmlyb25tZW50KToKICAgICAgICBidXMgPSBzdWJwcm9jZXNzLnJ1bihbImRidXMtbGF1bmNoIiwgIi0tc2gtc3ludGF4Il0sIGVudj1lbnZpcm9ubWVudCwK -ICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdGRpbj1zdWJwcm9jZXNzLkRFVk5VTEwsIHN0ZG91dD1zdWJwcm9jZXNzLlBJUEUsCiAgICAgICAgICAg -ICAgICAgICAgICAgICAgICAgc3RkZXJyPXN1YnByb2Nlc3MuREVWTlVMTCwgdGV4dD1UcnVlLCB0aW1lb3V0PTEwLAogICAgICAgICAgICAgICAgICAgICAg -ICAgICAgIGNoZWNrPVRydWUpLnN0ZG91dAogICAgICAgIGZvciBsaW5lIGluIGJ1cy5zcGxpdGxpbmVzKCk6CiAgICAgICAgICAgIGlmICI9IiBub3QgaW4g -bGluZToKICAgICAgICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgICAgIGtleSwgdmFsdWUgPSBsaW5lLnNwbGl0KCI9IiwgMSkKICAgICAgICAgICAgZW52 -aXJvbm1lbnRba2V5XSA9IHZhbHVlLnN0cmlwKCkucnN0cmlwKCI7Iikuc3RyaXAoKS5zdHJpcCgiJyIpCiAgICAgICAgdHJ5OgogICAgICAgICAgICBkYnVz -X3BpZCA9IGludChlbnZpcm9ubWVudC5nZXQoIkRCVVNfU0VTU0lPTl9CVVNfUElEIiwgIjAiKSkKICAgICAgICBleGNlcHQgVmFsdWVFcnJvcjoKICAgICAg -ICAgICAgZGJ1c19waWQgPSAwCiAgICAgICAgaWYgZGJ1c19waWQgPiAxOgogICAgICAgICAgICBzZWxmLnRyYWNrX3Byb2Nlc3MobnVtYmVyLCAiZGJ1cyIs -IE93bmVkUHJvY2VzcyhkYnVzX3BpZCkpCgogICAgZGVmIHN0YXJ0X3Nlc3Npb25fY29tcG9uZW50cyhzZWxmLCBudW1iZXIsIGVudmlyb25tZW50LCBydW50 -aW1lKToKICAgICAgICBmb3IgZXhlY3V0YWJsZSwgYXJncyBpbiBbCiAgICAgICAgICAgICgib3BlbmJveCIsIFtdKSwgKCJ0aW50MiIsIFsiLWMiLCAiL2V0 -Yy9jbXV4L3RpbnQycmMiXSksCiAgICAgICAgICAgICgidm5jY29uZmlnIiwgWyItbm93aW4iXSksCiAgICAgICAgXToKICAgICAgICAgICAgZXhpc3Rpbmcg -PSBzZWxmLm5hbWVkX3Byb2Nlc3Nlcy5nZXQobnVtYmVyLCB7fSkuZ2V0KGV4ZWN1dGFibGUpCiAgICAgICAgICAgIGlmIGV4aXN0aW5nIGlzIG5vdCBOb25l -IGFuZCBzZWxmLnByb2Nlc3NfYWxpdmUoZXhpc3RpbmcpOgogICAgICAgICAgICAgICAgY29udGludWUKICAgICAgICAgICAgcGF0aCA9IGV4ZWN1dGFibGUg -aWYgIi8iIGluIGV4ZWN1dGFibGUgZWxzZSBzaHV0aWwud2hpY2goZXhlY3V0YWJsZSkKICAgICAgICAgICAgaWYgbm90IHBhdGg6CiAgICAgICAgICAgICAg -ICBpZiBleGVjdXRhYmxlID09ICJ2bmNjb25maWciOgogICAgICAgICAgICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgICAgICAgICByYWlzZSBSdW50aW1l -RXJyb3IoZiJtaXNzaW5nIGRpc3BsYXkgY29tcG9uZW50OiB7ZXhlY3V0YWJsZX0iKQogICAgICAgICAgICBwYXRoID0gc2VsZi5zY29wZWRfY29tcG9uZW50 -X3BhdGgoZXhlY3V0YWJsZSwgcGF0aCwgbnVtYmVyLCBydW50aW1lKQogICAgICAgICAgICBwcm9jZXNzID0gc3VicHJvY2Vzcy5Qb3BlbihbcGF0aF0gKyBh -cmdzLCBlbnY9ZW52aXJvbm1lbnQsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0ZGluPXN1YnByb2Nlc3MuREVWTlVMTCwKICAg -ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3Rkb3V0PXN1YnByb2Nlc3MuREVWTlVMTCwgc3RkZXJyPXN1YnByb2Nlc3MuREVWTlVMTCkK -ICAgICAgICAgICAgc2VsZi50cmFja19wcm9jZXNzKG51bWJlciwgZXhlY3V0YWJsZSwgcHJvY2VzcykKCiAgICBkZWYgc3RhcnRfd2Vic29ja2lmeShzZWxm -LCBudW1iZXIsIGVudmlyb25tZW50LCBleGVjdXRhYmxlKToKICAgICAgICBwYXRoID0gZXhlY3V0YWJsZSBpZiAiLyIgaW4gZXhlY3V0YWJsZSBlbHNlIHNo -dXRpbC53aGljaChleGVjdXRhYmxlKQogICAgICAgIGlmIG5vdCBwYXRoOgogICAgICAgICAgICByYWlzZSBOb1ZOQ1VuYXZhaWxhYmxlKCJtaXNzaW5nIGRp -c3BsYXkgY29tcG9uZW50OiB3ZWJzb2NraWZ5IikKICAgICAgICBzZWxmLnRlcm1pbmF0ZV93ZWJzb2NraWZ5KG51bWJlcikKICAgICAgICByZmJfcG9ydCwg -bm92bmNfcG9ydCA9IDU5MDAgKyBudW1iZXIsIDY5MDAgKyBudW1iZXIKICAgICAgICBwcm9jZXNzID0gc3VicHJvY2Vzcy5Qb3BlbigKICAgICAgICAgICAg -W3BhdGgsICItLXdlYiIsICIvdXNyL3NoYXJlL25vdm5jIiwgIi0taGVhcnRiZWF0IiwgIjMwIiwKICAgICAgICAgICAgIGYiMTI3LjAuMC4xOntub3ZuY19w -b3J0fSIsIGYiMTI3LjAuMC4xOntyZmJfcG9ydH0iXSwKICAgICAgICAgICAgZW52PWVudmlyb25tZW50LCBzdGRpbj1zdWJwcm9jZXNzLkRFVk5VTEwsCiAg -ICAgICAgICAgIHN0ZG91dD1zdWJwcm9jZXNzLkRFVk5VTEwsIHN0ZGVycj1zdWJwcm9jZXNzLkRFVk5VTEwpCiAgICAgICAgc2VsZi53ZWJzb2NraWZ5X3By -b2Nlc3Nlc1tudW1iZXJdID0gcHJvY2VzcwogICAgICAgIGlmIG5vdCBzZWxmLndhaXRfZm9yX3BvcnQobm92bmNfcG9ydCwgdGltZW91dD0xMCk6CiAgICAg -ICAgICAgIHNlbGYudGVybWluYXRlX3dlYnNvY2tpZnkobnVtYmVyKQogICAgICAgICAgICByYWlzZSBOb1ZOQ1VuYXZhaWxhYmxlKCJub1ZOQyBkaWQgbm90 -IGJlY29tZSByZWFkeSIpCgogICAgZGVmIHN0YXJ0X2NvbXBvbmVudHMoc2VsZiwgbnVtYmVyLCBlbnZpcm9ubWVudCwgcnVudGltZSk6CiAgICAgICAgaWYg -c2VsZi5zZXJ2aWNlX3JlYWR5KG51bWJlcik6CiAgICAgICAgICAgIHJldHVybgogICAgICAgIHJmYl9pc19yZWFkeSA9IHJmYl9yZWFkeShudW1iZXIpCiAg -ICAgICAgdm5jID0gc2h1dGlsLndoaWNoKCJYdm5jIikgb3Igc2h1dGlsLndoaWNoKCJYdGlnZXJ2bmMiKQogICAgICAgIHdlYnNvY2tpZnkgPSBzaHV0aWwu -d2hpY2goIndlYnNvY2tpZnkiKQogICAgICAgIGlmIG5vdCB3ZWJzb2NraWZ5OgogICAgICAgICAgICByYWlzZSBOb1ZOQ1VuYXZhaWxhYmxlKCJkaXNwbGF5 -IHJ1bnRpbWUgdW5hdmFpbGFibGUiKQogICAgICAgIGlmIG5vdCByZmJfaXNfcmVhZHkgYW5kIG5vdCB2bmM6CiAgICAgICAgICAgIHJhaXNlIFJ1bnRpbWVF -cnJvcigiZGlzcGxheSBydW50aW1lIHVuYXZhaWxhYmxlIikKICAgICAgICBpZiByZmJfaXNfcmVhZHkgYW5kIG51bWJlciBub3QgaW4gc2VsZi5uYW1lZF9w -cm9jZXNzZXM6CiAgICAgICAgICAgIHNlbGYucmVjb3Zlcl9wcm9jZXNzZXMobnVtYmVyKQogICAgICAgIGlmIG5vdCByZmJfaXNfcmVhZHk6CiAgICAgICAg -ICAgIHNlbGYudGVybWluYXRlKG51bWJlcikKICAgICAgICAgICAgcmZiX3BvcnQgPSA1OTAwICsgbnVtYmVyCiAgICAgICAgICAgIHNlbGYucHJvY2Vzc2Vz -W251bWJlcl0gPSBbXQogICAgICAgICAgICBzZWxmLm5hbWVkX3Byb2Nlc3Nlc1tudW1iZXJdID0ge30KICAgICAgICAgICAgc2VsZi50cmFja19wcm9jZXNz -KG51bWJlciwgInh2bmMiLCBzdWJwcm9jZXNzLlBvcGVuKAogICAgICAgICAgICAgICAgW3ZuYywgZiI6e251bWJlcn0iLCAiLWdlb21ldHJ5Iiwgb3MuZW52 -aXJvbi5nZXQoIkNNVVhfVk5DX0dFT01FVFJZIiwgIjE0NDB4OTAwIiksCiAgICAgICAgICAgICAgICAgIi1kZXB0aCIsICIyNCIsICItcmZicG9ydCIsIHN0 -cihyZmJfcG9ydCksICItbG9jYWxob3N0IiwKICAgICAgICAgICAgICAgICAiLVNlY3VyaXR5VHlwZXMiLCAiTm9uZSIsICItQWx3YXlzU2hhcmVkIl0sCiAg -ICAgICAgICAgICAgICBlbnY9ZW52aXJvbm1lbnQsIHN0ZGluPXN1YnByb2Nlc3MuREVWTlVMTCwKICAgICAgICAgICAgICAgIHN0ZG91dD1zdWJwcm9jZXNz -LkRFVk5VTEwsIHN0ZGVycj1zdWJwcm9jZXNzLkRFVk5VTEwpKQogICAgICAgICAgICBpZiBub3Qgc2VsZi53YWl0X2Zvcl9wb3J0KHJmYl9wb3J0KToKICAg -ICAgICAgICAgICAgIHJhaXNlIFJ1bnRpbWVFcnJvcigiWHZuYyBkaWQgbm90IGJlY29tZSByZWFkeSIpCiAgICAgICAgICAgIHNlbGYuc3RhcnRfZGJ1cyhu -dW1iZXIsIGVudmlyb25tZW50KQogICAgICAgICAgICBzZWxmLnN0YXJ0X3Nlc3Npb25fY29tcG9uZW50cyhudW1iZXIsIGVudmlyb25tZW50LCBydW50aW1l -KQogICAgICAgIGVsaWYgbnVtYmVyIGluIHNlbGYubmFtZWRfcHJvY2Vzc2VzOgogICAgICAgICAgICBuYW1lZCA9IHNlbGYubmFtZWRfcHJvY2Vzc2VzW251 -bWJlcl0KICAgICAgICAgICAgZGJ1cyA9IG5hbWVkLmdldCgiZGJ1cyIpCiAgICAgICAgICAgIGlmIGRidXMgaXMgTm9uZSBvciBub3Qgc2VsZi5wcm9jZXNz -X2FsaXZlKGRidXMpOgogICAgICAgICAgICAgICAgZm9yIG5hbWUgaW4gKCJvcGVuYm94IiwgInRpbnQyIiwgInZuY2NvbmZpZyIpOgogICAgICAgICAgICAg -ICAgICAgIHNlbGYucmVtb3ZlX3Byb2Nlc3MobnVtYmVyLCBuYW1lKQogICAgICAgICAgICAgICAgc2VsZi5zdGFydF9kYnVzKG51bWJlciwgZW52aXJvbm1l -bnQpCiAgICAgICAgICAgIHNlbGYuc3RhcnRfc2Vzc2lvbl9jb21wb25lbnRzKG51bWJlciwgZW52aXJvbm1lbnQsIHJ1bnRpbWUpCiAgICAgICAgd2Vic29j -a2lmeV9wcm9jZXNzID0gc2VsZi53ZWJzb2NraWZ5X3Byb2Nlc3Nlcy5nZXQobnVtYmVyKQogICAgICAgIGlmICh3ZWJzb2NraWZ5X3Byb2Nlc3MgaXMgTm9u -ZSBvciBub3Qgc2VsZi5wcm9jZXNzX2FsaXZlKHdlYnNvY2tpZnlfcHJvY2VzcykKICAgICAgICAgICAgICAgIG9yIG5vdCBub3ZuY19yZWFkeShudW1iZXIp -KToKICAgICAgICAgICAgc2VsZi5zdGFydF93ZWJzb2NraWZ5KG51bWJlciwgZW52aXJvbm1lbnQsIHdlYnNvY2tpZnkpCiAgICAgICAgKHJ1bnRpbWUgLyAi -ZW52Iikud3JpdGVfdGV4dChmImV4cG9ydCBESVNQTEFZPTp7bnVtYmVyfVxuIikKCiAgICBkZWYgc3VwZXJ2aXNlKHNlbGYsIG51bWJlciwgZmluaXNoZWQp -OgogICAgICAgIHJ1bnRpbWUgPSBzZWxmLnJ1bnRpbWUgLyBzdHIobnVtYmVyKQogICAgICAgIHJ1bnRpbWUubWtkaXIobW9kZT0wbzcwMCwgcGFyZW50cz1U -cnVlLCBleGlzdF9vaz1UcnVlKQogICAgICAgIGVudmlyb25tZW50ID0gZGljdChvcy5lbnZpcm9uLCBESVNQTEFZPWYiOntudW1iZXJ9IiwgQ01VWF9ERVNL -VE9QX1JVTlRJTUVfRElSPXN0cihydW50aW1lKSkKICAgICAgICBlbnZpcm9ubWVudFsiQ0hST01FX0NPTkZJR19IT01FIl0gPSBzdHIocnVudGltZSAvICJj -aHJvbWUtY29uZmlnIikKICAgICAgICBlbnZpcm9ubWVudFsiWERHX0NPTkZJR19IT01FIl0gPSBzdHIocnVudGltZSAvICJ4ZGctY29uZmlnIikKICAgICAg -ICBmb3Iga2V5IGluICgiTk9USUZZX1NPQ0tFVCIsICJEQlVTX1NFU1NJT05fQlVTX0FERFJFU1MiLCAiREJVU19TRVNTSU9OX0JVU19QSUQiLAogICAgICAg -ICAgICAgICAgICAgICJBVF9TUElfQlVTX0FERFJFU1MiLCAiQVRfU1BJX0JVUyIpOgogICAgICAgICAgICBlbnZpcm9ubWVudC5wb3Aoa2V5LCBOb25lKQog -ICAgICAgIHdoaWxlIG5vdCBzZWxmLnNodXRkb3duLmlzX3NldCgpOgogICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICAjIFRoZSBzdXBlcnZpc29y -IGlzIGEgaGVhbHRoIGNoZWNrLCBub3QgYSB0aW1lci1kcml2ZW4gcmVzdGFydC4KICAgICAgICAgICAgICAgICMgUHJlc2VydmUgYW4gYWN0aXZlIFJGQi9u -b1ZOQyBzZXNzaW9uIHVudGlsIGl0cyBvd25lZCBlbmRwb2ludHMgZmFpbC4KICAgICAgICAgICAgICAgIGlmIG5vdCBzZWxmLnNlcnZpY2VfcmVhZHkobnVt -YmVyKToKICAgICAgICAgICAgICAgICAgICBzZWxmLnN0YXJ0X2NvbXBvbmVudHMobnVtYmVyLCBlbnZpcm9ubWVudCwgcnVudGltZSkKICAgICAgICAgICAg -ICAgIHN0YXRlID0gInJ1bm5pbmciIGlmIHNlbGYuc2VydmljZV9yZWFkeShudW1iZXIpIGVsc2UgInVuYXZhaWxhYmxlIgogICAgICAgICAgICBleGNlcHQg -Tm9WTkNVbmF2YWlsYWJsZToKICAgICAgICAgICAgICAgIHNlbGYudGVybWluYXRlX3dlYnNvY2tpZnkobnVtYmVyKQogICAgICAgICAgICAgICAgc3RhdGUg -PSAidW5hdmFpbGFibGUiCiAgICAgICAgICAgIGV4Y2VwdCAoT1NFcnJvciwgUnVudGltZUVycm9yLCBzdWJwcm9jZXNzLlN1YnByb2Nlc3NFcnJvcik6CiAg -ICAgICAgICAgICAgICBzZWxmLnRlcm1pbmF0ZShudW1iZXIpCiAgICAgICAgICAgICAgICBzdGF0ZSA9ICJ1bmF2YWlsYWJsZSIKICAgICAgICAgICAgd2l0 -aCBzZWxmLmxvY2s6CiAgICAgICAgICAgICAgICBzZWxmLnN0YXRlc1tudW1iZXJdID0gc3RhdGUKICAgICAgICAgICAgZmluaXNoZWQuc2V0KCkKICAgICAg -ICAgICAgc2VsZi5zaHV0ZG93bi53YWl0KDMwKQogICAgICAgIHNlbGYudGVybWluYXRlKG51bWJlcikKCiAgICBkZWYgc25hcHNob3Qoc2VsZik6CiAgICAg -ICAgd2l0aCBzZWxmLmxvY2s6CiAgICAgICAgICAgIHJldHVybiB7InZlcnNpb24iOiAxLCAiY2FuQ3JlYXRlIjogbGVuKHNlbGYuam9icykgPCBNQVhfRElT -UExBWVMgLSAxLAogICAgICAgICAgICAgICAgICAgICJkaXNwbGF5cyI6IFtkZXNjcmlwdG9yKDEsICJydW5uaW5nIiBpZiByZWFkeSgxKSBlbHNlICJ1bmF2 -YWlsYWJsZSIpXQogICAgICAgICAgICAgICAgICAgICsgW2Rlc2NyaXB0b3Iobiwgc2VsZi5zdGF0ZXMuZ2V0KG4sICJsYXVuY2hpbmciKSkgZm9yIG4gaW4g -c2VsZi5jYXRhbG9nLm51bWJlcnMoKV19CgogICAgZGVmIGhhbmRsZShzZWxmLCByZXF1ZXN0KToKICAgICAgICBpZiByZXF1ZXN0LmdldCgiYWN0aW9uIikg -PT0gImxpc3QiOgogICAgICAgICAgICByZXR1cm4gc2VsZi5zbmFwc2hvdCgpCiAgICAgICAgaWYgcmVxdWVzdC5nZXQoImFjdGlvbiIpICE9ICJjcmVhdGUi -OgogICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJ1bnN1cHBvcnRlZCBkaXNwbGF5IGFjdGlvbiIpCiAgICAgICAgbnVtYmVyID0gc2VsZi5jYXRhbG9n -LmFsbG9jYXRlKHJlcXVlc3QuZ2V0KCJyZXF1ZXN0IikpCiAgICAgICAgc2VsZi5zdGFydChudW1iZXIpLndhaXQoNTUpCiAgICAgICAgcmVzdWx0ID0gc2Vs -Zi5zbmFwc2hvdCgpCiAgICAgICAgcmVzdWx0WyJjcmVhdGVkIl0gPSBmImRpc3BsYXk6e251bWJlcn0iCiAgICAgICAgaWYgc2VsZi5zdGF0ZXMuZ2V0KG51 -bWJlcikgIT0gInJ1bm5pbmciOgogICAgICAgICAgICByZXN1bHRbImVycm9yIl0gPSAiZGlzcGxheV9zdGFydF9mYWlsZWQiCiAgICAgICAgcmV0dXJuIHJl -c3VsdAoKCmRlZiBzZXJ2ZShydW50aW1lKToKICAgIHJ1bnRpbWUgPSBQYXRoKHJ1bnRpbWUpCiAgICBydW50aW1lLm1rZGlyKG1vZGU9MG83MDAsIHBhcmVu -dHM9VHJ1ZSwgZXhpc3Rfb2s9VHJ1ZSkKICAgIHdpdGggKHJ1bnRpbWUgLyAiZGlzcGxheS1jb250cm9sLmxvY2siKS5vcGVuKCJ3IikgYXMgbG9jazoKICAg -ICAgICB0cnk6CiAgICAgICAgICAgIGZjbnRsLmZsb2NrKGxvY2ssIGZjbnRsLkxPQ0tfRVggfCBmY250bC5MT0NLX05CKQogICAgICAgIGV4Y2VwdCBCbG9j -a2luZ0lPRXJyb3I6CiAgICAgICAgICAgIHJldHVybgogICAgICAgIGlmIG5vdCBhbGwoc2h1dGlsLndoaWNoKG5hbWUpIGZvciBuYW1lIGluICgid2Vic29j -a2lmeSIsICJvcGVuYm94IiwgImRidXMtbGF1bmNoIikpOgogICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJkaXNwbGF5IHJ1bnRpbWUgdW5hdmFpbGFi -bGUiKQogICAgICAgIGlmIG5vdCAoc2h1dGlsLndoaWNoKCJYdm5jIikgb3Igc2h1dGlsLndoaWNoKCJYdGlnZXJ2bmMiKSk6CiAgICAgICAgICAgIHJhaXNl -IFZhbHVlRXJyb3IoImRpc3BsYXkgcnVudGltZSB1bmF2YWlsYWJsZSIpCiAgICAgICAgc2VydmljZSA9IERpc3BsYXlTZXJ2aWNlKERpc3BsYXlDYXRhbG9n -KFBhdGguaG9tZSgpIC8gIi5jbXV4IiAvICJkaXNwbGF5cyIpLCBydW50aW1lIC8gImRpc3BsYXlzIikKICAgICAgICBmb3IgbnVtYmVyIGluIHNlcnZpY2Uu -Y2F0YWxvZy5udW1iZXJzKCk6CiAgICAgICAgICAgIHNlcnZpY2Uuc3RhcnQobnVtYmVyKQogICAgICAgIHBhdGggPSBydW50aW1lIC8gImRpc3BsYXktY29u -dHJvbC5zb2NrIgogICAgICAgIGlmIHBhdGguZXhpc3RzKCk6CiAgICAgICAgICAgIHBhdGgudW5saW5rKCkKCiAgICAgICAgY2xhc3MgSGFuZGxlcihzb2Nr -ZXRzZXJ2ZXIuU3RyZWFtUmVxdWVzdEhhbmRsZXIpOgogICAgICAgICAgICBkZWYgaGFuZGxlKHNlbGYpOgogICAgICAgICAgICAgICAgc2VsZi5jb25uZWN0 -aW9uLnNldHRpbWVvdXQoNjApCiAgICAgICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICAgICAgcmF3ID0gc2VsZi5yZmlsZS5yZWFkbGluZSg0MDk3 -KQogICAgICAgICAgICAgICAgICAgIGlmIGxlbihyYXcpID4gNDA5NjoKICAgICAgICAgICAgICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJvcigiZGlzcGxh -eSByZXF1ZXN0IHRvbyBsYXJnZSIpCiAgICAgICAgICAgICAgICAgICAgcmVzdWx0ID0gc2VydmljZS5oYW5kbGUoanNvbi5sb2FkcyhyYXcpKQogICAgICAg -ICAgICAgICAgZXhjZXB0IChWYWx1ZUVycm9yLCBLZXlFcnJvciwgVHlwZUVycm9yLCBPU0Vycm9yKToKICAgICAgICAgICAgICAgICAgICByZXN1bHQgPSB7 -ImVycm9yIjogImRpc3BsYXlfcmVxdWVzdF9mYWlsZWQifQogICAgICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgIHNlbGYud2ZpbGUud3Jp -dGUoanNvbi5kdW1wcyhyZXN1bHQpLmVuY29kZSgpICsgYiJcbiIpCiAgICAgICAgICAgICAgICBleGNlcHQgT1NFcnJvcjoKICAgICAgICAgICAgICAgICAg -ICBwYXNzICAjIFRoZSBwZXJzaXN0ZWQgcmVjZWlwdCByZW1haW5zIHJlcGxheWFibGUgYWZ0ZXIgZGlzY29ubmVjdC4KCiAgICAgICAgY2xhc3MgU2VydmVy -KHNvY2tldHNlcnZlci5UaHJlYWRpbmdVbml4U3RyZWFtU2VydmVyKToKICAgICAgICAgICAgZGFlbW9uX3RocmVhZHMgPSBUcnVlCgogICAgICAgIHByZXZp -b3VzX3VtYXNrID0gb3MudW1hc2soMG8wNzcpCiAgICAgICAgdHJ5OgogICAgICAgICAgICB3aXRoIFNlcnZlcihzdHIocGF0aCksIEhhbmRsZXIpIGFzIHNl -cnZlcjoKICAgICAgICAgICAgICAgIG9zLnVtYXNrKHByZXZpb3VzX3VtYXNrKQogICAgICAgICAgICAgICAgc2VydmVyLnNlcnZlX2ZvcmV2ZXIoKQogICAg -ICAgIGZpbmFsbHk6CiAgICAgICAgICAgIG9zLnVtYXNrKHByZXZpb3VzX3VtYXNrKQogICAgICAgICAgICBzZXJ2aWNlLnNodXRkb3duLnNldCgpCiAgICAg -ICAgICAgIHBhdGgudW5saW5rKG1pc3Npbmdfb2s9VHJ1ZSkKCgpkZWYgbWFpbigpOgogICAgcGFyc2VyID0gYXJncGFyc2UuQXJndW1lbnRQYXJzZXIoZGVz -Y3JpcHRpb249X19kb2NfXykKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoImFjdGlvbiIsIGNob2ljZXM9WyJsaXN0IiwgImNyZWF0ZSIsICJzZXJ2ZSJdKQog -ICAgcGFyc2VyLmFkZF9hcmd1bWVudCgiLS1yZXF1ZXN0LWlkIikKICAgIGFyZ3MgPSBwYXJzZXIucGFyc2VfYXJncygpCiAgICBydW50aW1lID0gb3MuZW52 -aXJvbi5nZXQoIkNNVVhfREVTS1RPUF9SVU5USU1FX0RJUiIsICIvcnVuL2NtdXgtZGVza3RvcCIpCiAgICBpZiBhcmdzLmFjdGlvbiA9PSAic2VydmUiOgog -ICAgICAgIHNlcnZlKHJ1bnRpbWUpCiAgICAgICAgcmV0dXJuIDAKICAgIGlmIGFyZ3MuYWN0aW9uID09ICJjcmVhdGUiIGFuZCBub3QgYXJncy5yZXF1ZXN0 -X2lkOgogICAgICAgIHBhcnNlci5lcnJvcigiY3JlYXRlIHJlcXVpcmVzIC0tcmVxdWVzdC1pZCAocmV0YWluIGl0IHdoZW4gcmV0cnlpbmcpIikKICAgIHRy -eToKICAgICAgICB3aXRoIHNvY2tldC5zb2NrZXQoc29ja2V0LkFGX1VOSVgsIHNvY2tldC5TT0NLX1NUUkVBTSkgYXMgY2xpZW50OgogICAgICAgICAgICBj -bGllbnQuc2V0dGltZW91dCg2MCkKICAgICAgICAgICAgY2xpZW50LmNvbm5lY3Qoc3RyKFBhdGgocnVudGltZSkgLyAiZGlzcGxheS1jb250cm9sLnNvY2si -KSkKICAgICAgICAgICAgY2xpZW50LnNlbmRhbGwoanNvbi5kdW1wcyh7ImFjdGlvbiI6IGFyZ3MuYWN0aW9uLCAicmVxdWVzdCI6IGFyZ3MucmVxdWVzdF9p -ZH0pLmVuY29kZSgpICsgYiJcbiIpCiAgICAgICAgICAgIHdpdGggY2xpZW50Lm1ha2VmaWxlKCJyYiIpIGFzIHN0cmVhbToKICAgICAgICAgICAgICAgIHJl -c3VsdCA9IGpzb24ubG9hZHMoc3RyZWFtLnJlYWRsaW5lKDY1NTM2KSkKICAgIGV4Y2VwdCAoT1NFcnJvciwgVmFsdWVFcnJvcik6CiAgICAgICAgcmVzdWx0 -ID0geyJlcnJvciI6ICJkaXNwbGF5X3J1bnRpbWVfdW5hdmFpbGFibGUifQogICAgcHJpbnQoanNvbi5kdW1wcyhyZXN1bHQpKQogICAgcmV0dXJuIDEgaWYg -ImVycm9yIiBpbiByZXN1bHQgZWxzZSAwCgoKaWYgX19uYW1lX18gPT0gIl9fbWFpbl9fIjoKICAgIHJhaXNlIFN5c3RlbUV4aXQobWFpbigpKQo= +aW1wb3J0IGh0dHAuY2xpZW50CmltcG9ydCBqc29uCmltcG9ydCBvcwpmcm9tIHBhdGhsaWIgaW1wb3J0IFBhdGgKaW1wb3J0IHJlCmltcG9ydCBzaHV0aWwK +aW1wb3J0IHNvY2tldAppbXBvcnQgc29ja2V0c2VydmVyCmltcG9ydCBzdWJwcm9jZXNzCmltcG9ydCB0ZW1wZmlsZQppbXBvcnQgdGhyZWFkaW5nCmltcG9y +dCB0aW1lCmltcG9ydCB1dWlkCgpNQVhfRElTUExBWVMgPSAxNgpTVEFSVEVSID0gIi91c3IvbG9jYWwvYmluL3N0YXJ0LXZuYy5zaCIKCgpkZWYgZGVzY3Jp +cHRvcihudW1iZXIsIHN0YXRlPSJydW5uaW5nIik6CiAgICByZXR1cm4geyJpZCI6IGYiZGlzcGxheTp7bnVtYmVyfSIsICJudW1iZXIiOiBudW1iZXIsCiAg +ICAgICAgICAgICJwb3J0IjogNjkwMCArIG51bWJlciwgInN0YXRlIjogc3RhdGV9CgoKZGVmIGxpc3RlbmluZyhwb3J0KToKICAgIHRyeToKICAgICAgICB3 +aXRoIHNvY2tldC5jcmVhdGVfY29ubmVjdGlvbigoIjEyNy4wLjAuMSIsIHBvcnQpLCB0aW1lb3V0PTAuMyk6CiAgICAgICAgICAgIHJldHVybiBUcnVlCiAg +ICBleGNlcHQgT1NFcnJvcjoKICAgICAgICByZXR1cm4gRmFsc2UKCgpkZWYgcmZiX3JlYWR5KG51bWJlcik6CiAgICAiIiJBIGJvdW5kIEhUVFAgcG9ydCBh +bG9uZSBkb2VzIG5vdCBwcm92ZSB0aGF0IGFuIFgvUkZCIHNlcnZlciBleGlzdHMuIiIiCiAgICB0cnk6CiAgICAgICAgd2l0aCBzb2NrZXQuY3JlYXRlX2Nv +bm5lY3Rpb24oKCIxMjcuMC4wLjEiLCA1OTAwICsgbnVtYmVyKSwgdGltZW91dD0wLjUpIGFzIHJmYjoKICAgICAgICAgICAgdmVyc2lvbiA9IGIiIgogICAg +ICAgICAgICB3aGlsZSBsZW4odmVyc2lvbikgPCAxMjoKICAgICAgICAgICAgICAgIHBhcnQgPSByZmIucmVjdigxMiAtIGxlbih2ZXJzaW9uKSkKICAgICAg +ICAgICAgICAgIGlmIG5vdCBwYXJ0OgogICAgICAgICAgICAgICAgICAgIHJldHVybiBGYWxzZQogICAgICAgICAgICAgICAgdmVyc2lvbiArPSBwYXJ0CiAg +ICAgICAgICAgIGlmIG5vdCB2ZXJzaW9uLnN0YXJ0c3dpdGgoYiJSRkIgIik6CiAgICAgICAgICAgICAgICByZXR1cm4gRmFsc2UKICAgIGV4Y2VwdCAoT1NF +cnJvciwgaHR0cC5jbGllbnQuSFRUUEV4Y2VwdGlvbik6CiAgICAgICAgcmV0dXJuIEZhbHNlCiAgICByZXR1cm4gVHJ1ZQoKCmRlZiBub3ZuY19yZWFkeShu +dW1iZXIpOgogICAgdHJ5OgogICAgICAgIGNvbm5lY3Rpb24gPSBodHRwLmNsaWVudC5IVFRQQ29ubmVjdGlvbigiMTI3LjAuMC4xIiwgNjkwMCArIG51bWJl +ciwgdGltZW91dD0wLjUpCiAgICAgICAgdHJ5OgogICAgICAgICAgICBjb25uZWN0aW9uLnJlcXVlc3QoIkhFQUQiLCAiL3ZuYy5odG1sIikKICAgICAgICAg +ICAgcmV0dXJuIGNvbm5lY3Rpb24uZ2V0cmVzcG9uc2UoKS5zdGF0dXMgPT0gMjAwCiAgICAgICAgZmluYWxseToKICAgICAgICAgICAgY29ubmVjdGlvbi5j +bG9zZSgpCiAgICBleGNlcHQgKE9TRXJyb3IsIGh0dHAuY2xpZW50LkhUVFBFeGNlcHRpb24pOgogICAgICAgIHJldHVybiBGYWxzZQoKCmRlZiByZWFkeShu +dW1iZXIpOgogICAgIiIiQSBib3VuZCBlbmRwb2ludCBhbG9uZSBkb2VzIG5vdCBwcm92ZSB0aGF0IHRoZSBjb21wbGV0ZSBkZXNrdG9wIGV4aXN0cy4iIiIK +ICAgIHJldHVybiByZmJfcmVhZHkobnVtYmVyKSBhbmQgbm92bmNfcmVhZHkobnVtYmVyKQoKCmNsYXNzIERpc3BsYXlDYXRhbG9nOgogICAgIiIiU2VyaWFs +aXplcyBhbGxvY2F0aW9uIGFuZCBwZXJzaXN0cyByZXF1ZXN0IHJlY2VpcHRzIGJlZm9yZSBzdGFydGluZyB3b3JrLiIiIgoKICAgIGRlZiBfX2luaXRfXyhz +ZWxmLCBkaXJlY3RvcnksIG9jY3VwaWVkPU5vbmUpOgogICAgICAgIHNlbGYuZGlyZWN0b3J5ID0gUGF0aChkaXJlY3RvcnkpCiAgICAgICAgc2VsZi5kaXJl +Y3RvcnkubWtkaXIobW9kZT0wbzcwMCwgcGFyZW50cz1UcnVlLCBleGlzdF9vaz1UcnVlKQogICAgICAgIHNlbGYucGF0aCA9IHNlbGYuZGlyZWN0b3J5IC8g +ImNhdGFsb2cuanNvbiIKICAgICAgICBzZWxmLmxvY2sgPSB0aHJlYWRpbmcuUkxvY2soKQogICAgICAgIHNlbGYub2NjdXBpZWQgPSBvY2N1cGllZCBvciAo +bGFtYmRhIG46IFBhdGgoZiIvdG1wLy5YMTEtdW5peC9Ye259IikuZXhpc3RzKCkKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG9yIGxp +c3RlbmluZyg1OTAwICsgbikgb3IgbGlzdGVuaW5nKDY5MDAgKyBuKSkKICAgICAgICBzZWxmLnJlY29yZHMgPSBbXQogICAgICAgIGlmIHNlbGYucGF0aC5l +eGlzdHMoKToKICAgICAgICAgICAgZGF0YSA9IGpzb24ubG9hZHMoc2VsZi5wYXRoLnJlYWRfdGV4dCgpKQogICAgICAgICAgICBpZiBkYXRhLmdldCgidmVy +c2lvbiIpICE9IDE6CiAgICAgICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJ1bnN1cHBvcnRlZCBkaXNwbGF5IGNhdGFsb2cgdmVyc2lvbiIpCiAgICAg +ICAgICAgIHNlZW5fbnVtYmVycywgc2Vlbl9yZXF1ZXN0cyA9IHNldCgpLCBzZXQoKQogICAgICAgICAgICBmb3IgcmVjb3JkIGluIGRhdGFbImRpc3BsYXlz +Il06CiAgICAgICAgICAgICAgICBudW1iZXIsIHJlcXVlc3QgPSByZWNvcmRbIm51bWJlciJdLCByZWNvcmRbInJlcXVlc3QiXQogICAgICAgICAgICAgICAg +aWYgdHlwZShudW1iZXIpIGlzIG5vdCBpbnQgb3Igbm90IDIgPD0gbnVtYmVyIDw9IE1BWF9ESVNQTEFZUzoKICAgICAgICAgICAgICAgICAgICByYWlzZSBW +YWx1ZUVycm9yKCJpbnZhbGlkIGRpc3BsYXkgbnVtYmVyIikKICAgICAgICAgICAgICAgIGlmIHN0cih1dWlkLlVVSUQocmVxdWVzdCkpICE9IHJlcXVlc3Qg +b3IgbnVtYmVyIGluIHNlZW5fbnVtYmVycyBvciByZXF1ZXN0IGluIHNlZW5fcmVxdWVzdHM6CiAgICAgICAgICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJv +cigiaW52YWxpZCBkaXNwbGF5IGlkZW50aXR5IikKICAgICAgICAgICAgICAgIHNlZW5fbnVtYmVycy5hZGQobnVtYmVyKQogICAgICAgICAgICAgICAgc2Vl +bl9yZXF1ZXN0cy5hZGQocmVxdWVzdCkKICAgICAgICAgICAgICAgIHNlbGYucmVjb3Jkcy5hcHBlbmQoeyJudW1iZXIiOiBudW1iZXIsICJyZXF1ZXN0Ijog +cmVxdWVzdH0pCgogICAgZGVmIG51bWJlcnMoc2VsZik6CiAgICAgICAgd2l0aCBzZWxmLmxvY2s6CiAgICAgICAgICAgIHJldHVybiBzb3J0ZWQocmVjb3Jk +WyJudW1iZXIiXSBmb3IgcmVjb3JkIGluIHNlbGYucmVjb3JkcykKCiAgICBkZWYgYWxsb2NhdGUoc2VsZiwgcmVxdWVzdCk6CiAgICAgICAgaWYgbm90IGlz +aW5zdGFuY2UocmVxdWVzdCwgc3RyKSBvciBzdHIodXVpZC5VVUlEKHJlcXVlc3QpKSAhPSByZXF1ZXN0OgogICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9y +KCJpbnZhbGlkIHJlcXVlc3QgaWRlbnRpdHkiKQogICAgICAgIHdpdGggc2VsZi5sb2NrOgogICAgICAgICAgICBmb3IgcmVjb3JkIGluIHNlbGYucmVjb3Jk +czoKICAgICAgICAgICAgICAgIGlmIHJlY29yZFsicmVxdWVzdCJdID09IHJlcXVlc3Q6CiAgICAgICAgICAgICAgICAgICAgcmV0dXJuIHJlY29yZFsibnVt +YmVyIl0KICAgICAgICAgICAgdXNlZCA9IHNldChzZWxmLm51bWJlcnMoKSkKICAgICAgICAgICAgbnVtYmVyID0gbmV4dCgobiBmb3IgbiBpbiByYW5nZSgy +LCBNQVhfRElTUExBWVMgKyAxKQogICAgICAgICAgICAgICAgICAgICAgICAgICBpZiBuIG5vdCBpbiB1c2VkIGFuZCBub3Qgc2VsZi5vY2N1cGllZChuKSks +IE5vbmUpCiAgICAgICAgICAgIGlmIG51bWJlciBpcyBOb25lOgogICAgICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJvcigiZGlzcGxheSBjYXBhY2l0eSBl +eGhhdXN0ZWQiKQogICAgICAgICAgICByZWNvcmRzID0gc2VsZi5yZWNvcmRzICsgW3sibnVtYmVyIjogbnVtYmVyLCAicmVxdWVzdCI6IHJlcXVlc3R9XQog +ICAgICAgICAgICBmZCwgdGVtcG9yYXJ5ID0gdGVtcGZpbGUubWtzdGVtcChwcmVmaXg9ImNhdGFsb2ctIiwgZGlyPXNlbGYuZGlyZWN0b3J5KQogICAgICAg +ICAgICB0cnk6CiAgICAgICAgICAgICAgICB3aXRoIG9zLmZkb3BlbihmZCwgInciKSBhcyBvdXRwdXQ6CiAgICAgICAgICAgICAgICAgICAganNvbi5kdW1w +KHsidmVyc2lvbiI6IDEsICJkaXNwbGF5cyI6IHJlY29yZHN9LCBvdXRwdXQpCiAgICAgICAgICAgICAgICAgICAgb3V0cHV0LmZsdXNoKCkKICAgICAgICAg +ICAgICAgICAgICBvcy5mc3luYyhvdXRwdXQuZmlsZW5vKCkpCiAgICAgICAgICAgICAgICBvcy5yZXBsYWNlKHRlbXBvcmFyeSwgc2VsZi5wYXRoKQogICAg +ICAgICAgICBmaW5hbGx5OgogICAgICAgICAgICAgICAgaWYgb3MucGF0aC5leGlzdHModGVtcG9yYXJ5KToKICAgICAgICAgICAgICAgICAgICBvcy51bmxp +bmsodGVtcG9yYXJ5KQogICAgICAgICAgICBzZWxmLnJlY29yZHMgPSByZWNvcmRzCiAgICAgICAgICAgIHJldHVybiBudW1iZXIKCgpjbGFzcyBPd25lZFBy +b2Nlc3M6CiAgICAiIiJBIGhhbmRsZSBmb3IgYSBkYWVtb24gc3RhcnRlZCBieSBkYnVzLWxhdW5jaCByYXRoZXIgdGhhbiBieSBQb3Blbi4iIiIKCiAgICBk +ZWYgX19pbml0X18oc2VsZiwgcGlkKToKICAgICAgICBzZWxmLnBpZCA9IHBpZAoKICAgIGRlZiB0ZXJtaW5hdGUoc2VsZik6CiAgICAgICAgdHJ5OgogICAg +ICAgICAgICBvcy5raWxsKHNlbGYucGlkLCAxNSkKICAgICAgICBleGNlcHQgT1NFcnJvcjoKICAgICAgICAgICAgcGFzcwoKICAgIGRlZiBwb2xsKHNlbGYp +OgogICAgICAgIHRyeToKICAgICAgICAgICAgb3Mua2lsbChzZWxmLnBpZCwgMCkKICAgICAgICAgICAgcmV0dXJuIE5vbmUKICAgICAgICBleGNlcHQgT1NF +cnJvcjoKICAgICAgICAgICAgcmV0dXJuIDAKCgpjbGFzcyBOb1ZOQ1VuYXZhaWxhYmxlKFJ1bnRpbWVFcnJvcik6CiAgICAiIiJUaGUgdmlld2luZyB0cmFu +c3BvcnQgZmFpbGVkIHdoaWxlIHRoZSBndWVzdCBkZXNrdG9wIG1heSBiZSBoZWFsdGh5LiIiIgoKCmNsYXNzIERpc3BsYXlTZXJ2aWNlOgogICAgZGVmIF9f +aW5pdF9fKHNlbGYsIGNhdGFsb2csIHJ1bnRpbWUsIHN0YXJ0ZXI9U1RBUlRFUik6CiAgICAgICAgc2VsZi5jYXRhbG9nID0gY2F0YWxvZwogICAgICAgIHNl +bGYucnVudGltZSA9IFBhdGgocnVudGltZSkKICAgICAgICBzZWxmLnN0YXJ0ZXIgPSBzdGFydGVyCiAgICAgICAgc2VsZi5sb2NrID0gdGhyZWFkaW5nLkxv +Y2soKQogICAgICAgIHNlbGYuam9icyA9IHt9CiAgICAgICAgc2VsZi5zdGF0ZXMgPSB7fQogICAgICAgIHNlbGYucHJvY2Vzc2VzID0ge30KICAgICAgICBz +ZWxmLm5hbWVkX3Byb2Nlc3NlcyA9IHt9CiAgICAgICAgc2VsZi53ZWJzb2NraWZ5X3Byb2Nlc3NlcyA9IHt9CiAgICAgICAgc2VsZi5zaHV0ZG93biA9IHRo +cmVhZGluZy5FdmVudCgpCgogICAgZGVmIHN0YXJ0KHNlbGYsIG51bWJlcik6CiAgICAgICAgd2l0aCBzZWxmLmxvY2s6CiAgICAgICAgICAgIGlmIG51bWJl +ciBpbiBzZWxmLmpvYnM6CiAgICAgICAgICAgICAgICByZXR1cm4gc2VsZi5qb2JzW251bWJlcl0KICAgICAgICAgICAgZmluaXNoZWQgPSB0aHJlYWRpbmcu +RXZlbnQoKQogICAgICAgICAgICBzZWxmLmpvYnNbbnVtYmVyXSA9IGZpbmlzaGVkCiAgICAgICAgICAgIHNlbGYuc3RhdGVzW251bWJlcl0gPSAibGF1bmNo +aW5nIgogICAgICAgICAgICB0aHJlYWRpbmcuVGhyZWFkKHRhcmdldD1zZWxmLnN1cGVydmlzZSwgYXJncz0obnVtYmVyLCBmaW5pc2hlZCksIGRhZW1vbj1U +cnVlKS5zdGFydCgpCiAgICAgICAgICAgIHJldHVybiBmaW5pc2hlZAoKICAgIGRlZiB0ZXJtaW5hdGUoc2VsZiwgbnVtYmVyKToKICAgICAgICBmb3IgcHJv +Y2VzcyBpbiBzZWxmLnByb2Nlc3Nlcy5wb3AobnVtYmVyLCBbXSk6CiAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgIHByb2Nlc3MudGVybWluYXRl +KCkKICAgICAgICAgICAgZXhjZXB0IE9TRXJyb3I6CiAgICAgICAgICAgICAgICBwYXNzCiAgICAgICAgc2VsZi5uYW1lZF9wcm9jZXNzZXMucG9wKG51bWJl +ciwgTm9uZSkKICAgICAgICBzZWxmLnRlcm1pbmF0ZV93ZWJzb2NraWZ5KG51bWJlcikKCiAgICBkZWYgdGVybWluYXRlX3dlYnNvY2tpZnkoc2VsZiwgbnVt +YmVyKToKICAgICAgICBwcm9jZXNzID0gc2VsZi53ZWJzb2NraWZ5X3Byb2Nlc3Nlcy5wb3AobnVtYmVyLCBOb25lKQogICAgICAgIGlmIHByb2Nlc3MgaXMg +Tm9uZToKICAgICAgICAgICAgcmV0dXJuCiAgICAgICAgdHJ5OgogICAgICAgICAgICBwcm9jZXNzLnRlcm1pbmF0ZSgpCiAgICAgICAgZXhjZXB0IE9TRXJy +b3I6CiAgICAgICAgICAgIHBhc3MKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgc2NvcGVkX2NvbXBvbmVudF9wYXRoKGV4ZWN1dGFibGUsIHBhdGgsIG51 +bWJlciwgcnVudGltZSk6CiAgICAgICAgIiIiR2l2ZSBwZXItZGlzcGxheSBkZXNrdG9wIGNsaWVudHMgbmFtZXMgdGhlIDoxIHN1cGVydmlzb3IgY2Fubm90 +IG1hdGNoLiIiIgogICAgICAgIHNvdXJjZSA9IFBhdGgocGF0aCkKICAgICAgICBpZiBub3Qgc291cmNlLmlzX2ZpbGUoKToKICAgICAgICAgICAgcmV0dXJu +IHBhdGgKICAgICAgICBkaXJlY3RvcnkgPSBQYXRoKHJ1bnRpbWUpIC8gImJpbiIKICAgICAgICBkaXJlY3RvcnkubWtkaXIobW9kZT0wbzcwMCwgcGFyZW50 +cz1UcnVlLCBleGlzdF9vaz1UcnVlKQogICAgICAgIHRhcmdldCA9IGRpcmVjdG9yeSAvIGYiY211eC1kaXNwbGF5LXtudW1iZXJ9LXtzb3VyY2UubmFtZX0i +CiAgICAgICAgaWYgbm90IHRhcmdldC5leGlzdHMoKSBvciB0YXJnZXQuc3RhdCgpLnN0X210aW1lX25zIDwgc291cmNlLnN0YXQoKS5zdF9tdGltZV9uczoK +ICAgICAgICAgICAgc2h1dGlsLmNvcHkyKHNvdXJjZSwgdGFyZ2V0KQogICAgICAgICAgICB0YXJnZXQuY2htb2QoMG83MDApCiAgICAgICAgcmV0dXJuIHN0 +cih0YXJnZXQpCgogICAgQHN0YXRpY21ldGhvZAogICAgZGVmIHdhaXRfZm9yX3BvcnQocG9ydCwgdGltZW91dD0yMCk6CiAgICAgICAgZGVhZGxpbmUgPSB0 +aW1lLm1vbm90b25pYygpICsgdGltZW91dAogICAgICAgIHdoaWxlIHRpbWUubW9ub3RvbmljKCkgPCBkZWFkbGluZToKICAgICAgICAgICAgaWYgbGlzdGVu +aW5nKHBvcnQpOgogICAgICAgICAgICAgICAgcmV0dXJuIFRydWUKICAgICAgICAgICAgdGltZS5zbGVlcCgwLjEpCiAgICAgICAgcmV0dXJuIEZhbHNlCgog +ICAgQHN0YXRpY21ldGhvZAogICAgZGVmIHByb2Nlc3NfYWxpdmUocHJvY2Vzcyk6CiAgICAgICAgcG9sbCA9IGdldGF0dHIocHJvY2VzcywgInBvbGwiLCBO +b25lKQogICAgICAgIHJldHVybiBwb2xsIGlzIE5vbmUgb3IgcG9sbCgpIGlzIE5vbmUKCiAgICBkZWYgY29tcG9uZW50c19hbGl2ZShzZWxmLCBudW1iZXIp +OgogICAgICAgIG5hbWVkID0gc2VsZi5uYW1lZF9wcm9jZXNzZXMuZ2V0KG51bWJlcikKICAgICAgICBpZiBuYW1lZCBpcyBub3QgTm9uZToKICAgICAgICAg +ICAgcmV0dXJuIGJvb2wobmFtZWQpIGFuZCBhbGwoc2VsZi5wcm9jZXNzX2FsaXZlKHByb2Nlc3MpIGZvciBwcm9jZXNzIGluIG5hbWVkLnZhbHVlcygpKSBc +CiAgICAgICAgICAgICAgICBhbmQgKCJ4dm5jIiBpbiBuYW1lZCBvciByZmJfcmVhZHkobnVtYmVyKSkKICAgICAgICBwcm9jZXNzZXMgPSBzZWxmLnByb2Nl +c3Nlcy5nZXQobnVtYmVyLCBbXSkKICAgICAgICByZXR1cm4gYm9vbChwcm9jZXNzZXMpIGFuZCBhbGwoc2VsZi5wcm9jZXNzX2FsaXZlKHByb2Nlc3MpIGZv +ciBwcm9jZXNzIGluIHByb2Nlc3NlcykKCiAgICBkZWYgc2VydmljZV9yZWFkeShzZWxmLCBudW1iZXIpOgogICAgICAgIGlmIG5vdCByZWFkeShudW1iZXIp +IG9yIG5vdCBzZWxmLmNvbXBvbmVudHNfYWxpdmUobnVtYmVyKToKICAgICAgICAgICAgcmV0dXJuIEZhbHNlCiAgICAgICAgd2Vic29ja2lmeSA9IHNlbGYu +d2Vic29ja2lmeV9wcm9jZXNzZXMuZ2V0KG51bWJlcikKICAgICAgICByZXR1cm4gd2Vic29ja2lmeSBpcyBOb25lIG9yIHNlbGYucHJvY2Vzc19hbGl2ZSh3 +ZWJzb2NraWZ5KQoKICAgIGRlZiB0cmFja19wcm9jZXNzKHNlbGYsIG51bWJlciwgbmFtZSwgcHJvY2Vzcyk6CiAgICAgICAgbmFtZWQgPSBzZWxmLm5hbWVk +X3Byb2Nlc3Nlcy5zZXRkZWZhdWx0KG51bWJlciwge30pCiAgICAgICAgcHJldmlvdXMgPSBuYW1lZC5nZXQobmFtZSkKICAgICAgICBpZiBwcmV2aW91cyBp +cyBub3QgTm9uZToKICAgICAgICAgICAgc2VsZi5wcm9jZXNzZXNbbnVtYmVyXSA9IFsKICAgICAgICAgICAgICAgIGl0ZW0gZm9yIGl0ZW0gaW4gc2VsZi5w +cm9jZXNzZXMuZ2V0KG51bWJlciwgW10pIGlmIGl0ZW0gaXMgbm90IHByZXZpb3VzCiAgICAgICAgICAgIF0KICAgICAgICBzZWxmLnByb2Nlc3Nlcy5zZXRk +ZWZhdWx0KG51bWJlciwgW10pLmFwcGVuZChwcm9jZXNzKQogICAgICAgIG5hbWVkW25hbWVdID0gcHJvY2VzcwoKICAgIGRlZiByZW1vdmVfcHJvY2Vzcyhz +ZWxmLCBudW1iZXIsIG5hbWUpOgogICAgICAgIHByb2Nlc3MgPSBzZWxmLm5hbWVkX3Byb2Nlc3Nlcy5zZXRkZWZhdWx0KG51bWJlciwge30pLnBvcChuYW1l +LCBOb25lKQogICAgICAgIGlmIHByb2Nlc3MgaXMgTm9uZToKICAgICAgICAgICAgcmV0dXJuCiAgICAgICAgc2VsZi5wcm9jZXNzZXNbbnVtYmVyXSA9IFtp +dGVtIGZvciBpdGVtIGluIHNlbGYucHJvY2Vzc2VzLmdldChudW1iZXIsIFtdKSBpZiBpdGVtIGlzIG5vdCBwcm9jZXNzXQogICAgICAgIHRyeToKICAgICAg +ICAgICAgcHJvY2Vzcy50ZXJtaW5hdGUoKQogICAgICAgIGV4Y2VwdCBPU0Vycm9yOgogICAgICAgICAgICBwYXNzCgogICAgZGVmIHJlY292ZXJfcHJvY2Vz +c2VzKHNlbGYsIG51bWJlciwgcnVudGltZSk6CiAgICAgICAgc2VsZi5wcm9jZXNzZXNbbnVtYmVyXSA9IFtdCiAgICAgICAgc2VsZi5uYW1lZF9wcm9jZXNz +ZXNbbnVtYmVyXSA9IHt9CiAgICAgICAgdWlkID0gc3RyKG9zLmdldHVpZCgpKQogICAgICAgIGZvciBuYW1lIGluICgib3BlbmJveCIsICJ0aW50MiIsICJ2 +bmNjb25maWciKToKICAgICAgICAgICAgbWFya2VyID0gc3RyKFBhdGgocnVudGltZSkgLyAiYmluIiAvIGYiY211eC1kaXNwbGF5LXtudW1iZXJ9LXtuYW1l +fSIpCiAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgIHBhdHRlcm4gPSByZiJee3JlLmVzY2FwZShtYXJrZXIpfSg/OltbOnNwYWNlOl1dfCQpIgog +ICAgICAgICAgICAgICAgb3V0cHV0ID0gc3VicHJvY2Vzcy5jaGVja19vdXRwdXQoWyJwZ3JlcCIsICItdSIsIHVpZCwgIi1mIiwgcGF0dGVybl0sIHRleHQ9 +VHJ1ZSkKICAgICAgICAgICAgICAgIHBpZCA9IGludChvdXRwdXQuc3BsaXRsaW5lcygpWzBdKQogICAgICAgICAgICBleGNlcHQgKE9TRXJyb3IsIFZhbHVl +RXJyb3IsIEluZGV4RXJyb3IsIHN1YnByb2Nlc3MuQ2FsbGVkUHJvY2Vzc0Vycm9yKToKICAgICAgICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgICAgIHNl +bGYudHJhY2tfcHJvY2VzcyhudW1iZXIsIG5hbWUsIE93bmVkUHJvY2VzcyhwaWQpKQogICAgICAgIHRyeToKICAgICAgICAgICAgb3V0cHV0ID0gc3VicHJv +Y2Vzcy5jaGVja19vdXRwdXQoCiAgICAgICAgICAgICAgICBbInBncmVwIiwgIi11IiwgdWlkLCAiLWYiLAogICAgICAgICAgICAgICAgIHJmIl4oPzouKlwv +KT8oPzpYdm5jfFh0aWdlcnZuYykuKjp7bnVtYmVyfS4qLXJmYnBvcnRccyt7NTkwMCArIG51bWJlcn0oPzpcc3wkKSJdLAogICAgICAgICAgICAgICAgdGV4 +dD1UcnVlCiAgICAgICAgICAgICkKICAgICAgICAgICAgcGlkID0gaW50KG91dHB1dC5zcGxpdGxpbmVzKClbMF0pCiAgICAgICAgZXhjZXB0IChPU0Vycm9y +LCBWYWx1ZUVycm9yLCBJbmRleEVycm9yLCBzdWJwcm9jZXNzLkNhbGxlZFByb2Nlc3NFcnJvcik6CiAgICAgICAgICAgIHBpZCA9IDAKICAgICAgICBpZiBw +aWQgPiAxOgogICAgICAgICAgICBzZWxmLnRyYWNrX3Byb2Nlc3MobnVtYmVyLCAieHZuYyIsIE93bmVkUHJvY2VzcyhwaWQpKQoKICAgIGRlZiBzdGFydF9k +YnVzKHNlbGYsIG51bWJlciwgZW52aXJvbm1lbnQpOgogICAgICAgIGJ1cyA9IHN1YnByb2Nlc3MucnVuKFsiZGJ1cy1sYXVuY2giLCAiLS1zaC1zeW50YXgi +XSwgZW52PWVudmlyb25tZW50LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0ZGluPXN1YnByb2Nlc3MuREVWTlVMTCwgc3Rkb3V0PXN1YnByb2Nl +c3MuUElQRSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdGRlcnI9c3VicHJvY2Vzcy5ERVZOVUxMLCB0ZXh0PVRydWUsIHRpbWVvdXQ9MTAsCiAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgY2hlY2s9VHJ1ZSkuc3Rkb3V0CiAgICAgICAgZm9yIGxpbmUgaW4gYnVzLnNwbGl0bGluZXMoKToKICAgICAg +ICAgICAgaWYgIj0iIG5vdCBpbiBsaW5lOgogICAgICAgICAgICAgICAgY29udGludWUKICAgICAgICAgICAga2V5LCB2YWx1ZSA9IGxpbmUuc3BsaXQoIj0i +LCAxKQogICAgICAgICAgICBlbnZpcm9ubWVudFtrZXldID0gdmFsdWUuc3RyaXAoKS5yc3RyaXAoIjsiKS5zdHJpcCgpLnN0cmlwKCInIikKICAgICAgICB0 +cnk6CiAgICAgICAgICAgIGRidXNfcGlkID0gaW50KGVudmlyb25tZW50LmdldCgiREJVU19TRVNTSU9OX0JVU19QSUQiLCAiMCIpKQogICAgICAgIGV4Y2Vw +dCBWYWx1ZUVycm9yOgogICAgICAgICAgICBkYnVzX3BpZCA9IDAKICAgICAgICBpZiBkYnVzX3BpZCA+IDE6CiAgICAgICAgICAgIHNlbGYudHJhY2tfcHJv +Y2VzcyhudW1iZXIsICJkYnVzIiwgT3duZWRQcm9jZXNzKGRidXNfcGlkKSkKCiAgICBkZWYgc3RhcnRfc2Vzc2lvbl9jb21wb25lbnRzKHNlbGYsIG51bWJl +ciwgZW52aXJvbm1lbnQsIHJ1bnRpbWUpOgogICAgICAgIGZvciBleGVjdXRhYmxlLCBhcmdzIGluIFsKICAgICAgICAgICAgKCJvcGVuYm94IiwgW10pLCAo +InRpbnQyIiwgWyItYyIsICIvZXRjL2NtdXgvdGludDJyYyJdKSwKICAgICAgICAgICAgKCJ2bmNjb25maWciLCBbIi1ub3dpbiJdKSwKICAgICAgICBdOgog +ICAgICAgICAgICBleGlzdGluZyA9IHNlbGYubmFtZWRfcHJvY2Vzc2VzLmdldChudW1iZXIsIHt9KS5nZXQoZXhlY3V0YWJsZSkKICAgICAgICAgICAgaWYg +ZXhpc3RpbmcgaXMgbm90IE5vbmUgYW5kIHNlbGYucHJvY2Vzc19hbGl2ZShleGlzdGluZyk6CiAgICAgICAgICAgICAgICBjb250aW51ZQogICAgICAgICAg +ICBwYXRoID0gZXhlY3V0YWJsZSBpZiAiLyIgaW4gZXhlY3V0YWJsZSBlbHNlIHNodXRpbC53aGljaChleGVjdXRhYmxlKQogICAgICAgICAgICBpZiBub3Qg +cGF0aDoKICAgICAgICAgICAgICAgIGlmIGV4ZWN1dGFibGUgPT0gInZuY2NvbmZpZyI6CiAgICAgICAgICAgICAgICAgICAgY29udGludWUKICAgICAgICAg +ICAgICAgIHJhaXNlIFJ1bnRpbWVFcnJvcihmIm1pc3NpbmcgZGlzcGxheSBjb21wb25lbnQ6IHtleGVjdXRhYmxlfSIpCiAgICAgICAgICAgIHBhdGggPSBz +ZWxmLnNjb3BlZF9jb21wb25lbnRfcGF0aChleGVjdXRhYmxlLCBwYXRoLCBudW1iZXIsIHJ1bnRpbWUpCiAgICAgICAgICAgIHByb2Nlc3MgPSBzdWJwcm9j +ZXNzLlBvcGVuKFtwYXRoXSArIGFyZ3MsIGVudj1lbnZpcm9ubWVudCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3RkaW49c3Vi +cHJvY2Vzcy5ERVZOVUxMLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdGRvdXQ9c3VicHJvY2Vzcy5ERVZOVUxMLCBzdGRlcnI9 +c3VicHJvY2Vzcy5ERVZOVUxMKQogICAgICAgICAgICBzZWxmLnRyYWNrX3Byb2Nlc3MobnVtYmVyLCBleGVjdXRhYmxlLCBwcm9jZXNzKQoKICAgIGRlZiBz +dGFydF93ZWJzb2NraWZ5KHNlbGYsIG51bWJlciwgZW52aXJvbm1lbnQsIGV4ZWN1dGFibGUpOgogICAgICAgIHBhdGggPSBleGVjdXRhYmxlIGlmICIvIiBp +biBleGVjdXRhYmxlIGVsc2Ugc2h1dGlsLndoaWNoKGV4ZWN1dGFibGUpCiAgICAgICAgaWYgbm90IHBhdGg6CiAgICAgICAgICAgIHJhaXNlIE5vVk5DVW5h +dmFpbGFibGUoIm1pc3NpbmcgZGlzcGxheSBjb21wb25lbnQ6IHdlYnNvY2tpZnkiKQogICAgICAgIHNlbGYudGVybWluYXRlX3dlYnNvY2tpZnkobnVtYmVy +KQogICAgICAgIHJmYl9wb3J0LCBub3ZuY19wb3J0ID0gNTkwMCArIG51bWJlciwgNjkwMCArIG51bWJlcgogICAgICAgIHByb2Nlc3MgPSBzdWJwcm9jZXNz +LlBvcGVuKAogICAgICAgICAgICBbcGF0aCwgIi0td2ViIiwgIi91c3Ivc2hhcmUvbm92bmMiLCAiLS1oZWFydGJlYXQiLCAiMzAiLAogICAgICAgICAgICAg +ZiIxMjcuMC4wLjE6e25vdm5jX3BvcnR9IiwgZiIxMjcuMC4wLjE6e3JmYl9wb3J0fSJdLAogICAgICAgICAgICBlbnY9ZW52aXJvbm1lbnQsIHN0ZGluPXN1 +YnByb2Nlc3MuREVWTlVMTCwKICAgICAgICAgICAgc3Rkb3V0PXN1YnByb2Nlc3MuREVWTlVMTCwgc3RkZXJyPXN1YnByb2Nlc3MuREVWTlVMTCkKICAgICAg +ICBzZWxmLndlYnNvY2tpZnlfcHJvY2Vzc2VzW251bWJlcl0gPSBwcm9jZXNzCiAgICAgICAgaWYgbm90IHNlbGYud2FpdF9mb3JfcG9ydChub3ZuY19wb3J0 +LCB0aW1lb3V0PTEwKToKICAgICAgICAgICAgc2VsZi50ZXJtaW5hdGVfd2Vic29ja2lmeShudW1iZXIpCiAgICAgICAgICAgIHJhaXNlIE5vVk5DVW5hdmFp +bGFibGUoIm5vVk5DIGRpZCBub3QgYmVjb21lIHJlYWR5IikKCiAgICBkZWYgc3RhcnRfY29tcG9uZW50cyhzZWxmLCBudW1iZXIsIGVudmlyb25tZW50LCBy +dW50aW1lKToKICAgICAgICBpZiBzZWxmLnNlcnZpY2VfcmVhZHkobnVtYmVyKToKICAgICAgICAgICAgcmV0dXJuCiAgICAgICAgcmZiX2lzX3JlYWR5ID0g +cmZiX3JlYWR5KG51bWJlcikKICAgICAgICB2bmMgPSBzaHV0aWwud2hpY2goIlh2bmMiKSBvciBzaHV0aWwud2hpY2goIlh0aWdlcnZuYyIpCiAgICAgICAg +d2Vic29ja2lmeSA9IHNodXRpbC53aGljaCgid2Vic29ja2lmeSIpCiAgICAgICAgaWYgbm90IHdlYnNvY2tpZnk6CiAgICAgICAgICAgIHJhaXNlIE5vVk5D +VW5hdmFpbGFibGUoImRpc3BsYXkgcnVudGltZSB1bmF2YWlsYWJsZSIpCiAgICAgICAgaWYgbm90IHJmYl9pc19yZWFkeSBhbmQgbm90IHZuYzoKICAgICAg +ICAgICAgcmFpc2UgUnVudGltZUVycm9yKCJkaXNwbGF5IHJ1bnRpbWUgdW5hdmFpbGFibGUiKQogICAgICAgIGlmIHJmYl9pc19yZWFkeSBhbmQgbnVtYmVy +IG5vdCBpbiBzZWxmLm5hbWVkX3Byb2Nlc3NlczoKICAgICAgICAgICAgc2VsZi5yZWNvdmVyX3Byb2Nlc3NlcyhudW1iZXIsIHJ1bnRpbWUpCiAgICAgICAg +aWYgbm90IHJmYl9pc19yZWFkeToKICAgICAgICAgICAgc2VsZi50ZXJtaW5hdGUobnVtYmVyKQogICAgICAgICAgICByZmJfcG9ydCA9IDU5MDAgKyBudW1i +ZXIKICAgICAgICAgICAgc2VsZi5wcm9jZXNzZXNbbnVtYmVyXSA9IFtdCiAgICAgICAgICAgIHNlbGYubmFtZWRfcHJvY2Vzc2VzW251bWJlcl0gPSB7fQog +ICAgICAgICAgICBzZWxmLnRyYWNrX3Byb2Nlc3MobnVtYmVyLCAieHZuYyIsIHN1YnByb2Nlc3MuUG9wZW4oCiAgICAgICAgICAgICAgICBbdm5jLCBmIjp7 +bnVtYmVyfSIsICItZ2VvbWV0cnkiLCBvcy5lbnZpcm9uLmdldCgiQ01VWF9WTkNfR0VPTUVUUlkiLCAiMTQ0MHg5MDAiKSwKICAgICAgICAgICAgICAgICAi +LWRlcHRoIiwgIjI0IiwgIi1yZmJwb3J0Iiwgc3RyKHJmYl9wb3J0KSwgIi1sb2NhbGhvc3QiLAogICAgICAgICAgICAgICAgICItU2VjdXJpdHlUeXBlcyIs +ICJOb25lIiwgIi1BbHdheXNTaGFyZWQiXSwKICAgICAgICAgICAgICAgIGVudj1lbnZpcm9ubWVudCwgc3RkaW49c3VicHJvY2Vzcy5ERVZOVUxMLAogICAg +ICAgICAgICAgICAgc3Rkb3V0PXN1YnByb2Nlc3MuREVWTlVMTCwgc3RkZXJyPXN1YnByb2Nlc3MuREVWTlVMTCkpCiAgICAgICAgICAgIGlmIG5vdCBzZWxm +LndhaXRfZm9yX3BvcnQocmZiX3BvcnQpOgogICAgICAgICAgICAgICAgcmFpc2UgUnVudGltZUVycm9yKCJYdm5jIGRpZCBub3QgYmVjb21lIHJlYWR5IikK +ICAgICAgICAgICAgc2VsZi5zdGFydF9kYnVzKG51bWJlciwgZW52aXJvbm1lbnQpCiAgICAgICAgICAgIHNlbGYuc3RhcnRfc2Vzc2lvbl9jb21wb25lbnRz +KG51bWJlciwgZW52aXJvbm1lbnQsIHJ1bnRpbWUpCiAgICAgICAgZWxpZiBudW1iZXIgaW4gc2VsZi5uYW1lZF9wcm9jZXNzZXM6CiAgICAgICAgICAgIG5h +bWVkID0gc2VsZi5uYW1lZF9wcm9jZXNzZXNbbnVtYmVyXQogICAgICAgICAgICBkYnVzID0gbmFtZWQuZ2V0KCJkYnVzIikKICAgICAgICAgICAgaWYgZGJ1 +cyBpcyBOb25lIG9yIG5vdCBzZWxmLnByb2Nlc3NfYWxpdmUoZGJ1cyk6CiAgICAgICAgICAgICAgICBmb3IgbmFtZSBpbiAoIm9wZW5ib3giLCAidGludDIi +LCAidm5jY29uZmlnIik6CiAgICAgICAgICAgICAgICAgICAgc2VsZi5yZW1vdmVfcHJvY2VzcyhudW1iZXIsIG5hbWUpCiAgICAgICAgICAgICAgICBzZWxm +LnN0YXJ0X2RidXMobnVtYmVyLCBlbnZpcm9ubWVudCkKICAgICAgICAgICAgc2VsZi5zdGFydF9zZXNzaW9uX2NvbXBvbmVudHMobnVtYmVyLCBlbnZpcm9u +bWVudCwgcnVudGltZSkKICAgICAgICB3ZWJzb2NraWZ5X3Byb2Nlc3MgPSBzZWxmLndlYnNvY2tpZnlfcHJvY2Vzc2VzLmdldChudW1iZXIpCiAgICAgICAg +aWYgKHdlYnNvY2tpZnlfcHJvY2VzcyBpcyBOb25lIG9yIG5vdCBzZWxmLnByb2Nlc3NfYWxpdmUod2Vic29ja2lmeV9wcm9jZXNzKQogICAgICAgICAgICAg +ICAgb3Igbm90IG5vdm5jX3JlYWR5KG51bWJlcikpOgogICAgICAgICAgICBzZWxmLnN0YXJ0X3dlYnNvY2tpZnkobnVtYmVyLCBlbnZpcm9ubWVudCwgd2Vi +c29ja2lmeSkKICAgICAgICAocnVudGltZSAvICJlbnYiKS53cml0ZV90ZXh0KGYiZXhwb3J0IERJU1BMQVk9OntudW1iZXJ9XG4iKQoKICAgIGRlZiBzdXBl +cnZpc2Uoc2VsZiwgbnVtYmVyLCBmaW5pc2hlZCk6CiAgICAgICAgcnVudGltZSA9IHNlbGYucnVudGltZSAvIHN0cihudW1iZXIpCiAgICAgICAgcnVudGlt +ZS5ta2Rpcihtb2RlPTBvNzAwLCBwYXJlbnRzPVRydWUsIGV4aXN0X29rPVRydWUpCiAgICAgICAgZW52aXJvbm1lbnQgPSBkaWN0KG9zLmVudmlyb24sIERJ +U1BMQVk9ZiI6e251bWJlcn0iLCBDTVVYX0RFU0tUT1BfUlVOVElNRV9ESVI9c3RyKHJ1bnRpbWUpKQogICAgICAgIGVudmlyb25tZW50WyJDSFJPTUVfQ09O +RklHX0hPTUUiXSA9IHN0cihydW50aW1lIC8gImNocm9tZS1jb25maWciKQogICAgICAgIGVudmlyb25tZW50WyJYREdfQ09ORklHX0hPTUUiXSA9IHN0cihy +dW50aW1lIC8gInhkZy1jb25maWciKQogICAgICAgIGZvciBrZXkgaW4gKCJOT1RJRllfU09DS0VUIiwgIkRCVVNfU0VTU0lPTl9CVVNfQUREUkVTUyIsICJE +QlVTX1NFU1NJT05fQlVTX1BJRCIsCiAgICAgICAgICAgICAgICAgICAgIkFUX1NQSV9CVVNfQUREUkVTUyIsICJBVF9TUElfQlVTIik6CiAgICAgICAgICAg +IGVudmlyb25tZW50LnBvcChrZXksIE5vbmUpCiAgICAgICAgd2hpbGUgbm90IHNlbGYuc2h1dGRvd24uaXNfc2V0KCk6CiAgICAgICAgICAgIHRyeToKICAg +ICAgICAgICAgICAgICMgVGhlIHN1cGVydmlzb3IgaXMgYSBoZWFsdGggY2hlY2ssIG5vdCBhIHRpbWVyLWRyaXZlbiByZXN0YXJ0LgogICAgICAgICAgICAg +ICAgIyBQcmVzZXJ2ZSBhbiBhY3RpdmUgUkZCL25vVk5DIHNlc3Npb24gdW50aWwgaXRzIG93bmVkIGVuZHBvaW50cyBmYWlsLgogICAgICAgICAgICAgICAg +aWYgbm90IHNlbGYuc2VydmljZV9yZWFkeShudW1iZXIpOgogICAgICAgICAgICAgICAgICAgIHNlbGYuc3RhcnRfY29tcG9uZW50cyhudW1iZXIsIGVudmly +b25tZW50LCBydW50aW1lKQogICAgICAgICAgICAgICAgc3RhdGUgPSAicnVubmluZyIgaWYgc2VsZi5zZXJ2aWNlX3JlYWR5KG51bWJlcikgZWxzZSAidW5h +dmFpbGFibGUiCiAgICAgICAgICAgIGV4Y2VwdCBOb1ZOQ1VuYXZhaWxhYmxlOgogICAgICAgICAgICAgICAgc2VsZi50ZXJtaW5hdGVfd2Vic29ja2lmeShu +dW1iZXIpCiAgICAgICAgICAgICAgICBzdGF0ZSA9ICJ1bmF2YWlsYWJsZSIKICAgICAgICAgICAgZXhjZXB0IChPU0Vycm9yLCBSdW50aW1lRXJyb3IsIHN1 +YnByb2Nlc3MuU3VicHJvY2Vzc0Vycm9yKToKICAgICAgICAgICAgICAgIHNlbGYudGVybWluYXRlKG51bWJlcikKICAgICAgICAgICAgICAgIHN0YXRlID0g +InVuYXZhaWxhYmxlIgogICAgICAgICAgICB3aXRoIHNlbGYubG9jazoKICAgICAgICAgICAgICAgIHNlbGYuc3RhdGVzW251bWJlcl0gPSBzdGF0ZQogICAg +ICAgICAgICBmaW5pc2hlZC5zZXQoKQogICAgICAgICAgICBzZWxmLnNodXRkb3duLndhaXQoMzApCiAgICAgICAgc2VsZi50ZXJtaW5hdGUobnVtYmVyKQoK +ICAgIGRlZiBzbmFwc2hvdChzZWxmKToKICAgICAgICBwcmltYXJ5X3N0YXRlID0gInJ1bm5pbmciIGlmIHJlYWR5KDEpIGVsc2UgInVuYXZhaWxhYmxlIgog +ICAgICAgIHdpdGggc2VsZi5sb2NrOgogICAgICAgICAgICByZXR1cm4geyJ2ZXJzaW9uIjogMSwgImNhbkNyZWF0ZSI6IGxlbihzZWxmLmpvYnMpIDwgTUFY +X0RJU1BMQVlTIC0gMSwKICAgICAgICAgICAgICAgICAgICAiZGlzcGxheXMiOiBbZGVzY3JpcHRvcigxLCBwcmltYXJ5X3N0YXRlKV0KICAgICAgICAgICAg +ICAgICAgICArIFtkZXNjcmlwdG9yKG4sIHNlbGYuc3RhdGVzLmdldChuLCAibGF1bmNoaW5nIikpIGZvciBuIGluIHNlbGYuY2F0YWxvZy5udW1iZXJzKCld +fQoKICAgIGRlZiBoYW5kbGUoc2VsZiwgcmVxdWVzdCk6CiAgICAgICAgaWYgcmVxdWVzdC5nZXQoImFjdGlvbiIpID09ICJsaXN0IjoKICAgICAgICAgICAg +cmV0dXJuIHNlbGYuc25hcHNob3QoKQogICAgICAgIGlmIHJlcXVlc3QuZ2V0KCJhY3Rpb24iKSAhPSAiY3JlYXRlIjoKICAgICAgICAgICAgcmFpc2UgVmFs +dWVFcnJvcigidW5zdXBwb3J0ZWQgZGlzcGxheSBhY3Rpb24iKQogICAgICAgIG51bWJlciA9IHNlbGYuY2F0YWxvZy5hbGxvY2F0ZShyZXF1ZXN0LmdldCgi +cmVxdWVzdCIpKQogICAgICAgIHNlbGYuc3RhcnQobnVtYmVyKS53YWl0KDU1KQogICAgICAgIHJlc3VsdCA9IHNlbGYuc25hcHNob3QoKQogICAgICAgIHJl +c3VsdFsiY3JlYXRlZCJdID0gZiJkaXNwbGF5OntudW1iZXJ9IgogICAgICAgIGlmIHNlbGYuc3RhdGVzLmdldChudW1iZXIpICE9ICJydW5uaW5nIjoKICAg +ICAgICAgICAgcmVzdWx0WyJlcnJvciJdID0gImRpc3BsYXlfc3RhcnRfZmFpbGVkIgogICAgICAgIHJldHVybiByZXN1bHQKCgpkZWYgc2VydmUocnVudGlt +ZSk6CiAgICBydW50aW1lID0gUGF0aChydW50aW1lKQogICAgcnVudGltZS5ta2Rpcihtb2RlPTBvNzAwLCBwYXJlbnRzPVRydWUsIGV4aXN0X29rPVRydWUp +CiAgICB3aXRoIChydW50aW1lIC8gImRpc3BsYXktY29udHJvbC5sb2NrIikub3BlbigidyIpIGFzIGxvY2s6CiAgICAgICAgdHJ5OgogICAgICAgICAgICBm +Y250bC5mbG9jayhsb2NrLCBmY250bC5MT0NLX0VYIHwgZmNudGwuTE9DS19OQikKICAgICAgICBleGNlcHQgQmxvY2tpbmdJT0Vycm9yOgogICAgICAgICAg +ICByZXR1cm4KICAgICAgICBpZiBub3QgYWxsKHNodXRpbC53aGljaChuYW1lKSBmb3IgbmFtZSBpbiAoIndlYnNvY2tpZnkiLCAib3BlbmJveCIsICJkYnVz +LWxhdW5jaCIpKToKICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJvcigiZGlzcGxheSBydW50aW1lIHVuYXZhaWxhYmxlIikKICAgICAgICBpZiBub3QgKHNo +dXRpbC53aGljaCgiWHZuYyIpIG9yIHNodXRpbC53aGljaCgiWHRpZ2Vydm5jIikpOgogICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJkaXNwbGF5IHJ1 +bnRpbWUgdW5hdmFpbGFibGUiKQogICAgICAgIHNlcnZpY2UgPSBEaXNwbGF5U2VydmljZShEaXNwbGF5Q2F0YWxvZyhQYXRoLmhvbWUoKSAvICIuY211eCIg +LyAiZGlzcGxheXMiKSwgcnVudGltZSAvICJkaXNwbGF5cyIpCiAgICAgICAgZm9yIG51bWJlciBpbiBzZXJ2aWNlLmNhdGFsb2cubnVtYmVycygpOgogICAg +ICAgICAgICBzZXJ2aWNlLnN0YXJ0KG51bWJlcikKICAgICAgICBwYXRoID0gcnVudGltZSAvICJkaXNwbGF5LWNvbnRyb2wuc29jayIKICAgICAgICBpZiBw +YXRoLmV4aXN0cygpOgogICAgICAgICAgICBwYXRoLnVubGluaygpCgogICAgICAgIGNsYXNzIEhhbmRsZXIoc29ja2V0c2VydmVyLlN0cmVhbVJlcXVlc3RI +YW5kbGVyKToKICAgICAgICAgICAgZGVmIGhhbmRsZShzZWxmKToKICAgICAgICAgICAgICAgIHNlbGYuY29ubmVjdGlvbi5zZXR0aW1lb3V0KDYwKQogICAg +ICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgIHJhdyA9IHNlbGYucmZpbGUucmVhZGxpbmUoNDA5NykKICAgICAgICAgICAgICAgICAgICBp +ZiBsZW4ocmF3KSA+IDQwOTY6CiAgICAgICAgICAgICAgICAgICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoImRpc3BsYXkgcmVxdWVzdCB0b28gbGFyZ2UiKQog +ICAgICAgICAgICAgICAgICAgIHJlc3VsdCA9IHNlcnZpY2UuaGFuZGxlKGpzb24ubG9hZHMocmF3KSkKICAgICAgICAgICAgICAgIGV4Y2VwdCAoVmFsdWVF +cnJvciwgS2V5RXJyb3IsIFR5cGVFcnJvciwgT1NFcnJvcik6CiAgICAgICAgICAgICAgICAgICAgcmVzdWx0ID0geyJlcnJvciI6ICJkaXNwbGF5X3JlcXVl +c3RfZmFpbGVkIn0KICAgICAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgICAgICBzZWxmLndmaWxlLndyaXRlKGpzb24uZHVtcHMocmVzdWx0KS5l +bmNvZGUoKSArIGIiXG4iKQogICAgICAgICAgICAgICAgZXhjZXB0IE9TRXJyb3I6CiAgICAgICAgICAgICAgICAgICAgcGFzcyAgIyBUaGUgcGVyc2lzdGVk +IHJlY2VpcHQgcmVtYWlucyByZXBsYXlhYmxlIGFmdGVyIGRpc2Nvbm5lY3QuCgogICAgICAgIGNsYXNzIFNlcnZlcihzb2NrZXRzZXJ2ZXIuVGhyZWFkaW5n +VW5peFN0cmVhbVNlcnZlcik6CiAgICAgICAgICAgIGRhZW1vbl90aHJlYWRzID0gVHJ1ZQoKICAgICAgICBwcmV2aW91c191bWFzayA9IG9zLnVtYXNrKDBv +MDc3KQogICAgICAgIHRyeToKICAgICAgICAgICAgd2l0aCBTZXJ2ZXIoc3RyKHBhdGgpLCBIYW5kbGVyKSBhcyBzZXJ2ZXI6CiAgICAgICAgICAgICAgICBv +cy51bWFzayhwcmV2aW91c191bWFzaykKICAgICAgICAgICAgICAgIHNlcnZlci5zZXJ2ZV9mb3JldmVyKCkKICAgICAgICBmaW5hbGx5OgogICAgICAgICAg +ICBvcy51bWFzayhwcmV2aW91c191bWFzaykKICAgICAgICAgICAgc2VydmljZS5zaHV0ZG93bi5zZXQoKQogICAgICAgICAgICBwYXRoLnVubGluayhtaXNz +aW5nX29rPVRydWUpCgoKZGVmIG1haW4oKToKICAgIHBhcnNlciA9IGFyZ3BhcnNlLkFyZ3VtZW50UGFyc2VyKGRlc2NyaXB0aW9uPV9fZG9jX18pCiAgICBw +YXJzZXIuYWRkX2FyZ3VtZW50KCJhY3Rpb24iLCBjaG9pY2VzPVsibGlzdCIsICJjcmVhdGUiLCAic2VydmUiXSkKICAgIHBhcnNlci5hZGRfYXJndW1lbnQo +Ii0tcmVxdWVzdC1pZCIpCiAgICBhcmdzID0gcGFyc2VyLnBhcnNlX2FyZ3MoKQogICAgcnVudGltZSA9IG9zLmVudmlyb24uZ2V0KCJDTVVYX0RFU0tUT1Bf +UlVOVElNRV9ESVIiLCAiL3J1bi9jbXV4LWRlc2t0b3AiKQogICAgaWYgYXJncy5hY3Rpb24gPT0gInNlcnZlIjoKICAgICAgICBzZXJ2ZShydW50aW1lKQog +ICAgICAgIHJldHVybiAwCiAgICBpZiBhcmdzLmFjdGlvbiA9PSAiY3JlYXRlIiBhbmQgbm90IGFyZ3MucmVxdWVzdF9pZDoKICAgICAgICBwYXJzZXIuZXJy +b3IoImNyZWF0ZSByZXF1aXJlcyAtLXJlcXVlc3QtaWQgKHJldGFpbiBpdCB3aGVuIHJldHJ5aW5nKSIpCiAgICB0cnk6CiAgICAgICAgd2l0aCBzb2NrZXQu +c29ja2V0KHNvY2tldC5BRl9VTklYLCBzb2NrZXQuU09DS19TVFJFQU0pIGFzIGNsaWVudDoKICAgICAgICAgICAgY2xpZW50LnNldHRpbWVvdXQoNjApCiAg +ICAgICAgICAgIGNsaWVudC5jb25uZWN0KHN0cihQYXRoKHJ1bnRpbWUpIC8gImRpc3BsYXktY29udHJvbC5zb2NrIikpCiAgICAgICAgICAgIGNsaWVudC5z +ZW5kYWxsKGpzb24uZHVtcHMoeyJhY3Rpb24iOiBhcmdzLmFjdGlvbiwgInJlcXVlc3QiOiBhcmdzLnJlcXVlc3RfaWR9KS5lbmNvZGUoKSArIGIiXG4iKQog +ICAgICAgICAgICB3aXRoIGNsaWVudC5tYWtlZmlsZSgicmIiKSBhcyBzdHJlYW06CiAgICAgICAgICAgICAgICByZXN1bHQgPSBqc29uLmxvYWRzKHN0cmVh +bS5yZWFkbGluZSg2NTUzNikpCiAgICBleGNlcHQgKE9TRXJyb3IsIFZhbHVlRXJyb3IpOgogICAgICAgIHJlc3VsdCA9IHsiZXJyb3IiOiAiZGlzcGxheV9y +dW50aW1lX3VuYXZhaWxhYmxlIn0KICAgIHByaW50KGpzb24uZHVtcHMocmVzdWx0KSkKICAgIHJldHVybiAxIGlmICJlcnJvciIgaW4gcmVzdWx0IGVsc2Ug +MAoKCmlmIF9fbmFtZV9fID09ICJfX21haW5fXyI6CiAgICByYWlzZSBTeXN0ZW1FeGl0KG1haW4oKSkK """ private static let path = "$HOME/.cmux/cmux-display" @@ -243,8 +245,8 @@ ImVycm9yIiBpbiByZXN1bHQgZWxzZSAwCgoKaWYgX19uYW1lX18gPT0gIl9fbWFpbl9fIjoKICAgIHJh if [ ! -x \"$path\" ]; then printf %s \"\(encodedSource)\" | base64 -d > \"$path\"; chmod 700 \"$path\"; fi if ! pgrep -u \"$(id -u)\" -f \"$path serve\" >/dev/null 2>&1; then nohup \"$path\" serve > \"$HOME/.cmux/display-service.log\" 2>&1 & - for i in $(seq 1 100); do [ -S /run/cmux-desktop/display-control.sock ] && break; sleep 0.1; done fi + \"$path\" list > /dev/null 2>&1 || exit 1 \"$path\" \(action)\(argument) """ return "runuser -u cmux -- env HOME=/home/cmux USER=cmux /bin/bash -lc '\(body)'" diff --git a/Sources/Cloud/PortForward/CloudBrowserAccessState.swift b/Sources/Cloud/PortForward/CloudBrowserAccessState.swift index d1ffe141a77c..09d067f40d8b 100644 --- a/Sources/Cloud/PortForward/CloudBrowserAccessState.swift +++ b/Sources/Cloud/PortForward/CloudBrowserAccessState.swift @@ -204,6 +204,7 @@ final class CloudBrowserAccessState { error = nil desktopFailure = nil dismissedFailure = nil + desktopConnected = false startDeadline() loaded = false trace("route_ready") diff --git a/Sources/Panels/BrowserPanel+CloudConnection.swift b/Sources/Panels/BrowserPanel+CloudConnection.swift index a8d029c6083d..1f7153281be9 100644 --- a/Sources/Panels/BrowserPanel+CloudConnection.swift +++ b/Sources/Panels/BrowserPanel+CloudConnection.swift @@ -64,14 +64,14 @@ extension BrowserPanel { private static func cloudRestoredURL(_ preferred: URL?, on target: URL, isDisplay: Bool = false) -> URL { guard let preferred, var components = URLComponents(url: target, resolvingAgainstBaseURL: false), let saved = URLComponents(url: preferred, resolvingAgainstBaseURL: false) else { return target } - components.path = saved.path.isEmpty ? components.path : saved.path + if !saved.percentEncodedPath.isEmpty { components.percentEncodedPath = saved.percentEncodedPath } if isDisplay { let allowed = Set(["path", "autoconnect", "resize", "reconnect", "reconnect_delay"]) let safeItems = (saved.queryItems ?? []).filter { allowed.contains($0.name.lowercased()) } if !safeItems.isEmpty { components.queryItems = safeItems } } else { - components.query = saved.query - components.fragment = saved.fragment + components.percentEncodedQuery = saved.percentEncodedQuery + components.percentEncodedFragment = saved.percentEncodedFragment } return components.url ?? target } @@ -84,6 +84,17 @@ extension BrowserPanel { ) } + @discardableResult + func rebindCloudRouteIfNeeded(to url: URL) -> Bool { + guard let provider = SurfaceCatalog.shared.machines.values.first(where: { + $0.privateAddress?.trimmingCharacters(in: CharacterSet(charactersIn: "[]")) + == url.host?.trimmingCharacters(in: CharacterSet(charactersIn: "[]")) + }).flatMap({ SurfaceCatalog.shared.provider(for: $0.id) as? CmuxTuiSurfaceProvider }) else { + return false + } + return provider.configureBrowser(self, url: url) + } + /// Cloud panes use their own persistent data store so configuring one VM cannot reroute another. func prepareCloudBrowserStore(machineID: String) { let identifier = CloudBrowserRouting.storeID(panelID: id, profileID: profileID, machineID: machineID) diff --git a/Sources/Panels/BrowserPanel.swift b/Sources/Panels/BrowserPanel.swift index e5190fddfde4..5492adf4b553 100644 --- a/Sources/Panels/BrowserPanel.swift +++ b/Sources/Panels/BrowserPanel.swift @@ -3244,7 +3244,9 @@ final class BrowserPanel: Panel, ObservableObject { && !self.cloudAccess.owns($0) } == true if leavingCloudRoute { - self.leaveCloudResourceForLocalNavigation() + if let url = webView.url, self.rebindCloudRouteIfNeeded(to: url) == false { + self.leaveCloudResourceForLocalNavigation() + } } else { self.cloudAccess.didCommit(url: webView.url, navigationID: navigation.map(ObjectIdentifier.init)) } @@ -5408,6 +5410,7 @@ final class BrowserPanel: Panel, ObservableObject { recordTypedNavigation: Bool = false, onNavigationStarted: ((WKNavigation?) -> Void)? = nil ) -> WKNavigation? { + var leaveCloudRouteAfterValidation = false if cloudAccess.model != nil && cloudAccess.owns(url) { if cloudAccess.model?.isReady != true { return nil } prepareCloudBrowserNavigation() @@ -5418,7 +5421,7 @@ final class BrowserPanel: Panel, ObservableObject { provider.configureBrowser(self, url: url) return nil } else { - leaveCloudResourceForLocalNavigation() + leaveCloudRouteAfterValidation = retainsCloudResourceForDuplication } let request = URLRequest(url: url) let policy = BrowserURLAllowlistPolicy(defaults: .standard) @@ -5437,6 +5440,9 @@ final class BrowserPanel: Panel, ObservableObject { ) return nil } + if leaveCloudRouteAfterValidation { + leaveCloudResourceForLocalNavigation() + } return navigateWithoutInsecureHTTPPrompt( request: request, recordTypedNavigation: recordTypedNavigation, diff --git a/Sources/Surfaces/CmuxTuiSnapshotParser.swift b/Sources/Surfaces/CmuxTuiSnapshotParser.swift index f94df7168eb6..78c0b058d156 100644 --- a/Sources/Surfaces/CmuxTuiSnapshotParser.swift +++ b/Sources/Surfaces/CmuxTuiSnapshotParser.swift @@ -1755,7 +1755,8 @@ struct CmuxTuiSnapshotParser: Sendable { /// image's internal 8080 listener. /// Guest display slots use these private RFB/noVNC ports; they are owned by /// the display catalog and must never become generic forwarded-port rows. - static let internalPorts: Set = Set([22, 1337, 8080] + Array(5901...5916) + Array(6901...6916)) + static let internalPorts: Set = [22, 1337, 8080] + static let displayPorts: Set = Set(Array(5901...5916) + Array(6901...6916)) static let desktopPort = 6901 diff --git a/Sources/Surfaces/CmuxTuiSurfaceProvider+Displays.swift b/Sources/Surfaces/CmuxTuiSurfaceProvider+Displays.swift index 704bb0bb47e0..ce03e006b674 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProvider+Displays.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProvider+Displays.swift @@ -8,7 +8,7 @@ extension CmuxTuiSurfaceProvider { } var displayResources: [SurfaceResource] { - if let snapshot = displayCoordinator.snapshot { + if let snapshot = displayCoordinator.displaySnapshot { return snapshot.displays.map { $0.resource(on: machine, address: info.privateAddress) } } return [CmuxTuiSnapshotParser.display(machine: machine, diff --git a/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift b/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift index fed9be0a1017..b88c42e9d4ad 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift @@ -99,6 +99,10 @@ extension CmuxTuiSurfaceProvider { browser.cloudAccess.showUnavailable(String(localized: "browser.error.urlAllowlist.userMessage", defaultValue: "This URL is not allowed by the embedded-browser URL policy.")) return false } + if let existing = catalog.projectionRecord(forPanel: browser.id), existing.resource != resourceID { + catalog.endProjections(panelID: browser.id, reason: .replaced) + catalog.restore([SurfaceProjectionRecord(panelID: browser.id, resource: resourceID)], workspaceID: browser.workspaceId) + } let port = privateURL.port ?? (privateURL.scheme?.lowercased() == "https" ? 443 : 80) browser.webView.stopLoading() let model = accessModel(port: port, address: address, scheme: privateURL.scheme ?? "http") diff --git a/Sources/Surfaces/CmuxTuiSurfaceProviders.swift b/Sources/Surfaces/CmuxTuiSurfaceProviders.swift index bedb4ba49722..56f2229ad93a 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProviders.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProviders.swift @@ -266,7 +266,8 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { machine: machine, scannedPorts: scannedPorts, previousResources: previousResources, - privateAddress: summary.preferredPrivateAddress + privateAddress: summary.preferredPrivateAddress, + displayPortsOwned: hasDesktop ) resources = resourcesWithPendingCreations(parsed, state: cloudState) } else { @@ -275,7 +276,8 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { machine: machine, scannedPorts: scannedPorts, previousResources: previousResources, - privateAddress: summary.preferredPrivateAddress + privateAddress: summary.preferredPrivateAddress, + displayPortsOwned: hasDesktop )) appendMissingResources(preservedNonPortResources, to: &fallback) resources = resourcesWithPendingCreations(fallback, state: nil) @@ -744,7 +746,8 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { machine: machine, scannedPorts: ports, previousResources: catalog.authoritativeSnapshot.resources(on: machine), - privateAddress: summary.preferredPrivateAddress + privateAddress: summary.preferredPrivateAddress, + displayPortsOwned: summary.resolvedKind.hasDesktop ) } diff --git a/Sources/Surfaces/DockSplitStore+SurfaceOwnership.swift b/Sources/Surfaces/DockSplitStore+SurfaceOwnership.swift index 2b39f0439732..64ae8d7c0edb 100644 --- a/Sources/Surfaces/DockSplitStore+SurfaceOwnership.swift +++ b/Sources/Surfaces/DockSplitStore+SurfaceOwnership.swift @@ -6,8 +6,9 @@ extension DockSplitStore { } func acceptsUnownedBrowserURL(_ url: URL?) -> Bool { + if scope == .global { return true } guard url?.path == "/vnc.html" else { return true } - return scope != .global && surfaceOwnershipPolicy.rejection(for: nil) == nil + return surfaceOwnershipPolicy.rejection(for: nil) == nil } func surfaceDropRejection(_ transfer: PaneDragTransfer, source: PaneTransferSourceResolver.Source) -> SurfaceTransferRejection? { @@ -34,7 +35,7 @@ extension DockSplitStore { return surfaceOwnershipPolicy.rejection(for: resource.machine) == nil } if let raw = snapshot.browser?.urlString, URL(string: raw)?.path == "/vnc.html" { - return scope != .global && surfaceOwnershipPolicy.rejection(for: nil) == nil + return scope == .global || surfaceOwnershipPolicy.rejection(for: nil) == nil } return true } diff --git a/Sources/Surfaces/SurfaceCatalog+CloudPorts.swift b/Sources/Surfaces/SurfaceCatalog+CloudPorts.swift index 9aeea59eafd7..d71683b7cf88 100644 --- a/Sources/Surfaces/SurfaceCatalog+CloudPorts.swift +++ b/Sources/Surfaces/SurfaceCatalog+CloudPorts.swift @@ -282,13 +282,15 @@ extension CmuxTuiSurfaceProvider { machine: SurfaceMachineID, scannedPorts: [Int]?, previousResources: [SurfaceResource], - privateAddress: String? + privateAddress: String?, + displayPortsOwned: Bool = false ) -> [SurfaceResource] { let previous: [SurfaceResourceID: SurfaceResource] = Dictionary( uniqueKeysWithValues: previousResources .filter { $0.id.isForwardedPort && !CmuxTuiSnapshotParser.internalPorts.contains($0.id.forwardedPort ?? -1) + && (!displayPortsOwned || !CmuxTuiSnapshotParser.displayPorts.contains($0.id.forwardedPort ?? -1)) } .map { ($0.id, $0) } ) @@ -309,6 +311,7 @@ extension CmuxTuiSurfaceProvider { return scannedPorts .filter { (port: Int) in (1...65_535).contains(port) && seen.insert(port).inserted + && (!displayPortsOwned || !CmuxTuiSnapshotParser.displayPorts.contains(port)) } .sorted(by: <) .map { (port: Int) -> SurfaceResource in diff --git a/Sources/Surfaces/SurfaceCatalog+Ownership.swift b/Sources/Surfaces/SurfaceCatalog+Ownership.swift index 2f2f94105bcd..c65ebd8f6347 100644 --- a/Sources/Surfaces/SurfaceCatalog+Ownership.swift +++ b/Sources/Surfaces/SurfaceCatalog+Ownership.swift @@ -38,10 +38,13 @@ extension SurfaceCatalog { } func validateOwnership(of resources: [SurfaceResourceID], at destination: SurfaceDestination) throws { - let workspace = cloudWorkspaceRenameService.environment.workspace(destination.workspaceID) - ?? Workspace.liveWorkspace(id: destination.workspaceID) - if let policy = workspace?.surfaceOwnershipPolicy, - let rejection = ownershipRejection(for: resources, policy: policy) { throw rejection } + guard let workspace = cloudWorkspaceRenameService.environment.workspace(destination.workspaceID) + ?? Workspace.liveWorkspace(id: destination.workspaceID) else { + throw SurfaceCatalogError.destinationNotFound(destination.workspaceID.uuidString) + } + if let rejection = ownershipRejection(for: resources, policy: workspace.surfaceOwnershipPolicy) { + throw rejection + } } /// Provider work may suspend. Check the live destination again before its diff --git a/tests/fixtures/cmux-display b/tests/fixtures/cmux-display index afb2b1ecf9ed..346d0816ec40 100755 --- a/tests/fixtures/cmux-display +++ b/tests/fixtures/cmux-display @@ -11,6 +11,7 @@ import http.client import json import os from pathlib import Path +import re import shutil import socket import socketserver @@ -255,14 +256,14 @@ class DisplayService: except OSError: pass - def recover_processes(self, number): + def recover_processes(self, number, runtime): self.processes[number] = [] self.named_processes[number] = {} uid = str(os.getuid()) for name in ("openbox", "tint2", "vncconfig"): - marker = f"cmux-display-{number}-{name}" + marker = str(Path(runtime) / "bin" / f"cmux-display-{number}-{name}") try: - pattern = rf"(^|/| ){marker}([[:space:]]|$)" + pattern = rf"^{re.escape(marker)}(?:[[:space:]]|$)" output = subprocess.check_output(["pgrep", "-u", uid, "-f", pattern], text=True) pid = int(output.splitlines()[0]) except (OSError, ValueError, IndexError, subprocess.CalledProcessError): @@ -270,7 +271,8 @@ class DisplayService: self.track_process(number, name, OwnedProcess(pid)) try: output = subprocess.check_output( - ["pgrep", "-u", uid, "-f", rf"(?:Xvnc|Xtigervnc).*:{number}(?:\s|$)"], + ["pgrep", "-u", uid, "-f", + rf"^(?:.*\/)?(?:Xvnc|Xtigervnc).*:{number}.*-rfbport\s+{5900 + number}(?:\s|$)"], text=True ) pid = int(output.splitlines()[0]) @@ -342,7 +344,7 @@ class DisplayService: if not rfb_is_ready and not vnc: raise RuntimeError("display runtime unavailable") if rfb_is_ready and number not in self.named_processes: - self.recover_processes(number) + self.recover_processes(number, runtime) if not rfb_is_ready: self.terminate(number) rfb_port = 5900 + number @@ -401,9 +403,10 @@ class DisplayService: self.terminate(number) def snapshot(self): + primary_state = "running" if ready(1) else "unavailable" with self.lock: return {"version": 1, "canCreate": len(self.jobs) < MAX_DISPLAYS - 1, - "displays": [descriptor(1, "running" if ready(1) else "unavailable")] + "displays": [descriptor(1, primary_state)] + [descriptor(n, self.states.get(n, "launching")) for n in self.catalog.numbers()]} def handle(self, request): From 4795f0f5f53ba97f930e6cffa5a80545d2d5931f Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 18:13:50 -0700 Subject: [PATCH 48/64] test: cover display port ownership and recovery --- cmuxTests/CmuxTuiSurfaceProviderTests.swift | 3 ++- tests/test_cloud_display_catalog.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cmuxTests/CmuxTuiSurfaceProviderTests.swift b/cmuxTests/CmuxTuiSurfaceProviderTests.swift index 481da33ec782..274ce3191eaa 100644 --- a/cmuxTests/CmuxTuiSurfaceProviderTests.swift +++ b/cmuxTests/CmuxTuiSurfaceProviderTests.swift @@ -1009,7 +1009,8 @@ import Testing LISTEN 0 128 0.0.0.0:3000 0.0.0.0:* """ #expect(CmuxTuiSnapshotParser.listeningPorts(fromSocketListing: ss) == [1337, 3000, 5901]) - #expect(CmuxTuiSnapshotParser.internalPorts.isSuperset(of: [1337, 5901, 5902, 5916, 6901, 6902, 6916])) + #expect(CmuxTuiSnapshotParser.internalPorts.isSuperset(of: [1337])) + #expect(CmuxTuiSnapshotParser.displayPorts.isSuperset(of: [5901, 5902, 5916, 6901, 6902, 6916])) #expect(CmuxTuiSnapshotParser.machineHasDesktop(image: "cmux-xfce-vnc:latest")) #expect(!CmuxTuiSnapshotParser.machineHasDesktop(image: "cmuxd-ws:tooling-20260509f")) diff --git a/tests/test_cloud_display_catalog.py b/tests/test_cloud_display_catalog.py index 42fa9f1850bd..18589a5a9d85 100644 --- a/tests/test_cloud_display_catalog.py +++ b/tests/test_cloud_display_catalog.py @@ -213,7 +213,7 @@ def pgrep(command, **_options): raise display.subprocess.CalledProcessError(1, command) with mock.patch.object(display.subprocess, "check_output", side_effect=pgrep): - service.recover_processes(2, self.root / "runtime" / "2") + service.recover_processes(2, self.root / "runtime" / "2", {"DISPLAY": ":2"}) self.assertIn("openbox", service.named_processes[2]) def test_additional_desktop_clients_use_display_scoped_process_names(self): From cbe486309dc5fd20b6ee466629c0c5455ca11ad0 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 18:14:57 -0700 Subject: [PATCH 49/64] fix: harden Cloud display supervisor and route lifecycle --- Sources/Cloud/CloudGuestDisplayScript.swift | 329 +++++++++--------- .../PortForward/CloudBrowserAccessState.swift | 3 +- .../Surfaces/CmuxTuiSurfaceProviders.swift | 9 +- tests/fixtures/cmux-display | 30 +- 4 files changed, 200 insertions(+), 171 deletions(-) diff --git a/Sources/Cloud/CloudGuestDisplayScript.swift b/Sources/Cloud/CloudGuestDisplayScript.swift index ebbcfe3455ca..7505edaa699c 100644 --- a/Sources/Cloud/CloudGuestDisplayScript.swift +++ b/Sources/Cloud/CloudGuestDisplayScript.swift @@ -74,165 +74,176 @@ aW5nIgogICAgICAgICAgICB0aHJlYWRpbmcuVGhyZWFkKHRhcmdldD1zZWxmLnN1cGVydmlzZSwgYXJn cnVlKS5zdGFydCgpCiAgICAgICAgICAgIHJldHVybiBmaW5pc2hlZAoKICAgIGRlZiB0ZXJtaW5hdGUoc2VsZiwgbnVtYmVyKToKICAgICAgICBmb3IgcHJv Y2VzcyBpbiBzZWxmLnByb2Nlc3Nlcy5wb3AobnVtYmVyLCBbXSk6CiAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgIHByb2Nlc3MudGVybWluYXRl KCkKICAgICAgICAgICAgZXhjZXB0IE9TRXJyb3I6CiAgICAgICAgICAgICAgICBwYXNzCiAgICAgICAgc2VsZi5uYW1lZF9wcm9jZXNzZXMucG9wKG51bWJl -ciwgTm9uZSkKICAgICAgICBzZWxmLnRlcm1pbmF0ZV93ZWJzb2NraWZ5KG51bWJlcikKCiAgICBkZWYgdGVybWluYXRlX3dlYnNvY2tpZnkoc2VsZiwgbnVt -YmVyKToKICAgICAgICBwcm9jZXNzID0gc2VsZi53ZWJzb2NraWZ5X3Byb2Nlc3Nlcy5wb3AobnVtYmVyLCBOb25lKQogICAgICAgIGlmIHByb2Nlc3MgaXMg -Tm9uZToKICAgICAgICAgICAgcmV0dXJuCiAgICAgICAgdHJ5OgogICAgICAgICAgICBwcm9jZXNzLnRlcm1pbmF0ZSgpCiAgICAgICAgZXhjZXB0IE9TRXJy -b3I6CiAgICAgICAgICAgIHBhc3MKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgc2NvcGVkX2NvbXBvbmVudF9wYXRoKGV4ZWN1dGFibGUsIHBhdGgsIG51 -bWJlciwgcnVudGltZSk6CiAgICAgICAgIiIiR2l2ZSBwZXItZGlzcGxheSBkZXNrdG9wIGNsaWVudHMgbmFtZXMgdGhlIDoxIHN1cGVydmlzb3IgY2Fubm90 -IG1hdGNoLiIiIgogICAgICAgIHNvdXJjZSA9IFBhdGgocGF0aCkKICAgICAgICBpZiBub3Qgc291cmNlLmlzX2ZpbGUoKToKICAgICAgICAgICAgcmV0dXJu -IHBhdGgKICAgICAgICBkaXJlY3RvcnkgPSBQYXRoKHJ1bnRpbWUpIC8gImJpbiIKICAgICAgICBkaXJlY3RvcnkubWtkaXIobW9kZT0wbzcwMCwgcGFyZW50 -cz1UcnVlLCBleGlzdF9vaz1UcnVlKQogICAgICAgIHRhcmdldCA9IGRpcmVjdG9yeSAvIGYiY211eC1kaXNwbGF5LXtudW1iZXJ9LXtzb3VyY2UubmFtZX0i -CiAgICAgICAgaWYgbm90IHRhcmdldC5leGlzdHMoKSBvciB0YXJnZXQuc3RhdCgpLnN0X210aW1lX25zIDwgc291cmNlLnN0YXQoKS5zdF9tdGltZV9uczoK -ICAgICAgICAgICAgc2h1dGlsLmNvcHkyKHNvdXJjZSwgdGFyZ2V0KQogICAgICAgICAgICB0YXJnZXQuY2htb2QoMG83MDApCiAgICAgICAgcmV0dXJuIHN0 -cih0YXJnZXQpCgogICAgQHN0YXRpY21ldGhvZAogICAgZGVmIHdhaXRfZm9yX3BvcnQocG9ydCwgdGltZW91dD0yMCk6CiAgICAgICAgZGVhZGxpbmUgPSB0 -aW1lLm1vbm90b25pYygpICsgdGltZW91dAogICAgICAgIHdoaWxlIHRpbWUubW9ub3RvbmljKCkgPCBkZWFkbGluZToKICAgICAgICAgICAgaWYgbGlzdGVu -aW5nKHBvcnQpOgogICAgICAgICAgICAgICAgcmV0dXJuIFRydWUKICAgICAgICAgICAgdGltZS5zbGVlcCgwLjEpCiAgICAgICAgcmV0dXJuIEZhbHNlCgog -ICAgQHN0YXRpY21ldGhvZAogICAgZGVmIHByb2Nlc3NfYWxpdmUocHJvY2Vzcyk6CiAgICAgICAgcG9sbCA9IGdldGF0dHIocHJvY2VzcywgInBvbGwiLCBO -b25lKQogICAgICAgIHJldHVybiBwb2xsIGlzIE5vbmUgb3IgcG9sbCgpIGlzIE5vbmUKCiAgICBkZWYgY29tcG9uZW50c19hbGl2ZShzZWxmLCBudW1iZXIp -OgogICAgICAgIG5hbWVkID0gc2VsZi5uYW1lZF9wcm9jZXNzZXMuZ2V0KG51bWJlcikKICAgICAgICBpZiBuYW1lZCBpcyBub3QgTm9uZToKICAgICAgICAg -ICAgcmV0dXJuIGJvb2wobmFtZWQpIGFuZCBhbGwoc2VsZi5wcm9jZXNzX2FsaXZlKHByb2Nlc3MpIGZvciBwcm9jZXNzIGluIG5hbWVkLnZhbHVlcygpKSBc -CiAgICAgICAgICAgICAgICBhbmQgKCJ4dm5jIiBpbiBuYW1lZCBvciByZmJfcmVhZHkobnVtYmVyKSkKICAgICAgICBwcm9jZXNzZXMgPSBzZWxmLnByb2Nl -c3Nlcy5nZXQobnVtYmVyLCBbXSkKICAgICAgICByZXR1cm4gYm9vbChwcm9jZXNzZXMpIGFuZCBhbGwoc2VsZi5wcm9jZXNzX2FsaXZlKHByb2Nlc3MpIGZv -ciBwcm9jZXNzIGluIHByb2Nlc3NlcykKCiAgICBkZWYgc2VydmljZV9yZWFkeShzZWxmLCBudW1iZXIpOgogICAgICAgIGlmIG5vdCByZWFkeShudW1iZXIp -IG9yIG5vdCBzZWxmLmNvbXBvbmVudHNfYWxpdmUobnVtYmVyKToKICAgICAgICAgICAgcmV0dXJuIEZhbHNlCiAgICAgICAgd2Vic29ja2lmeSA9IHNlbGYu -d2Vic29ja2lmeV9wcm9jZXNzZXMuZ2V0KG51bWJlcikKICAgICAgICByZXR1cm4gd2Vic29ja2lmeSBpcyBOb25lIG9yIHNlbGYucHJvY2Vzc19hbGl2ZSh3 -ZWJzb2NraWZ5KQoKICAgIGRlZiB0cmFja19wcm9jZXNzKHNlbGYsIG51bWJlciwgbmFtZSwgcHJvY2Vzcyk6CiAgICAgICAgbmFtZWQgPSBzZWxmLm5hbWVk -X3Byb2Nlc3Nlcy5zZXRkZWZhdWx0KG51bWJlciwge30pCiAgICAgICAgcHJldmlvdXMgPSBuYW1lZC5nZXQobmFtZSkKICAgICAgICBpZiBwcmV2aW91cyBp -cyBub3QgTm9uZToKICAgICAgICAgICAgc2VsZi5wcm9jZXNzZXNbbnVtYmVyXSA9IFsKICAgICAgICAgICAgICAgIGl0ZW0gZm9yIGl0ZW0gaW4gc2VsZi5w -cm9jZXNzZXMuZ2V0KG51bWJlciwgW10pIGlmIGl0ZW0gaXMgbm90IHByZXZpb3VzCiAgICAgICAgICAgIF0KICAgICAgICBzZWxmLnByb2Nlc3Nlcy5zZXRk -ZWZhdWx0KG51bWJlciwgW10pLmFwcGVuZChwcm9jZXNzKQogICAgICAgIG5hbWVkW25hbWVdID0gcHJvY2VzcwoKICAgIGRlZiByZW1vdmVfcHJvY2Vzcyhz -ZWxmLCBudW1iZXIsIG5hbWUpOgogICAgICAgIHByb2Nlc3MgPSBzZWxmLm5hbWVkX3Byb2Nlc3Nlcy5zZXRkZWZhdWx0KG51bWJlciwge30pLnBvcChuYW1l -LCBOb25lKQogICAgICAgIGlmIHByb2Nlc3MgaXMgTm9uZToKICAgICAgICAgICAgcmV0dXJuCiAgICAgICAgc2VsZi5wcm9jZXNzZXNbbnVtYmVyXSA9IFtp -dGVtIGZvciBpdGVtIGluIHNlbGYucHJvY2Vzc2VzLmdldChudW1iZXIsIFtdKSBpZiBpdGVtIGlzIG5vdCBwcm9jZXNzXQogICAgICAgIHRyeToKICAgICAg -ICAgICAgcHJvY2Vzcy50ZXJtaW5hdGUoKQogICAgICAgIGV4Y2VwdCBPU0Vycm9yOgogICAgICAgICAgICBwYXNzCgogICAgZGVmIHJlY292ZXJfcHJvY2Vz -c2VzKHNlbGYsIG51bWJlciwgcnVudGltZSk6CiAgICAgICAgc2VsZi5wcm9jZXNzZXNbbnVtYmVyXSA9IFtdCiAgICAgICAgc2VsZi5uYW1lZF9wcm9jZXNz -ZXNbbnVtYmVyXSA9IHt9CiAgICAgICAgdWlkID0gc3RyKG9zLmdldHVpZCgpKQogICAgICAgIGZvciBuYW1lIGluICgib3BlbmJveCIsICJ0aW50MiIsICJ2 -bmNjb25maWciKToKICAgICAgICAgICAgbWFya2VyID0gc3RyKFBhdGgocnVudGltZSkgLyAiYmluIiAvIGYiY211eC1kaXNwbGF5LXtudW1iZXJ9LXtuYW1l -fSIpCiAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgIHBhdHRlcm4gPSByZiJee3JlLmVzY2FwZShtYXJrZXIpfSg/OltbOnNwYWNlOl1dfCQpIgog -ICAgICAgICAgICAgICAgb3V0cHV0ID0gc3VicHJvY2Vzcy5jaGVja19vdXRwdXQoWyJwZ3JlcCIsICItdSIsIHVpZCwgIi1mIiwgcGF0dGVybl0sIHRleHQ9 -VHJ1ZSkKICAgICAgICAgICAgICAgIHBpZCA9IGludChvdXRwdXQuc3BsaXRsaW5lcygpWzBdKQogICAgICAgICAgICBleGNlcHQgKE9TRXJyb3IsIFZhbHVl -RXJyb3IsIEluZGV4RXJyb3IsIHN1YnByb2Nlc3MuQ2FsbGVkUHJvY2Vzc0Vycm9yKToKICAgICAgICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgICAgIHNl -bGYudHJhY2tfcHJvY2VzcyhudW1iZXIsIG5hbWUsIE93bmVkUHJvY2VzcyhwaWQpKQogICAgICAgIHRyeToKICAgICAgICAgICAgb3V0cHV0ID0gc3VicHJv -Y2Vzcy5jaGVja19vdXRwdXQoCiAgICAgICAgICAgICAgICBbInBncmVwIiwgIi11IiwgdWlkLCAiLWYiLAogICAgICAgICAgICAgICAgIHJmIl4oPzouKlwv -KT8oPzpYdm5jfFh0aWdlcnZuYykuKjp7bnVtYmVyfS4qLXJmYnBvcnRccyt7NTkwMCArIG51bWJlcn0oPzpcc3wkKSJdLAogICAgICAgICAgICAgICAgdGV4 -dD1UcnVlCiAgICAgICAgICAgICkKICAgICAgICAgICAgcGlkID0gaW50KG91dHB1dC5zcGxpdGxpbmVzKClbMF0pCiAgICAgICAgZXhjZXB0IChPU0Vycm9y -LCBWYWx1ZUVycm9yLCBJbmRleEVycm9yLCBzdWJwcm9jZXNzLkNhbGxlZFByb2Nlc3NFcnJvcik6CiAgICAgICAgICAgIHBpZCA9IDAKICAgICAgICBpZiBw -aWQgPiAxOgogICAgICAgICAgICBzZWxmLnRyYWNrX3Byb2Nlc3MobnVtYmVyLCAieHZuYyIsIE93bmVkUHJvY2VzcyhwaWQpKQoKICAgIGRlZiBzdGFydF9k -YnVzKHNlbGYsIG51bWJlciwgZW52aXJvbm1lbnQpOgogICAgICAgIGJ1cyA9IHN1YnByb2Nlc3MucnVuKFsiZGJ1cy1sYXVuY2giLCAiLS1zaC1zeW50YXgi -XSwgZW52PWVudmlyb25tZW50LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0ZGluPXN1YnByb2Nlc3MuREVWTlVMTCwgc3Rkb3V0PXN1YnByb2Nl -c3MuUElQRSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdGRlcnI9c3VicHJvY2Vzcy5ERVZOVUxMLCB0ZXh0PVRydWUsIHRpbWVvdXQ9MTAsCiAg -ICAgICAgICAgICAgICAgICAgICAgICAgICAgY2hlY2s9VHJ1ZSkuc3Rkb3V0CiAgICAgICAgZm9yIGxpbmUgaW4gYnVzLnNwbGl0bGluZXMoKToKICAgICAg -ICAgICAgaWYgIj0iIG5vdCBpbiBsaW5lOgogICAgICAgICAgICAgICAgY29udGludWUKICAgICAgICAgICAga2V5LCB2YWx1ZSA9IGxpbmUuc3BsaXQoIj0i -LCAxKQogICAgICAgICAgICBlbnZpcm9ubWVudFtrZXldID0gdmFsdWUuc3RyaXAoKS5yc3RyaXAoIjsiKS5zdHJpcCgpLnN0cmlwKCInIikKICAgICAgICB0 -cnk6CiAgICAgICAgICAgIGRidXNfcGlkID0gaW50KGVudmlyb25tZW50LmdldCgiREJVU19TRVNTSU9OX0JVU19QSUQiLCAiMCIpKQogICAgICAgIGV4Y2Vw -dCBWYWx1ZUVycm9yOgogICAgICAgICAgICBkYnVzX3BpZCA9IDAKICAgICAgICBpZiBkYnVzX3BpZCA+IDE6CiAgICAgICAgICAgIHNlbGYudHJhY2tfcHJv -Y2VzcyhudW1iZXIsICJkYnVzIiwgT3duZWRQcm9jZXNzKGRidXNfcGlkKSkKCiAgICBkZWYgc3RhcnRfc2Vzc2lvbl9jb21wb25lbnRzKHNlbGYsIG51bWJl -ciwgZW52aXJvbm1lbnQsIHJ1bnRpbWUpOgogICAgICAgIGZvciBleGVjdXRhYmxlLCBhcmdzIGluIFsKICAgICAgICAgICAgKCJvcGVuYm94IiwgW10pLCAo -InRpbnQyIiwgWyItYyIsICIvZXRjL2NtdXgvdGludDJyYyJdKSwKICAgICAgICAgICAgKCJ2bmNjb25maWciLCBbIi1ub3dpbiJdKSwKICAgICAgICBdOgog -ICAgICAgICAgICBleGlzdGluZyA9IHNlbGYubmFtZWRfcHJvY2Vzc2VzLmdldChudW1iZXIsIHt9KS5nZXQoZXhlY3V0YWJsZSkKICAgICAgICAgICAgaWYg -ZXhpc3RpbmcgaXMgbm90IE5vbmUgYW5kIHNlbGYucHJvY2Vzc19hbGl2ZShleGlzdGluZyk6CiAgICAgICAgICAgICAgICBjb250aW51ZQogICAgICAgICAg -ICBwYXRoID0gZXhlY3V0YWJsZSBpZiAiLyIgaW4gZXhlY3V0YWJsZSBlbHNlIHNodXRpbC53aGljaChleGVjdXRhYmxlKQogICAgICAgICAgICBpZiBub3Qg -cGF0aDoKICAgICAgICAgICAgICAgIGlmIGV4ZWN1dGFibGUgPT0gInZuY2NvbmZpZyI6CiAgICAgICAgICAgICAgICAgICAgY29udGludWUKICAgICAgICAg -ICAgICAgIHJhaXNlIFJ1bnRpbWVFcnJvcihmIm1pc3NpbmcgZGlzcGxheSBjb21wb25lbnQ6IHtleGVjdXRhYmxlfSIpCiAgICAgICAgICAgIHBhdGggPSBz -ZWxmLnNjb3BlZF9jb21wb25lbnRfcGF0aChleGVjdXRhYmxlLCBwYXRoLCBudW1iZXIsIHJ1bnRpbWUpCiAgICAgICAgICAgIHByb2Nlc3MgPSBzdWJwcm9j -ZXNzLlBvcGVuKFtwYXRoXSArIGFyZ3MsIGVudj1lbnZpcm9ubWVudCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3RkaW49c3Vi -cHJvY2Vzcy5ERVZOVUxMLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdGRvdXQ9c3VicHJvY2Vzcy5ERVZOVUxMLCBzdGRlcnI9 -c3VicHJvY2Vzcy5ERVZOVUxMKQogICAgICAgICAgICBzZWxmLnRyYWNrX3Byb2Nlc3MobnVtYmVyLCBleGVjdXRhYmxlLCBwcm9jZXNzKQoKICAgIGRlZiBz -dGFydF93ZWJzb2NraWZ5KHNlbGYsIG51bWJlciwgZW52aXJvbm1lbnQsIGV4ZWN1dGFibGUpOgogICAgICAgIHBhdGggPSBleGVjdXRhYmxlIGlmICIvIiBp -biBleGVjdXRhYmxlIGVsc2Ugc2h1dGlsLndoaWNoKGV4ZWN1dGFibGUpCiAgICAgICAgaWYgbm90IHBhdGg6CiAgICAgICAgICAgIHJhaXNlIE5vVk5DVW5h -dmFpbGFibGUoIm1pc3NpbmcgZGlzcGxheSBjb21wb25lbnQ6IHdlYnNvY2tpZnkiKQogICAgICAgIHNlbGYudGVybWluYXRlX3dlYnNvY2tpZnkobnVtYmVy -KQogICAgICAgIHJmYl9wb3J0LCBub3ZuY19wb3J0ID0gNTkwMCArIG51bWJlciwgNjkwMCArIG51bWJlcgogICAgICAgIHByb2Nlc3MgPSBzdWJwcm9jZXNz -LlBvcGVuKAogICAgICAgICAgICBbcGF0aCwgIi0td2ViIiwgIi91c3Ivc2hhcmUvbm92bmMiLCAiLS1oZWFydGJlYXQiLCAiMzAiLAogICAgICAgICAgICAg -ZiIxMjcuMC4wLjE6e25vdm5jX3BvcnR9IiwgZiIxMjcuMC4wLjE6e3JmYl9wb3J0fSJdLAogICAgICAgICAgICBlbnY9ZW52aXJvbm1lbnQsIHN0ZGluPXN1 -YnByb2Nlc3MuREVWTlVMTCwKICAgICAgICAgICAgc3Rkb3V0PXN1YnByb2Nlc3MuREVWTlVMTCwgc3RkZXJyPXN1YnByb2Nlc3MuREVWTlVMTCkKICAgICAg -ICBzZWxmLndlYnNvY2tpZnlfcHJvY2Vzc2VzW251bWJlcl0gPSBwcm9jZXNzCiAgICAgICAgaWYgbm90IHNlbGYud2FpdF9mb3JfcG9ydChub3ZuY19wb3J0 -LCB0aW1lb3V0PTEwKToKICAgICAgICAgICAgc2VsZi50ZXJtaW5hdGVfd2Vic29ja2lmeShudW1iZXIpCiAgICAgICAgICAgIHJhaXNlIE5vVk5DVW5hdmFp -bGFibGUoIm5vVk5DIGRpZCBub3QgYmVjb21lIHJlYWR5IikKCiAgICBkZWYgc3RhcnRfY29tcG9uZW50cyhzZWxmLCBudW1iZXIsIGVudmlyb25tZW50LCBy -dW50aW1lKToKICAgICAgICBpZiBzZWxmLnNlcnZpY2VfcmVhZHkobnVtYmVyKToKICAgICAgICAgICAgcmV0dXJuCiAgICAgICAgcmZiX2lzX3JlYWR5ID0g -cmZiX3JlYWR5KG51bWJlcikKICAgICAgICB2bmMgPSBzaHV0aWwud2hpY2goIlh2bmMiKSBvciBzaHV0aWwud2hpY2goIlh0aWdlcnZuYyIpCiAgICAgICAg -d2Vic29ja2lmeSA9IHNodXRpbC53aGljaCgid2Vic29ja2lmeSIpCiAgICAgICAgaWYgbm90IHdlYnNvY2tpZnk6CiAgICAgICAgICAgIHJhaXNlIE5vVk5D -VW5hdmFpbGFibGUoImRpc3BsYXkgcnVudGltZSB1bmF2YWlsYWJsZSIpCiAgICAgICAgaWYgbm90IHJmYl9pc19yZWFkeSBhbmQgbm90IHZuYzoKICAgICAg -ICAgICAgcmFpc2UgUnVudGltZUVycm9yKCJkaXNwbGF5IHJ1bnRpbWUgdW5hdmFpbGFibGUiKQogICAgICAgIGlmIHJmYl9pc19yZWFkeSBhbmQgbnVtYmVy -IG5vdCBpbiBzZWxmLm5hbWVkX3Byb2Nlc3NlczoKICAgICAgICAgICAgc2VsZi5yZWNvdmVyX3Byb2Nlc3NlcyhudW1iZXIsIHJ1bnRpbWUpCiAgICAgICAg -aWYgbm90IHJmYl9pc19yZWFkeToKICAgICAgICAgICAgc2VsZi50ZXJtaW5hdGUobnVtYmVyKQogICAgICAgICAgICByZmJfcG9ydCA9IDU5MDAgKyBudW1i -ZXIKICAgICAgICAgICAgc2VsZi5wcm9jZXNzZXNbbnVtYmVyXSA9IFtdCiAgICAgICAgICAgIHNlbGYubmFtZWRfcHJvY2Vzc2VzW251bWJlcl0gPSB7fQog -ICAgICAgICAgICBzZWxmLnRyYWNrX3Byb2Nlc3MobnVtYmVyLCAieHZuYyIsIHN1YnByb2Nlc3MuUG9wZW4oCiAgICAgICAgICAgICAgICBbdm5jLCBmIjp7 -bnVtYmVyfSIsICItZ2VvbWV0cnkiLCBvcy5lbnZpcm9uLmdldCgiQ01VWF9WTkNfR0VPTUVUUlkiLCAiMTQ0MHg5MDAiKSwKICAgICAgICAgICAgICAgICAi -LWRlcHRoIiwgIjI0IiwgIi1yZmJwb3J0Iiwgc3RyKHJmYl9wb3J0KSwgIi1sb2NhbGhvc3QiLAogICAgICAgICAgICAgICAgICItU2VjdXJpdHlUeXBlcyIs -ICJOb25lIiwgIi1BbHdheXNTaGFyZWQiXSwKICAgICAgICAgICAgICAgIGVudj1lbnZpcm9ubWVudCwgc3RkaW49c3VicHJvY2Vzcy5ERVZOVUxMLAogICAg -ICAgICAgICAgICAgc3Rkb3V0PXN1YnByb2Nlc3MuREVWTlVMTCwgc3RkZXJyPXN1YnByb2Nlc3MuREVWTlVMTCkpCiAgICAgICAgICAgIGlmIG5vdCBzZWxm -LndhaXRfZm9yX3BvcnQocmZiX3BvcnQpOgogICAgICAgICAgICAgICAgcmFpc2UgUnVudGltZUVycm9yKCJYdm5jIGRpZCBub3QgYmVjb21lIHJlYWR5IikK -ICAgICAgICAgICAgc2VsZi5zdGFydF9kYnVzKG51bWJlciwgZW52aXJvbm1lbnQpCiAgICAgICAgICAgIHNlbGYuc3RhcnRfc2Vzc2lvbl9jb21wb25lbnRz -KG51bWJlciwgZW52aXJvbm1lbnQsIHJ1bnRpbWUpCiAgICAgICAgZWxpZiBudW1iZXIgaW4gc2VsZi5uYW1lZF9wcm9jZXNzZXM6CiAgICAgICAgICAgIG5h -bWVkID0gc2VsZi5uYW1lZF9wcm9jZXNzZXNbbnVtYmVyXQogICAgICAgICAgICBkYnVzID0gbmFtZWQuZ2V0KCJkYnVzIikKICAgICAgICAgICAgaWYgZGJ1 -cyBpcyBOb25lIG9yIG5vdCBzZWxmLnByb2Nlc3NfYWxpdmUoZGJ1cyk6CiAgICAgICAgICAgICAgICBmb3IgbmFtZSBpbiAoIm9wZW5ib3giLCAidGludDIi -LCAidm5jY29uZmlnIik6CiAgICAgICAgICAgICAgICAgICAgc2VsZi5yZW1vdmVfcHJvY2VzcyhudW1iZXIsIG5hbWUpCiAgICAgICAgICAgICAgICBzZWxm -LnN0YXJ0X2RidXMobnVtYmVyLCBlbnZpcm9ubWVudCkKICAgICAgICAgICAgc2VsZi5zdGFydF9zZXNzaW9uX2NvbXBvbmVudHMobnVtYmVyLCBlbnZpcm9u -bWVudCwgcnVudGltZSkKICAgICAgICB3ZWJzb2NraWZ5X3Byb2Nlc3MgPSBzZWxmLndlYnNvY2tpZnlfcHJvY2Vzc2VzLmdldChudW1iZXIpCiAgICAgICAg -aWYgKHdlYnNvY2tpZnlfcHJvY2VzcyBpcyBOb25lIG9yIG5vdCBzZWxmLnByb2Nlc3NfYWxpdmUod2Vic29ja2lmeV9wcm9jZXNzKQogICAgICAgICAgICAg -ICAgb3Igbm90IG5vdm5jX3JlYWR5KG51bWJlcikpOgogICAgICAgICAgICBzZWxmLnN0YXJ0X3dlYnNvY2tpZnkobnVtYmVyLCBlbnZpcm9ubWVudCwgd2Vi -c29ja2lmeSkKICAgICAgICAocnVudGltZSAvICJlbnYiKS53cml0ZV90ZXh0KGYiZXhwb3J0IERJU1BMQVk9OntudW1iZXJ9XG4iKQoKICAgIGRlZiBzdXBl -cnZpc2Uoc2VsZiwgbnVtYmVyLCBmaW5pc2hlZCk6CiAgICAgICAgcnVudGltZSA9IHNlbGYucnVudGltZSAvIHN0cihudW1iZXIpCiAgICAgICAgcnVudGlt -ZS5ta2Rpcihtb2RlPTBvNzAwLCBwYXJlbnRzPVRydWUsIGV4aXN0X29rPVRydWUpCiAgICAgICAgZW52aXJvbm1lbnQgPSBkaWN0KG9zLmVudmlyb24sIERJ -U1BMQVk9ZiI6e251bWJlcn0iLCBDTVVYX0RFU0tUT1BfUlVOVElNRV9ESVI9c3RyKHJ1bnRpbWUpKQogICAgICAgIGVudmlyb25tZW50WyJDSFJPTUVfQ09O -RklHX0hPTUUiXSA9IHN0cihydW50aW1lIC8gImNocm9tZS1jb25maWciKQogICAgICAgIGVudmlyb25tZW50WyJYREdfQ09ORklHX0hPTUUiXSA9IHN0cihy -dW50aW1lIC8gInhkZy1jb25maWciKQogICAgICAgIGZvciBrZXkgaW4gKCJOT1RJRllfU09DS0VUIiwgIkRCVVNfU0VTU0lPTl9CVVNfQUREUkVTUyIsICJE -QlVTX1NFU1NJT05fQlVTX1BJRCIsCiAgICAgICAgICAgICAgICAgICAgIkFUX1NQSV9CVVNfQUREUkVTUyIsICJBVF9TUElfQlVTIik6CiAgICAgICAgICAg -IGVudmlyb25tZW50LnBvcChrZXksIE5vbmUpCiAgICAgICAgd2hpbGUgbm90IHNlbGYuc2h1dGRvd24uaXNfc2V0KCk6CiAgICAgICAgICAgIHRyeToKICAg -ICAgICAgICAgICAgICMgVGhlIHN1cGVydmlzb3IgaXMgYSBoZWFsdGggY2hlY2ssIG5vdCBhIHRpbWVyLWRyaXZlbiByZXN0YXJ0LgogICAgICAgICAgICAg -ICAgIyBQcmVzZXJ2ZSBhbiBhY3RpdmUgUkZCL25vVk5DIHNlc3Npb24gdW50aWwgaXRzIG93bmVkIGVuZHBvaW50cyBmYWlsLgogICAgICAgICAgICAgICAg -aWYgbm90IHNlbGYuc2VydmljZV9yZWFkeShudW1iZXIpOgogICAgICAgICAgICAgICAgICAgIHNlbGYuc3RhcnRfY29tcG9uZW50cyhudW1iZXIsIGVudmly -b25tZW50LCBydW50aW1lKQogICAgICAgICAgICAgICAgc3RhdGUgPSAicnVubmluZyIgaWYgc2VsZi5zZXJ2aWNlX3JlYWR5KG51bWJlcikgZWxzZSAidW5h -dmFpbGFibGUiCiAgICAgICAgICAgIGV4Y2VwdCBOb1ZOQ1VuYXZhaWxhYmxlOgogICAgICAgICAgICAgICAgc2VsZi50ZXJtaW5hdGVfd2Vic29ja2lmeShu -dW1iZXIpCiAgICAgICAgICAgICAgICBzdGF0ZSA9ICJ1bmF2YWlsYWJsZSIKICAgICAgICAgICAgZXhjZXB0IChPU0Vycm9yLCBSdW50aW1lRXJyb3IsIHN1 -YnByb2Nlc3MuU3VicHJvY2Vzc0Vycm9yKToKICAgICAgICAgICAgICAgIHNlbGYudGVybWluYXRlKG51bWJlcikKICAgICAgICAgICAgICAgIHN0YXRlID0g -InVuYXZhaWxhYmxlIgogICAgICAgICAgICB3aXRoIHNlbGYubG9jazoKICAgICAgICAgICAgICAgIHNlbGYuc3RhdGVzW251bWJlcl0gPSBzdGF0ZQogICAg -ICAgICAgICBmaW5pc2hlZC5zZXQoKQogICAgICAgICAgICBzZWxmLnNodXRkb3duLndhaXQoMzApCiAgICAgICAgc2VsZi50ZXJtaW5hdGUobnVtYmVyKQoK -ICAgIGRlZiBzbmFwc2hvdChzZWxmKToKICAgICAgICBwcmltYXJ5X3N0YXRlID0gInJ1bm5pbmciIGlmIHJlYWR5KDEpIGVsc2UgInVuYXZhaWxhYmxlIgog -ICAgICAgIHdpdGggc2VsZi5sb2NrOgogICAgICAgICAgICByZXR1cm4geyJ2ZXJzaW9uIjogMSwgImNhbkNyZWF0ZSI6IGxlbihzZWxmLmpvYnMpIDwgTUFY -X0RJU1BMQVlTIC0gMSwKICAgICAgICAgICAgICAgICAgICAiZGlzcGxheXMiOiBbZGVzY3JpcHRvcigxLCBwcmltYXJ5X3N0YXRlKV0KICAgICAgICAgICAg -ICAgICAgICArIFtkZXNjcmlwdG9yKG4sIHNlbGYuc3RhdGVzLmdldChuLCAibGF1bmNoaW5nIikpIGZvciBuIGluIHNlbGYuY2F0YWxvZy5udW1iZXJzKCld -fQoKICAgIGRlZiBoYW5kbGUoc2VsZiwgcmVxdWVzdCk6CiAgICAgICAgaWYgcmVxdWVzdC5nZXQoImFjdGlvbiIpID09ICJsaXN0IjoKICAgICAgICAgICAg -cmV0dXJuIHNlbGYuc25hcHNob3QoKQogICAgICAgIGlmIHJlcXVlc3QuZ2V0KCJhY3Rpb24iKSAhPSAiY3JlYXRlIjoKICAgICAgICAgICAgcmFpc2UgVmFs -dWVFcnJvcigidW5zdXBwb3J0ZWQgZGlzcGxheSBhY3Rpb24iKQogICAgICAgIG51bWJlciA9IHNlbGYuY2F0YWxvZy5hbGxvY2F0ZShyZXF1ZXN0LmdldCgi -cmVxdWVzdCIpKQogICAgICAgIHNlbGYuc3RhcnQobnVtYmVyKS53YWl0KDU1KQogICAgICAgIHJlc3VsdCA9IHNlbGYuc25hcHNob3QoKQogICAgICAgIHJl -c3VsdFsiY3JlYXRlZCJdID0gZiJkaXNwbGF5OntudW1iZXJ9IgogICAgICAgIGlmIHNlbGYuc3RhdGVzLmdldChudW1iZXIpICE9ICJydW5uaW5nIjoKICAg -ICAgICAgICAgcmVzdWx0WyJlcnJvciJdID0gImRpc3BsYXlfc3RhcnRfZmFpbGVkIgogICAgICAgIHJldHVybiByZXN1bHQKCgpkZWYgc2VydmUocnVudGlt -ZSk6CiAgICBydW50aW1lID0gUGF0aChydW50aW1lKQogICAgcnVudGltZS5ta2Rpcihtb2RlPTBvNzAwLCBwYXJlbnRzPVRydWUsIGV4aXN0X29rPVRydWUp -CiAgICB3aXRoIChydW50aW1lIC8gImRpc3BsYXktY29udHJvbC5sb2NrIikub3BlbigidyIpIGFzIGxvY2s6CiAgICAgICAgdHJ5OgogICAgICAgICAgICBm -Y250bC5mbG9jayhsb2NrLCBmY250bC5MT0NLX0VYIHwgZmNudGwuTE9DS19OQikKICAgICAgICBleGNlcHQgQmxvY2tpbmdJT0Vycm9yOgogICAgICAgICAg -ICByZXR1cm4KICAgICAgICBpZiBub3QgYWxsKHNodXRpbC53aGljaChuYW1lKSBmb3IgbmFtZSBpbiAoIndlYnNvY2tpZnkiLCAib3BlbmJveCIsICJkYnVz -LWxhdW5jaCIpKToKICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJvcigiZGlzcGxheSBydW50aW1lIHVuYXZhaWxhYmxlIikKICAgICAgICBpZiBub3QgKHNo -dXRpbC53aGljaCgiWHZuYyIpIG9yIHNodXRpbC53aGljaCgiWHRpZ2Vydm5jIikpOgogICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJkaXNwbGF5IHJ1 -bnRpbWUgdW5hdmFpbGFibGUiKQogICAgICAgIHNlcnZpY2UgPSBEaXNwbGF5U2VydmljZShEaXNwbGF5Q2F0YWxvZyhQYXRoLmhvbWUoKSAvICIuY211eCIg -LyAiZGlzcGxheXMiKSwgcnVudGltZSAvICJkaXNwbGF5cyIpCiAgICAgICAgZm9yIG51bWJlciBpbiBzZXJ2aWNlLmNhdGFsb2cubnVtYmVycygpOgogICAg -ICAgICAgICBzZXJ2aWNlLnN0YXJ0KG51bWJlcikKICAgICAgICBwYXRoID0gcnVudGltZSAvICJkaXNwbGF5LWNvbnRyb2wuc29jayIKICAgICAgICBpZiBw -YXRoLmV4aXN0cygpOgogICAgICAgICAgICBwYXRoLnVubGluaygpCgogICAgICAgIGNsYXNzIEhhbmRsZXIoc29ja2V0c2VydmVyLlN0cmVhbVJlcXVlc3RI -YW5kbGVyKToKICAgICAgICAgICAgZGVmIGhhbmRsZShzZWxmKToKICAgICAgICAgICAgICAgIHNlbGYuY29ubmVjdGlvbi5zZXR0aW1lb3V0KDYwKQogICAg -ICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgIHJhdyA9IHNlbGYucmZpbGUucmVhZGxpbmUoNDA5NykKICAgICAgICAgICAgICAgICAgICBp -ZiBsZW4ocmF3KSA+IDQwOTY6CiAgICAgICAgICAgICAgICAgICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoImRpc3BsYXkgcmVxdWVzdCB0b28gbGFyZ2UiKQog -ICAgICAgICAgICAgICAgICAgIHJlc3VsdCA9IHNlcnZpY2UuaGFuZGxlKGpzb24ubG9hZHMocmF3KSkKICAgICAgICAgICAgICAgIGV4Y2VwdCAoVmFsdWVF -cnJvciwgS2V5RXJyb3IsIFR5cGVFcnJvciwgT1NFcnJvcik6CiAgICAgICAgICAgICAgICAgICAgcmVzdWx0ID0geyJlcnJvciI6ICJkaXNwbGF5X3JlcXVl -c3RfZmFpbGVkIn0KICAgICAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgICAgICBzZWxmLndmaWxlLndyaXRlKGpzb24uZHVtcHMocmVzdWx0KS5l -bmNvZGUoKSArIGIiXG4iKQogICAgICAgICAgICAgICAgZXhjZXB0IE9TRXJyb3I6CiAgICAgICAgICAgICAgICAgICAgcGFzcyAgIyBUaGUgcGVyc2lzdGVk -IHJlY2VpcHQgcmVtYWlucyByZXBsYXlhYmxlIGFmdGVyIGRpc2Nvbm5lY3QuCgogICAgICAgIGNsYXNzIFNlcnZlcihzb2NrZXRzZXJ2ZXIuVGhyZWFkaW5n -VW5peFN0cmVhbVNlcnZlcik6CiAgICAgICAgICAgIGRhZW1vbl90aHJlYWRzID0gVHJ1ZQoKICAgICAgICBwcmV2aW91c191bWFzayA9IG9zLnVtYXNrKDBv -MDc3KQogICAgICAgIHRyeToKICAgICAgICAgICAgd2l0aCBTZXJ2ZXIoc3RyKHBhdGgpLCBIYW5kbGVyKSBhcyBzZXJ2ZXI6CiAgICAgICAgICAgICAgICBv -cy51bWFzayhwcmV2aW91c191bWFzaykKICAgICAgICAgICAgICAgIHNlcnZlci5zZXJ2ZV9mb3JldmVyKCkKICAgICAgICBmaW5hbGx5OgogICAgICAgICAg -ICBvcy51bWFzayhwcmV2aW91c191bWFzaykKICAgICAgICAgICAgc2VydmljZS5zaHV0ZG93bi5zZXQoKQogICAgICAgICAgICBwYXRoLnVubGluayhtaXNz -aW5nX29rPVRydWUpCgoKZGVmIG1haW4oKToKICAgIHBhcnNlciA9IGFyZ3BhcnNlLkFyZ3VtZW50UGFyc2VyKGRlc2NyaXB0aW9uPV9fZG9jX18pCiAgICBw -YXJzZXIuYWRkX2FyZ3VtZW50KCJhY3Rpb24iLCBjaG9pY2VzPVsibGlzdCIsICJjcmVhdGUiLCAic2VydmUiXSkKICAgIHBhcnNlci5hZGRfYXJndW1lbnQo -Ii0tcmVxdWVzdC1pZCIpCiAgICBhcmdzID0gcGFyc2VyLnBhcnNlX2FyZ3MoKQogICAgcnVudGltZSA9IG9zLmVudmlyb24uZ2V0KCJDTVVYX0RFU0tUT1Bf -UlVOVElNRV9ESVIiLCAiL3J1bi9jbXV4LWRlc2t0b3AiKQogICAgaWYgYXJncy5hY3Rpb24gPT0gInNlcnZlIjoKICAgICAgICBzZXJ2ZShydW50aW1lKQog -ICAgICAgIHJldHVybiAwCiAgICBpZiBhcmdzLmFjdGlvbiA9PSAiY3JlYXRlIiBhbmQgbm90IGFyZ3MucmVxdWVzdF9pZDoKICAgICAgICBwYXJzZXIuZXJy -b3IoImNyZWF0ZSByZXF1aXJlcyAtLXJlcXVlc3QtaWQgKHJldGFpbiBpdCB3aGVuIHJldHJ5aW5nKSIpCiAgICB0cnk6CiAgICAgICAgd2l0aCBzb2NrZXQu -c29ja2V0KHNvY2tldC5BRl9VTklYLCBzb2NrZXQuU09DS19TVFJFQU0pIGFzIGNsaWVudDoKICAgICAgICAgICAgY2xpZW50LnNldHRpbWVvdXQoNjApCiAg -ICAgICAgICAgIGNsaWVudC5jb25uZWN0KHN0cihQYXRoKHJ1bnRpbWUpIC8gImRpc3BsYXktY29udHJvbC5zb2NrIikpCiAgICAgICAgICAgIGNsaWVudC5z -ZW5kYWxsKGpzb24uZHVtcHMoeyJhY3Rpb24iOiBhcmdzLmFjdGlvbiwgInJlcXVlc3QiOiBhcmdzLnJlcXVlc3RfaWR9KS5lbmNvZGUoKSArIGIiXG4iKQog -ICAgICAgICAgICB3aXRoIGNsaWVudC5tYWtlZmlsZSgicmIiKSBhcyBzdHJlYW06CiAgICAgICAgICAgICAgICByZXN1bHQgPSBqc29uLmxvYWRzKHN0cmVh -bS5yZWFkbGluZSg2NTUzNikpCiAgICBleGNlcHQgKE9TRXJyb3IsIFZhbHVlRXJyb3IpOgogICAgICAgIHJlc3VsdCA9IHsiZXJyb3IiOiAiZGlzcGxheV9y -dW50aW1lX3VuYXZhaWxhYmxlIn0KICAgIHByaW50KGpzb24uZHVtcHMocmVzdWx0KSkKICAgIHJldHVybiAxIGlmICJlcnJvciIgaW4gcmVzdWx0IGVsc2Ug -MAoKCmlmIF9fbmFtZV9fID09ICJfX21haW5fXyI6CiAgICByYWlzZSBTeXN0ZW1FeGl0KG1haW4oKSkK +ciwgTm9uZSkKICAgICAgICBzZWxmLnRlcm1pbmF0ZV93ZWJzb2NraWZ5KG51bWJlcikKICAgICAgICB0cnk6CiAgICAgICAgICAgIChzZWxmLnJ1bnRpbWUg +LyBzdHIobnVtYmVyKSAvICJzZXNzaW9uLmpzb24iKS51bmxpbmsoKQogICAgICAgIGV4Y2VwdCBGaWxlTm90Rm91bmRFcnJvcjoKICAgICAgICAgICAgcGFz +cwoKICAgIGRlZiB0ZXJtaW5hdGVfd2Vic29ja2lmeShzZWxmLCBudW1iZXIpOgogICAgICAgIHByb2Nlc3MgPSBzZWxmLndlYnNvY2tpZnlfcHJvY2Vzc2Vz +LnBvcChudW1iZXIsIE5vbmUpCiAgICAgICAgaWYgcHJvY2VzcyBpcyBOb25lOgogICAgICAgICAgICByZXR1cm4KICAgICAgICB0cnk6CiAgICAgICAgICAg +IHByb2Nlc3MudGVybWluYXRlKCkKICAgICAgICBleGNlcHQgT1NFcnJvcjoKICAgICAgICAgICAgcGFzcwoKICAgIEBzdGF0aWNtZXRob2QKICAgIGRlZiBz +Y29wZWRfY29tcG9uZW50X3BhdGgoZXhlY3V0YWJsZSwgcGF0aCwgbnVtYmVyLCBydW50aW1lKToKICAgICAgICAiIiJHaXZlIHBlci1kaXNwbGF5IGRlc2t0 +b3AgY2xpZW50cyBuYW1lcyB0aGUgOjEgc3VwZXJ2aXNvciBjYW5ub3QgbWF0Y2guIiIiCiAgICAgICAgc291cmNlID0gUGF0aChwYXRoKQogICAgICAgIGlm +IG5vdCBzb3VyY2UuaXNfZmlsZSgpOgogICAgICAgICAgICByZXR1cm4gcGF0aAogICAgICAgIGRpcmVjdG9yeSA9IFBhdGgocnVudGltZSkgLyAiYmluIgog +ICAgICAgIGRpcmVjdG9yeS5ta2Rpcihtb2RlPTBvNzAwLCBwYXJlbnRzPVRydWUsIGV4aXN0X29rPVRydWUpCiAgICAgICAgdGFyZ2V0ID0gZGlyZWN0b3J5 +IC8gZiJjbXV4LWRpc3BsYXkte251bWJlcn0te3NvdXJjZS5uYW1lfSIKICAgICAgICBpZiBub3QgdGFyZ2V0LmV4aXN0cygpIG9yIHRhcmdldC5zdGF0KCku +c3RfbXRpbWVfbnMgPCBzb3VyY2Uuc3RhdCgpLnN0X210aW1lX25zOgogICAgICAgICAgICBzaHV0aWwuY29weTIoc291cmNlLCB0YXJnZXQpCiAgICAgICAg +ICAgIHRhcmdldC5jaG1vZCgwbzcwMCkKICAgICAgICByZXR1cm4gc3RyKHRhcmdldCkKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgd2FpdF9mb3JfcG9y +dChwb3J0LCB0aW1lb3V0PTIwKToKICAgICAgICBkZWFkbGluZSA9IHRpbWUubW9ub3RvbmljKCkgKyB0aW1lb3V0CiAgICAgICAgd2hpbGUgdGltZS5tb25v +dG9uaWMoKSA8IGRlYWRsaW5lOgogICAgICAgICAgICBpZiBsaXN0ZW5pbmcocG9ydCk6CiAgICAgICAgICAgICAgICByZXR1cm4gVHJ1ZQogICAgICAgICAg +ICB0aW1lLnNsZWVwKDAuMSkKICAgICAgICByZXR1cm4gRmFsc2UKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgcHJvY2Vzc19hbGl2ZShwcm9jZXNzKToK +ICAgICAgICBwb2xsID0gZ2V0YXR0cihwcm9jZXNzLCAicG9sbCIsIE5vbmUpCiAgICAgICAgcmV0dXJuIHBvbGwgaXMgTm9uZSBvciBwb2xsKCkgaXMgTm9u +ZQoKICAgIGRlZiBjb21wb25lbnRzX2FsaXZlKHNlbGYsIG51bWJlcik6CiAgICAgICAgbmFtZWQgPSBzZWxmLm5hbWVkX3Byb2Nlc3Nlcy5nZXQobnVtYmVy +KQogICAgICAgIGlmIG5hbWVkIGlzIG5vdCBOb25lOgogICAgICAgICAgICByZXR1cm4gYm9vbChuYW1lZCkgYW5kIGFsbChzZWxmLnByb2Nlc3NfYWxpdmUo +cHJvY2VzcykgZm9yIHByb2Nlc3MgaW4gbmFtZWQudmFsdWVzKCkpIFwKICAgICAgICAgICAgICAgIGFuZCAoInh2bmMiIGluIG5hbWVkIG9yIHJmYl9yZWFk +eShudW1iZXIpKQogICAgICAgIHByb2Nlc3NlcyA9IHNlbGYucHJvY2Vzc2VzLmdldChudW1iZXIsIFtdKQogICAgICAgIHJldHVybiBib29sKHByb2Nlc3Nl +cykgYW5kIGFsbChzZWxmLnByb2Nlc3NfYWxpdmUocHJvY2VzcykgZm9yIHByb2Nlc3MgaW4gcHJvY2Vzc2VzKQoKICAgIGRlZiBzZXJ2aWNlX3JlYWR5KHNl +bGYsIG51bWJlcik6CiAgICAgICAgaWYgbm90IHJlYWR5KG51bWJlcikgb3Igbm90IHNlbGYuY29tcG9uZW50c19hbGl2ZShudW1iZXIpOgogICAgICAgICAg +ICByZXR1cm4gRmFsc2UKICAgICAgICB3ZWJzb2NraWZ5ID0gc2VsZi53ZWJzb2NraWZ5X3Byb2Nlc3Nlcy5nZXQobnVtYmVyKQogICAgICAgIHJldHVybiB3 +ZWJzb2NraWZ5IGlzIE5vbmUgb3Igc2VsZi5wcm9jZXNzX2FsaXZlKHdlYnNvY2tpZnkpCgogICAgZGVmIHRyYWNrX3Byb2Nlc3Moc2VsZiwgbnVtYmVyLCBu +YW1lLCBwcm9jZXNzKToKICAgICAgICBuYW1lZCA9IHNlbGYubmFtZWRfcHJvY2Vzc2VzLnNldGRlZmF1bHQobnVtYmVyLCB7fSkKICAgICAgICBwcmV2aW91 +cyA9IG5hbWVkLmdldChuYW1lKQogICAgICAgIGlmIHByZXZpb3VzIGlzIG5vdCBOb25lOgogICAgICAgICAgICBzZWxmLnByb2Nlc3Nlc1tudW1iZXJdID0g +WwogICAgICAgICAgICAgICAgaXRlbSBmb3IgaXRlbSBpbiBzZWxmLnByb2Nlc3Nlcy5nZXQobnVtYmVyLCBbXSkgaWYgaXRlbSBpcyBub3QgcHJldmlvdXMK +ICAgICAgICAgICAgXQogICAgICAgIHNlbGYucHJvY2Vzc2VzLnNldGRlZmF1bHQobnVtYmVyLCBbXSkuYXBwZW5kKHByb2Nlc3MpCiAgICAgICAgbmFtZWRb +bmFtZV0gPSBwcm9jZXNzCgogICAgZGVmIHJlbW92ZV9wcm9jZXNzKHNlbGYsIG51bWJlciwgbmFtZSk6CiAgICAgICAgcHJvY2VzcyA9IHNlbGYubmFtZWRf +cHJvY2Vzc2VzLnNldGRlZmF1bHQobnVtYmVyLCB7fSkucG9wKG5hbWUsIE5vbmUpCiAgICAgICAgaWYgcHJvY2VzcyBpcyBOb25lOgogICAgICAgICAgICBy +ZXR1cm4KICAgICAgICBzZWxmLnByb2Nlc3Nlc1tudW1iZXJdID0gW2l0ZW0gZm9yIGl0ZW0gaW4gc2VsZi5wcm9jZXNzZXMuZ2V0KG51bWJlciwgW10pIGlm +IGl0ZW0gaXMgbm90IHByb2Nlc3NdCiAgICAgICAgdHJ5OgogICAgICAgICAgICBwcm9jZXNzLnRlcm1pbmF0ZSgpCiAgICAgICAgZXhjZXB0IE9TRXJyb3I6 +CiAgICAgICAgICAgIHBhc3MKCiAgICBkZWYgcmVjb3Zlcl9wcm9jZXNzZXMoc2VsZiwgbnVtYmVyLCBydW50aW1lLCBlbnZpcm9ubWVudCk6CiAgICAgICAg +c2VsZi5wcm9jZXNzZXNbbnVtYmVyXSA9IFtdCiAgICAgICAgc2VsZi5uYW1lZF9wcm9jZXNzZXNbbnVtYmVyXSA9IHt9CiAgICAgICAgc2Vzc2lvbiA9IFBh +dGgocnVudGltZSkgLyAic2Vzc2lvbi5qc29uIgogICAgICAgIHRyeToKICAgICAgICAgICAgc2F2ZWQgPSBqc29uLmxvYWRzKHNlc3Npb24ucmVhZF90ZXh0 +KCkpCiAgICAgICAgICAgIGRidXNfcGlkID0gaW50KHNhdmVkLmdldCgicGlkIiwgMCkpCiAgICAgICAgICAgIGRidXMgPSBPd25lZFByb2Nlc3MoZGJ1c19w +aWQpCiAgICAgICAgICAgIGlmIGRidXNfcGlkID4gMSBhbmQgc2VsZi5wcm9jZXNzX2FsaXZlKGRidXMpOgogICAgICAgICAgICAgICAgaWYgc2F2ZWQuZ2V0 +KCJhZGRyZXNzIik6CiAgICAgICAgICAgICAgICAgICAgZW52aXJvbm1lbnRbIkRCVVNfU0VTU0lPTl9CVVNfQUREUkVTUyJdID0gc2F2ZWRbImFkZHJlc3Mi +XQogICAgICAgICAgICAgICAgZW52aXJvbm1lbnRbIkRCVVNfU0VTU0lPTl9CVVNfUElEIl0gPSBzdHIoZGJ1c19waWQpCiAgICAgICAgICAgICAgICBzZWxm +LnRyYWNrX3Byb2Nlc3MobnVtYmVyLCAiZGJ1cyIsIGRidXMpCiAgICAgICAgZXhjZXB0IChPU0Vycm9yLCBWYWx1ZUVycm9yLCBUeXBlRXJyb3IsIGpzb24u +SlNPTkRlY29kZUVycm9yKToKICAgICAgICAgICAgcGFzcwogICAgICAgIHVpZCA9IHN0cihvcy5nZXR1aWQoKSkKICAgICAgICBmb3IgbmFtZSBpbiAoIm9w +ZW5ib3giLCAidGludDIiLCAidm5jY29uZmlnIik6CiAgICAgICAgICAgIG1hcmtlciA9IHN0cihQYXRoKHJ1bnRpbWUpIC8gImJpbiIgLyBmImNtdXgtZGlz +cGxheS17bnVtYmVyfS17bmFtZX0iKQogICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICBwYXR0ZXJuID0gcmYiXntyZS5lc2NhcGUobWFya2VyKX0o +PzpbWzpzcGFjZTpdXXwkKSIKICAgICAgICAgICAgICAgIG91dHB1dCA9IHN1YnByb2Nlc3MuY2hlY2tfb3V0cHV0KFsicGdyZXAiLCAiLXUiLCB1aWQsICIt +ZiIsIHBhdHRlcm5dLCB0ZXh0PVRydWUpCiAgICAgICAgICAgICAgICBwaWQgPSBpbnQob3V0cHV0LnNwbGl0bGluZXMoKVswXSkKICAgICAgICAgICAgZXhj +ZXB0IChPU0Vycm9yLCBWYWx1ZUVycm9yLCBJbmRleEVycm9yLCBzdWJwcm9jZXNzLkNhbGxlZFByb2Nlc3NFcnJvcik6CiAgICAgICAgICAgICAgICBjb250 +aW51ZQogICAgICAgICAgICBzZWxmLnRyYWNrX3Byb2Nlc3MobnVtYmVyLCBuYW1lLCBPd25lZFByb2Nlc3MocGlkKSkKICAgICAgICB0cnk6CiAgICAgICAg +ICAgIG91dHB1dCA9IHN1YnByb2Nlc3MuY2hlY2tfb3V0cHV0KAogICAgICAgICAgICAgICAgWyJwZ3JlcCIsICItdSIsIHVpZCwgIi1mIiwKICAgICAgICAg +ICAgICAgICByZiJeKD86LipcLyk/KD86WHZuY3xYdGlnZXJ2bmMpLio6e251bWJlcn0uKi1yZmJwb3J0XHMrezU5MDAgKyBudW1iZXJ9KD86XHN8JCkiXSwK +ICAgICAgICAgICAgICAgIHRleHQ9VHJ1ZQogICAgICAgICAgICApCiAgICAgICAgICAgIHBpZCA9IGludChvdXRwdXQuc3BsaXRsaW5lcygpWzBdKQogICAg +ICAgIGV4Y2VwdCAoT1NFcnJvciwgVmFsdWVFcnJvciwgSW5kZXhFcnJvciwgc3VicHJvY2Vzcy5DYWxsZWRQcm9jZXNzRXJyb3IpOgogICAgICAgICAgICBw +aWQgPSAwCiAgICAgICAgaWYgcGlkID4gMToKICAgICAgICAgICAgc2VsZi50cmFja19wcm9jZXNzKG51bWJlciwgInh2bmMiLCBPd25lZFByb2Nlc3MocGlk +KSkKCiAgICBkZWYgc3RhcnRfZGJ1cyhzZWxmLCBudW1iZXIsIGVudmlyb25tZW50LCBydW50aW1lKToKICAgICAgICBidXMgPSBzdWJwcm9jZXNzLnJ1bihb +ImRidXMtbGF1bmNoIiwgIi0tc2gtc3ludGF4Il0sIGVudj1lbnZpcm9ubWVudCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdGRpbj1zdWJwcm9j +ZXNzLkRFVk5VTEwsIHN0ZG91dD1zdWJwcm9jZXNzLlBJUEUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3RkZXJyPXN1YnByb2Nlc3MuREVWTlVM +TCwgdGV4dD1UcnVlLCB0aW1lb3V0PTEwLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNoZWNrPVRydWUpLnN0ZG91dAogICAgICAgIGZvciBsaW5l +IGluIGJ1cy5zcGxpdGxpbmVzKCk6CiAgICAgICAgICAgIGlmICI9IiBub3QgaW4gbGluZToKICAgICAgICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgICAg +IGtleSwgdmFsdWUgPSBsaW5lLnNwbGl0KCI9IiwgMSkKICAgICAgICAgICAgZW52aXJvbm1lbnRba2V5XSA9IHZhbHVlLnN0cmlwKCkucnN0cmlwKCI7Iiku +c3RyaXAoKS5zdHJpcCgiJyIpCiAgICAgICAgdHJ5OgogICAgICAgICAgICBkYnVzX3BpZCA9IGludChlbnZpcm9ubWVudC5nZXQoIkRCVVNfU0VTU0lPTl9C +VVNfUElEIiwgIjAiKSkKICAgICAgICBleGNlcHQgVmFsdWVFcnJvcjoKICAgICAgICAgICAgZGJ1c19waWQgPSAwCiAgICAgICAgaWYgZGJ1c19waWQgPiAx +OgogICAgICAgICAgICBzZWxmLnRyYWNrX3Byb2Nlc3MobnVtYmVyLCAiZGJ1cyIsIE93bmVkUHJvY2VzcyhkYnVzX3BpZCkpCiAgICAgICAgICAgIChQYXRo +KHJ1bnRpbWUpIC8gInNlc3Npb24uanNvbiIpLndyaXRlX3RleHQoanNvbi5kdW1wcyh7CiAgICAgICAgICAgICAgICAicGlkIjogZGJ1c19waWQsCiAgICAg +ICAgICAgICAgICAiYWRkcmVzcyI6IGVudmlyb25tZW50LmdldCgiREJVU19TRVNTSU9OX0JVU19BRERSRVNTIiwgIiIpCiAgICAgICAgICAgIH0pKQoKICAg +IGRlZiBzdGFydF9zZXNzaW9uX2NvbXBvbmVudHMoc2VsZiwgbnVtYmVyLCBlbnZpcm9ubWVudCwgcnVudGltZSk6CiAgICAgICAgZm9yIGV4ZWN1dGFibGUs +IGFyZ3MgaW4gWwogICAgICAgICAgICAoIm9wZW5ib3giLCBbXSksICgidGludDIiLCBbIi1jIiwgIi9ldGMvY211eC90aW50MnJjIl0pLAogICAgICAgICAg +ICAoInZuY2NvbmZpZyIsIFsiLW5vd2luIl0pLAogICAgICAgIF06CiAgICAgICAgICAgIGV4aXN0aW5nID0gc2VsZi5uYW1lZF9wcm9jZXNzZXMuZ2V0KG51 +bWJlciwge30pLmdldChleGVjdXRhYmxlKQogICAgICAgICAgICBpZiBleGlzdGluZyBpcyBub3QgTm9uZSBhbmQgc2VsZi5wcm9jZXNzX2FsaXZlKGV4aXN0 +aW5nKToKICAgICAgICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgICAgIHBhdGggPSBleGVjdXRhYmxlIGlmICIvIiBpbiBleGVjdXRhYmxlIGVsc2Ugc2h1 +dGlsLndoaWNoKGV4ZWN1dGFibGUpCiAgICAgICAgICAgIGlmIG5vdCBwYXRoOgogICAgICAgICAgICAgICAgaWYgZXhlY3V0YWJsZSA9PSAidm5jY29uZmln +IjoKICAgICAgICAgICAgICAgICAgICBjb250aW51ZQogICAgICAgICAgICAgICAgcmFpc2UgUnVudGltZUVycm9yKGYibWlzc2luZyBkaXNwbGF5IGNvbXBv +bmVudDoge2V4ZWN1dGFibGV9IikKICAgICAgICAgICAgcGF0aCA9IHNlbGYuc2NvcGVkX2NvbXBvbmVudF9wYXRoKGV4ZWN1dGFibGUsIHBhdGgsIG51bWJl +ciwgcnVudGltZSkKICAgICAgICAgICAgcHJvY2VzcyA9IHN1YnByb2Nlc3MuUG9wZW4oW3BhdGhdICsgYXJncywgZW52PWVudmlyb25tZW50LAogICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdGRpbj1zdWJwcm9jZXNzLkRFVk5VTEwsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgIHN0ZG91dD1zdWJwcm9jZXNzLkRFVk5VTEwsIHN0ZGVycj1zdWJwcm9jZXNzLkRFVk5VTEwpCiAgICAgICAgICAgIHNlbGYudHJhY2tfcHJvY2Vz +cyhudW1iZXIsIGV4ZWN1dGFibGUsIHByb2Nlc3MpCgogICAgZGVmIHN0YXJ0X3dlYnNvY2tpZnkoc2VsZiwgbnVtYmVyLCBlbnZpcm9ubWVudCwgZXhlY3V0 +YWJsZSk6CiAgICAgICAgcGF0aCA9IGV4ZWN1dGFibGUgaWYgIi8iIGluIGV4ZWN1dGFibGUgZWxzZSBzaHV0aWwud2hpY2goZXhlY3V0YWJsZSkKICAgICAg +ICBpZiBub3QgcGF0aDoKICAgICAgICAgICAgcmFpc2UgTm9WTkNVbmF2YWlsYWJsZSgibWlzc2luZyBkaXNwbGF5IGNvbXBvbmVudDogd2Vic29ja2lmeSIp +CiAgICAgICAgc2VsZi50ZXJtaW5hdGVfd2Vic29ja2lmeShudW1iZXIpCiAgICAgICAgcmZiX3BvcnQsIG5vdm5jX3BvcnQgPSA1OTAwICsgbnVtYmVyLCA2 +OTAwICsgbnVtYmVyCiAgICAgICAgcHJvY2VzcyA9IHN1YnByb2Nlc3MuUG9wZW4oCiAgICAgICAgICAgIFtwYXRoLCAiLS13ZWIiLCAiL3Vzci9zaGFyZS9u +b3ZuYyIsICItLWhlYXJ0YmVhdCIsICIzMCIsCiAgICAgICAgICAgICBmIjEyNy4wLjAuMTp7bm92bmNfcG9ydH0iLCBmIjEyNy4wLjAuMTp7cmZiX3BvcnR9 +Il0sCiAgICAgICAgICAgIGVudj1lbnZpcm9ubWVudCwgc3RkaW49c3VicHJvY2Vzcy5ERVZOVUxMLAogICAgICAgICAgICBzdGRvdXQ9c3VicHJvY2Vzcy5E +RVZOVUxMLCBzdGRlcnI9c3VicHJvY2Vzcy5ERVZOVUxMKQogICAgICAgIHNlbGYud2Vic29ja2lmeV9wcm9jZXNzZXNbbnVtYmVyXSA9IHByb2Nlc3MKICAg +ICAgICBpZiBub3Qgc2VsZi53YWl0X2Zvcl9wb3J0KG5vdm5jX3BvcnQsIHRpbWVvdXQ9MTApOgogICAgICAgICAgICBzZWxmLnRlcm1pbmF0ZV93ZWJzb2Nr +aWZ5KG51bWJlcikKICAgICAgICAgICAgcmFpc2UgTm9WTkNVbmF2YWlsYWJsZSgibm9WTkMgZGlkIG5vdCBiZWNvbWUgcmVhZHkiKQoKICAgIGRlZiBzdGFy +dF9jb21wb25lbnRzKHNlbGYsIG51bWJlciwgZW52aXJvbm1lbnQsIHJ1bnRpbWUpOgogICAgICAgIGlmIHNlbGYuc2VydmljZV9yZWFkeShudW1iZXIpOgog +ICAgICAgICAgICByZXR1cm4KICAgICAgICByZmJfaXNfcmVhZHkgPSByZmJfcmVhZHkobnVtYmVyKQogICAgICAgIHZuYyA9IHNodXRpbC53aGljaCgiWHZu +YyIpIG9yIHNodXRpbC53aGljaCgiWHRpZ2Vydm5jIikKICAgICAgICB3ZWJzb2NraWZ5ID0gc2h1dGlsLndoaWNoKCJ3ZWJzb2NraWZ5IikKICAgICAgICBp +ZiBub3Qgd2Vic29ja2lmeToKICAgICAgICAgICAgcmFpc2UgTm9WTkNVbmF2YWlsYWJsZSgiZGlzcGxheSBydW50aW1lIHVuYXZhaWxhYmxlIikKICAgICAg +ICBpZiBub3QgcmZiX2lzX3JlYWR5IGFuZCBub3Qgdm5jOgogICAgICAgICAgICByYWlzZSBSdW50aW1lRXJyb3IoImRpc3BsYXkgcnVudGltZSB1bmF2YWls +YWJsZSIpCiAgICAgICAgaWYgcmZiX2lzX3JlYWR5IGFuZCBudW1iZXIgbm90IGluIHNlbGYubmFtZWRfcHJvY2Vzc2VzOgogICAgICAgICAgICBzZWxmLnJl +Y292ZXJfcHJvY2Vzc2VzKG51bWJlciwgcnVudGltZSwgZW52aXJvbm1lbnQpCiAgICAgICAgaWYgbm90IHJmYl9pc19yZWFkeToKICAgICAgICAgICAgc2Vs +Zi50ZXJtaW5hdGUobnVtYmVyKQogICAgICAgICAgICByZmJfcG9ydCA9IDU5MDAgKyBudW1iZXIKICAgICAgICAgICAgc2VsZi5wcm9jZXNzZXNbbnVtYmVy +XSA9IFtdCiAgICAgICAgICAgIHNlbGYubmFtZWRfcHJvY2Vzc2VzW251bWJlcl0gPSB7fQogICAgICAgICAgICBzZWxmLnRyYWNrX3Byb2Nlc3MobnVtYmVy +LCAieHZuYyIsIHN1YnByb2Nlc3MuUG9wZW4oCiAgICAgICAgICAgICAgICBbdm5jLCBmIjp7bnVtYmVyfSIsICItZ2VvbWV0cnkiLCBvcy5lbnZpcm9uLmdl +dCgiQ01VWF9WTkNfR0VPTUVUUlkiLCAiMTQ0MHg5MDAiKSwKICAgICAgICAgICAgICAgICAiLWRlcHRoIiwgIjI0IiwgIi1yZmJwb3J0Iiwgc3RyKHJmYl9w +b3J0KSwgIi1sb2NhbGhvc3QiLAogICAgICAgICAgICAgICAgICItU2VjdXJpdHlUeXBlcyIsICJOb25lIiwgIi1BbHdheXNTaGFyZWQiXSwKICAgICAgICAg +ICAgICAgIGVudj1lbnZpcm9ubWVudCwgc3RkaW49c3VicHJvY2Vzcy5ERVZOVUxMLAogICAgICAgICAgICAgICAgc3Rkb3V0PXN1YnByb2Nlc3MuREVWTlVM +TCwgc3RkZXJyPXN1YnByb2Nlc3MuREVWTlVMTCkpCiAgICAgICAgICAgIGlmIG5vdCBzZWxmLndhaXRfZm9yX3BvcnQocmZiX3BvcnQpOgogICAgICAgICAg +ICAgICAgcmFpc2UgUnVudGltZUVycm9yKCJYdm5jIGRpZCBub3QgYmVjb21lIHJlYWR5IikKICAgICAgICAgICAgc2VsZi5zdGFydF9kYnVzKG51bWJlciwg +ZW52aXJvbm1lbnQsIHJ1bnRpbWUpCiAgICAgICAgICAgIHNlbGYuc3RhcnRfc2Vzc2lvbl9jb21wb25lbnRzKG51bWJlciwgZW52aXJvbm1lbnQsIHJ1bnRp +bWUpCiAgICAgICAgZWxpZiBudW1iZXIgaW4gc2VsZi5uYW1lZF9wcm9jZXNzZXM6CiAgICAgICAgICAgIG5hbWVkID0gc2VsZi5uYW1lZF9wcm9jZXNzZXNb +bnVtYmVyXQogICAgICAgICAgICBkYnVzID0gbmFtZWQuZ2V0KCJkYnVzIikKICAgICAgICAgICAgaWYgZGJ1cyBpcyBOb25lIG9yIG5vdCBzZWxmLnByb2Nl +c3NfYWxpdmUoZGJ1cyk6CiAgICAgICAgICAgICAgICBmb3IgbmFtZSBpbiAoIm9wZW5ib3giLCAidGludDIiLCAidm5jY29uZmlnIik6CiAgICAgICAgICAg +ICAgICAgICAgc2VsZi5yZW1vdmVfcHJvY2VzcyhudW1iZXIsIG5hbWUpCiAgICAgICAgICAgICAgICBzZWxmLnN0YXJ0X2RidXMobnVtYmVyLCBlbnZpcm9u +bWVudCwgcnVudGltZSkKICAgICAgICAgICAgc2VsZi5zdGFydF9zZXNzaW9uX2NvbXBvbmVudHMobnVtYmVyLCBlbnZpcm9ubWVudCwgcnVudGltZSkKICAg +ICAgICB3ZWJzb2NraWZ5X3Byb2Nlc3MgPSBzZWxmLndlYnNvY2tpZnlfcHJvY2Vzc2VzLmdldChudW1iZXIpCiAgICAgICAgaWYgKHdlYnNvY2tpZnlfcHJv +Y2VzcyBpcyBOb25lIG9yIG5vdCBzZWxmLnByb2Nlc3NfYWxpdmUod2Vic29ja2lmeV9wcm9jZXNzKQogICAgICAgICAgICAgICAgb3Igbm90IG5vdm5jX3Jl +YWR5KG51bWJlcikpOgogICAgICAgICAgICBzZWxmLnN0YXJ0X3dlYnNvY2tpZnkobnVtYmVyLCBlbnZpcm9ubWVudCwgd2Vic29ja2lmeSkKICAgICAgICAo +cnVudGltZSAvICJlbnYiKS53cml0ZV90ZXh0KGYiZXhwb3J0IERJU1BMQVk9OntudW1iZXJ9XG4iKQoKICAgIGRlZiBzdXBlcnZpc2Uoc2VsZiwgbnVtYmVy +LCBmaW5pc2hlZCk6CiAgICAgICAgcnVudGltZSA9IHNlbGYucnVudGltZSAvIHN0cihudW1iZXIpCiAgICAgICAgcnVudGltZS5ta2Rpcihtb2RlPTBvNzAw +LCBwYXJlbnRzPVRydWUsIGV4aXN0X29rPVRydWUpCiAgICAgICAgZW52aXJvbm1lbnQgPSBkaWN0KG9zLmVudmlyb24sIERJU1BMQVk9ZiI6e251bWJlcn0i +LCBDTVVYX0RFU0tUT1BfUlVOVElNRV9ESVI9c3RyKHJ1bnRpbWUpKQogICAgICAgIGVudmlyb25tZW50WyJDSFJPTUVfQ09ORklHX0hPTUUiXSA9IHN0cihy +dW50aW1lIC8gImNocm9tZS1jb25maWciKQogICAgICAgIGVudmlyb25tZW50WyJYREdfQ09ORklHX0hPTUUiXSA9IHN0cihydW50aW1lIC8gInhkZy1jb25m +aWciKQogICAgICAgIGZvciBrZXkgaW4gKCJOT1RJRllfU09DS0VUIiwgIkRCVVNfU0VTU0lPTl9CVVNfQUREUkVTUyIsICJEQlVTX1NFU1NJT05fQlVTX1BJ +RCIsCiAgICAgICAgICAgICAgICAgICAgIkFUX1NQSV9CVVNfQUREUkVTUyIsICJBVF9TUElfQlVTIik6CiAgICAgICAgICAgIGVudmlyb25tZW50LnBvcChr +ZXksIE5vbmUpCiAgICAgICAgd2hpbGUgbm90IHNlbGYuc2h1dGRvd24uaXNfc2V0KCk6CiAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgICMgVGhl +IHN1cGVydmlzb3IgaXMgYSBoZWFsdGggY2hlY2ssIG5vdCBhIHRpbWVyLWRyaXZlbiByZXN0YXJ0LgogICAgICAgICAgICAgICAgIyBQcmVzZXJ2ZSBhbiBh +Y3RpdmUgUkZCL25vVk5DIHNlc3Npb24gdW50aWwgaXRzIG93bmVkIGVuZHBvaW50cyBmYWlsLgogICAgICAgICAgICAgICAgaWYgbm90IHNlbGYuc2Vydmlj +ZV9yZWFkeShudW1iZXIpOgogICAgICAgICAgICAgICAgICAgIHNlbGYuc3RhcnRfY29tcG9uZW50cyhudW1iZXIsIGVudmlyb25tZW50LCBydW50aW1lKQog +ICAgICAgICAgICAgICAgc3RhdGUgPSAicnVubmluZyIgaWYgc2VsZi5zZXJ2aWNlX3JlYWR5KG51bWJlcikgZWxzZSAidW5hdmFpbGFibGUiCiAgICAgICAg +ICAgIGV4Y2VwdCBOb1ZOQ1VuYXZhaWxhYmxlOgogICAgICAgICAgICAgICAgc2VsZi50ZXJtaW5hdGVfd2Vic29ja2lmeShudW1iZXIpCiAgICAgICAgICAg +ICAgICBzdGF0ZSA9ICJ1bmF2YWlsYWJsZSIKICAgICAgICAgICAgZXhjZXB0IChPU0Vycm9yLCBSdW50aW1lRXJyb3IsIHN1YnByb2Nlc3MuU3VicHJvY2Vz +c0Vycm9yKToKICAgICAgICAgICAgICAgIHNlbGYudGVybWluYXRlKG51bWJlcikKICAgICAgICAgICAgICAgIHN0YXRlID0gInVuYXZhaWxhYmxlIgogICAg +ICAgICAgICB3aXRoIHNlbGYubG9jazoKICAgICAgICAgICAgICAgIHNlbGYuc3RhdGVzW251bWJlcl0gPSBzdGF0ZQogICAgICAgICAgICBmaW5pc2hlZC5z +ZXQoKQogICAgICAgICAgICBzZWxmLnNodXRkb3duLndhaXQoMzApCiAgICAgICAgc2VsZi50ZXJtaW5hdGUobnVtYmVyKQoKICAgIGRlZiBzbmFwc2hvdChz +ZWxmKToKICAgICAgICBwcmltYXJ5X3N0YXRlID0gInJ1bm5pbmciIGlmIHJlYWR5KDEpIGVsc2UgInVuYXZhaWxhYmxlIgogICAgICAgIHdpdGggc2VsZi5s +b2NrOgogICAgICAgICAgICByZXR1cm4geyJ2ZXJzaW9uIjogMSwgImNhbkNyZWF0ZSI6IGxlbihzZWxmLmpvYnMpIDwgTUFYX0RJU1BMQVlTIC0gMSwKICAg +ICAgICAgICAgICAgICAgICAiZGlzcGxheXMiOiBbZGVzY3JpcHRvcigxLCBwcmltYXJ5X3N0YXRlKV0KICAgICAgICAgICAgICAgICAgICArIFtkZXNjcmlw +dG9yKG4sIHNlbGYuc3RhdGVzLmdldChuLCAibGF1bmNoaW5nIikpIGZvciBuIGluIHNlbGYuY2F0YWxvZy5udW1iZXJzKCldfQoKICAgIGRlZiBoYW5kbGUo +c2VsZiwgcmVxdWVzdCk6CiAgICAgICAgaWYgcmVxdWVzdC5nZXQoImFjdGlvbiIpID09ICJsaXN0IjoKICAgICAgICAgICAgcmV0dXJuIHNlbGYuc25hcHNo +b3QoKQogICAgICAgIGlmIHJlcXVlc3QuZ2V0KCJhY3Rpb24iKSAhPSAiY3JlYXRlIjoKICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJvcigidW5zdXBwb3J0 +ZWQgZGlzcGxheSBhY3Rpb24iKQogICAgICAgIG51bWJlciA9IHNlbGYuY2F0YWxvZy5hbGxvY2F0ZShyZXF1ZXN0LmdldCgicmVxdWVzdCIpKQogICAgICAg +IHNlbGYuc3RhcnQobnVtYmVyKS53YWl0KDU1KQogICAgICAgIHJlc3VsdCA9IHNlbGYuc25hcHNob3QoKQogICAgICAgIHJlc3VsdFsiY3JlYXRlZCJdID0g +ZiJkaXNwbGF5OntudW1iZXJ9IgogICAgICAgIGlmIHNlbGYuc3RhdGVzLmdldChudW1iZXIpICE9ICJydW5uaW5nIjoKICAgICAgICAgICAgcmVzdWx0WyJl +cnJvciJdID0gImRpc3BsYXlfc3RhcnRfZmFpbGVkIgogICAgICAgIHJldHVybiByZXN1bHQKCgpkZWYgc2VydmUocnVudGltZSk6CiAgICBydW50aW1lID0g +UGF0aChydW50aW1lKQogICAgcnVudGltZS5ta2Rpcihtb2RlPTBvNzAwLCBwYXJlbnRzPVRydWUsIGV4aXN0X29rPVRydWUpCiAgICB3aXRoIChydW50aW1l +IC8gImRpc3BsYXktY29udHJvbC5sb2NrIikub3BlbigidyIpIGFzIGxvY2s6CiAgICAgICAgdHJ5OgogICAgICAgICAgICBmY250bC5mbG9jayhsb2NrLCBm +Y250bC5MT0NLX0VYIHwgZmNudGwuTE9DS19OQikKICAgICAgICBleGNlcHQgQmxvY2tpbmdJT0Vycm9yOgogICAgICAgICAgICByZXR1cm4KICAgICAgICBp +ZiBub3QgYWxsKHNodXRpbC53aGljaChuYW1lKSBmb3IgbmFtZSBpbiAoIndlYnNvY2tpZnkiLCAib3BlbmJveCIsICJkYnVzLWxhdW5jaCIpKToKICAgICAg +ICAgICAgcmFpc2UgVmFsdWVFcnJvcigiZGlzcGxheSBydW50aW1lIHVuYXZhaWxhYmxlIikKICAgICAgICBpZiBub3QgKHNodXRpbC53aGljaCgiWHZuYyIp +IG9yIHNodXRpbC53aGljaCgiWHRpZ2Vydm5jIikpOgogICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJkaXNwbGF5IHJ1bnRpbWUgdW5hdmFpbGFibGUi +KQogICAgICAgIHNlcnZpY2UgPSBEaXNwbGF5U2VydmljZShEaXNwbGF5Q2F0YWxvZyhQYXRoLmhvbWUoKSAvICIuY211eCIgLyAiZGlzcGxheXMiKSwgcnVu +dGltZSAvICJkaXNwbGF5cyIpCiAgICAgICAgZm9yIG51bWJlciBpbiBzZXJ2aWNlLmNhdGFsb2cubnVtYmVycygpOgogICAgICAgICAgICBzZXJ2aWNlLnN0 +YXJ0KG51bWJlcikKICAgICAgICBwYXRoID0gcnVudGltZSAvICJkaXNwbGF5LWNvbnRyb2wuc29jayIKICAgICAgICBpZiBwYXRoLmV4aXN0cygpOgogICAg +ICAgICAgICBwYXRoLnVubGluaygpCgogICAgICAgIGNsYXNzIEhhbmRsZXIoc29ja2V0c2VydmVyLlN0cmVhbVJlcXVlc3RIYW5kbGVyKToKICAgICAgICAg +ICAgZGVmIGhhbmRsZShzZWxmKToKICAgICAgICAgICAgICAgIHNlbGYuY29ubmVjdGlvbi5zZXR0aW1lb3V0KDYwKQogICAgICAgICAgICAgICAgdHJ5Ogog +ICAgICAgICAgICAgICAgICAgIHJhdyA9IHNlbGYucmZpbGUucmVhZGxpbmUoNDA5NykKICAgICAgICAgICAgICAgICAgICBpZiBsZW4ocmF3KSA+IDQwOTY6 +CiAgICAgICAgICAgICAgICAgICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoImRpc3BsYXkgcmVxdWVzdCB0b28gbGFyZ2UiKQogICAgICAgICAgICAgICAgICAg +IHJlc3VsdCA9IHNlcnZpY2UuaGFuZGxlKGpzb24ubG9hZHMocmF3KSkKICAgICAgICAgICAgICAgIGV4Y2VwdCAoVmFsdWVFcnJvciwgS2V5RXJyb3IsIFR5 +cGVFcnJvciwgT1NFcnJvcik6CiAgICAgICAgICAgICAgICAgICAgcmVzdWx0ID0geyJlcnJvciI6ICJkaXNwbGF5X3JlcXVlc3RfZmFpbGVkIn0KICAgICAg +ICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgICAgICBzZWxmLndmaWxlLndyaXRlKGpzb24uZHVtcHMocmVzdWx0KS5lbmNvZGUoKSArIGIiXG4iKQog +ICAgICAgICAgICAgICAgZXhjZXB0IE9TRXJyb3I6CiAgICAgICAgICAgICAgICAgICAgcGFzcyAgIyBUaGUgcGVyc2lzdGVkIHJlY2VpcHQgcmVtYWlucyBy +ZXBsYXlhYmxlIGFmdGVyIGRpc2Nvbm5lY3QuCgogICAgICAgIGNsYXNzIFNlcnZlcihzb2NrZXRzZXJ2ZXIuVGhyZWFkaW5nVW5peFN0cmVhbVNlcnZlcik6 +CiAgICAgICAgICAgIGRhZW1vbl90aHJlYWRzID0gVHJ1ZQoKICAgICAgICBwcmV2aW91c191bWFzayA9IG9zLnVtYXNrKDBvMDc3KQogICAgICAgIHRyeToK +ICAgICAgICAgICAgd2l0aCBTZXJ2ZXIoc3RyKHBhdGgpLCBIYW5kbGVyKSBhcyBzZXJ2ZXI6CiAgICAgICAgICAgICAgICBvcy51bWFzayhwcmV2aW91c191 +bWFzaykKICAgICAgICAgICAgICAgIHNlcnZlci5zZXJ2ZV9mb3JldmVyKCkKICAgICAgICBmaW5hbGx5OgogICAgICAgICAgICBvcy51bWFzayhwcmV2aW91 +c191bWFzaykKICAgICAgICAgICAgc2VydmljZS5zaHV0ZG93bi5zZXQoKQogICAgICAgICAgICBwYXRoLnVubGluayhtaXNzaW5nX29rPVRydWUpCgoKZGVm +IG1haW4oKToKICAgIHBhcnNlciA9IGFyZ3BhcnNlLkFyZ3VtZW50UGFyc2VyKGRlc2NyaXB0aW9uPV9fZG9jX18pCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50 +KCJhY3Rpb24iLCBjaG9pY2VzPVsibGlzdCIsICJjcmVhdGUiLCAic2VydmUiXSkKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoIi0tcmVxdWVzdC1pZCIpCiAg +ICBhcmdzID0gcGFyc2VyLnBhcnNlX2FyZ3MoKQogICAgcnVudGltZSA9IG9zLmVudmlyb24uZ2V0KCJDTVVYX0RFU0tUT1BfUlVOVElNRV9ESVIiLCAiL3J1 +bi9jbXV4LWRlc2t0b3AiKQogICAgaWYgYXJncy5hY3Rpb24gPT0gInNlcnZlIjoKICAgICAgICBzZXJ2ZShydW50aW1lKQogICAgICAgIHJldHVybiAwCiAg +ICBpZiBhcmdzLmFjdGlvbiA9PSAiY3JlYXRlIiBhbmQgbm90IGFyZ3MucmVxdWVzdF9pZDoKICAgICAgICBwYXJzZXIuZXJyb3IoImNyZWF0ZSByZXF1aXJl +cyAtLXJlcXVlc3QtaWQgKHJldGFpbiBpdCB3aGVuIHJldHJ5aW5nKSIpCiAgICB0cnk6CiAgICAgICAgd2l0aCBzb2NrZXQuc29ja2V0KHNvY2tldC5BRl9V +TklYLCBzb2NrZXQuU09DS19TVFJFQU0pIGFzIGNsaWVudDoKICAgICAgICAgICAgY2xpZW50LnNldHRpbWVvdXQoNjApCiAgICAgICAgICAgIGNsaWVudC5j +b25uZWN0KHN0cihQYXRoKHJ1bnRpbWUpIC8gImRpc3BsYXktY29udHJvbC5zb2NrIikpCiAgICAgICAgICAgIGNsaWVudC5zZW5kYWxsKGpzb24uZHVtcHMo +eyJhY3Rpb24iOiBhcmdzLmFjdGlvbiwgInJlcXVlc3QiOiBhcmdzLnJlcXVlc3RfaWR9KS5lbmNvZGUoKSArIGIiXG4iKQogICAgICAgICAgICB3aXRoIGNs +aWVudC5tYWtlZmlsZSgicmIiKSBhcyBzdHJlYW06CiAgICAgICAgICAgICAgICByZXN1bHQgPSBqc29uLmxvYWRzKHN0cmVhbS5yZWFkbGluZSg2NTUzNikp +CiAgICBleGNlcHQgKE9TRXJyb3IsIFZhbHVlRXJyb3IpOgogICAgICAgIHJlc3VsdCA9IHsiZXJyb3IiOiAiZGlzcGxheV9ydW50aW1lX3VuYXZhaWxhYmxl +In0KICAgIHByaW50KGpzb24uZHVtcHMocmVzdWx0KSkKICAgIHJldHVybiAxIGlmICJlcnJvciIgaW4gcmVzdWx0IGVsc2UgMAoKCmlmIF9fbmFtZV9fID09 +ICJfX21haW5fXyI6CiAgICByYWlzZSBTeXN0ZW1FeGl0KG1haW4oKSkK """ private static let path = "$HOME/.cmux/cmux-display" diff --git a/Sources/Cloud/PortForward/CloudBrowserAccessState.swift b/Sources/Cloud/PortForward/CloudBrowserAccessState.swift index 09d067f40d8b..ebde49e9fd6b 100644 --- a/Sources/Cloud/PortForward/CloudBrowserAccessState.swift +++ b/Sources/Cloud/PortForward/CloudBrowserAccessState.swift @@ -212,8 +212,9 @@ final class CloudBrowserAccessState { } func didStart(url: URL?, navigationID: ObjectIdentifier? = nil) { - guard let url, owns(url), navigationURL != nil else { return } + guard let url, navigationURL != nil else { return } activeNavigationID = navigationID + guard owns(url) else { return } hasCommittedNavigation = false loaded = false error = nil diff --git a/Sources/Surfaces/CmuxTuiSurfaceProviders.swift b/Sources/Surfaces/CmuxTuiSurfaceProviders.swift index 56f2229ad93a..41e5311bf1ec 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProviders.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProviders.swift @@ -266,8 +266,7 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { machine: machine, scannedPorts: scannedPorts, previousResources: previousResources, - privateAddress: summary.preferredPrivateAddress, - displayPortsOwned: hasDesktop + privateAddress: summary.preferredPrivateAddress, displayPortsOwned: hasDesktop ) resources = resourcesWithPendingCreations(parsed, state: cloudState) } else { @@ -276,8 +275,7 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { machine: machine, scannedPorts: scannedPorts, previousResources: previousResources, - privateAddress: summary.preferredPrivateAddress, - displayPortsOwned: hasDesktop + privateAddress: summary.preferredPrivateAddress, displayPortsOwned: hasDesktop )) appendMissingResources(preservedNonPortResources, to: &fallback) resources = resourcesWithPendingCreations(fallback, state: nil) @@ -746,8 +744,7 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { machine: machine, scannedPorts: ports, previousResources: catalog.authoritativeSnapshot.resources(on: machine), - privateAddress: summary.preferredPrivateAddress, - displayPortsOwned: summary.resolvedKind.hasDesktop + privateAddress: summary.preferredPrivateAddress, displayPortsOwned: summary.resolvedKind.hasDesktop ) } diff --git a/tests/fixtures/cmux-display b/tests/fixtures/cmux-display index 346d0816ec40..86cfff96d804 100755 --- a/tests/fixtures/cmux-display +++ b/tests/fixtures/cmux-display @@ -184,6 +184,10 @@ class DisplayService: pass self.named_processes.pop(number, None) self.terminate_websockify(number) + try: + (self.runtime / str(number) / "session.json").unlink() + except FileNotFoundError: + pass def terminate_websockify(self, number): process = self.websockify_processes.pop(number, None) @@ -256,9 +260,21 @@ class DisplayService: except OSError: pass - def recover_processes(self, number, runtime): + def recover_processes(self, number, runtime, environment): self.processes[number] = [] self.named_processes[number] = {} + session = Path(runtime) / "session.json" + try: + saved = json.loads(session.read_text()) + dbus_pid = int(saved.get("pid", 0)) + dbus = OwnedProcess(dbus_pid) + if dbus_pid > 1 and self.process_alive(dbus): + if saved.get("address"): + environment["DBUS_SESSION_BUS_ADDRESS"] = saved["address"] + environment["DBUS_SESSION_BUS_PID"] = str(dbus_pid) + self.track_process(number, "dbus", dbus) + except (OSError, ValueError, TypeError, json.JSONDecodeError): + pass uid = str(os.getuid()) for name in ("openbox", "tint2", "vncconfig"): marker = str(Path(runtime) / "bin" / f"cmux-display-{number}-{name}") @@ -281,7 +297,7 @@ class DisplayService: if pid > 1: self.track_process(number, "xvnc", OwnedProcess(pid)) - def start_dbus(self, number, environment): + def start_dbus(self, number, environment, runtime): bus = subprocess.run(["dbus-launch", "--sh-syntax"], env=environment, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, timeout=10, @@ -297,6 +313,10 @@ class DisplayService: dbus_pid = 0 if dbus_pid > 1: self.track_process(number, "dbus", OwnedProcess(dbus_pid)) + (Path(runtime) / "session.json").write_text(json.dumps({ + "pid": dbus_pid, + "address": environment.get("DBUS_SESSION_BUS_ADDRESS", "") + })) def start_session_components(self, number, environment, runtime): for executable, args in [ @@ -344,7 +364,7 @@ class DisplayService: if not rfb_is_ready and not vnc: raise RuntimeError("display runtime unavailable") if rfb_is_ready and number not in self.named_processes: - self.recover_processes(number, runtime) + self.recover_processes(number, runtime, environment) if not rfb_is_ready: self.terminate(number) rfb_port = 5900 + number @@ -358,7 +378,7 @@ class DisplayService: stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)) if not self.wait_for_port(rfb_port): raise RuntimeError("Xvnc did not become ready") - self.start_dbus(number, environment) + self.start_dbus(number, environment, runtime) self.start_session_components(number, environment, runtime) elif number in self.named_processes: named = self.named_processes[number] @@ -366,7 +386,7 @@ class DisplayService: if dbus is None or not self.process_alive(dbus): for name in ("openbox", "tint2", "vncconfig"): self.remove_process(number, name) - self.start_dbus(number, environment) + self.start_dbus(number, environment, runtime) self.start_session_components(number, environment, runtime) websockify_process = self.websockify_processes.get(number) if (websockify_process is None or not self.process_alive(websockify_process) From 3d27a164049ece56a78f5f7aa29a43ad23e5d89c Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 18:17:00 -0700 Subject: [PATCH 50/64] test: stay within Swift file budget --- cmuxTests/CmuxTuiSurfaceProviderTests.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/cmuxTests/CmuxTuiSurfaceProviderTests.swift b/cmuxTests/CmuxTuiSurfaceProviderTests.swift index 274ce3191eaa..7da611bbfffa 100644 --- a/cmuxTests/CmuxTuiSurfaceProviderTests.swift +++ b/cmuxTests/CmuxTuiSurfaceProviderTests.swift @@ -1009,7 +1009,6 @@ import Testing LISTEN 0 128 0.0.0.0:3000 0.0.0.0:* """ #expect(CmuxTuiSnapshotParser.listeningPorts(fromSocketListing: ss) == [1337, 3000, 5901]) - #expect(CmuxTuiSnapshotParser.internalPorts.isSuperset(of: [1337])) #expect(CmuxTuiSnapshotParser.displayPorts.isSuperset(of: [5901, 5902, 5916, 6901, 6902, 6916])) #expect(CmuxTuiSnapshotParser.machineHasDesktop(image: "cmux-xfce-vnc:latest")) #expect(!CmuxTuiSnapshotParser.machineHasDesktop(image: "cmuxd-ws:tooling-20260509f")) From 92ed22af704c6178b7f6bd1656ffde24b4df594f Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 18:33:25 -0700 Subject: [PATCH 51/64] fix: sanitize display errors and readiness probes --- Sources/Cloud/CloudGuestDisplayScript.swift | 316 ++++++++++---------- Sources/Cloud/CloudTreeNodeActions.swift | 8 +- tests/fixtures/cmux-display | 9 +- 3 files changed, 169 insertions(+), 164 deletions(-) diff --git a/Sources/Cloud/CloudGuestDisplayScript.swift b/Sources/Cloud/CloudGuestDisplayScript.swift index 7505edaa699c..453e65c782b7 100644 --- a/Sources/Cloud/CloudGuestDisplayScript.swift +++ b/Sources/Cloud/CloudGuestDisplayScript.swift @@ -86,164 +86,164 @@ ICAgICAgIGRpcmVjdG9yeS5ta2Rpcihtb2RlPTBvNzAwLCBwYXJlbnRzPVRydWUsIGV4aXN0X29rPVRy IC8gZiJjbXV4LWRpc3BsYXkte251bWJlcn0te3NvdXJjZS5uYW1lfSIKICAgICAgICBpZiBub3QgdGFyZ2V0LmV4aXN0cygpIG9yIHRhcmdldC5zdGF0KCku c3RfbXRpbWVfbnMgPCBzb3VyY2Uuc3RhdCgpLnN0X210aW1lX25zOgogICAgICAgICAgICBzaHV0aWwuY29weTIoc291cmNlLCB0YXJnZXQpCiAgICAgICAg ICAgIHRhcmdldC5jaG1vZCgwbzcwMCkKICAgICAgICByZXR1cm4gc3RyKHRhcmdldCkKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgd2FpdF9mb3JfcG9y -dChwb3J0LCB0aW1lb3V0PTIwKToKICAgICAgICBkZWFkbGluZSA9IHRpbWUubW9ub3RvbmljKCkgKyB0aW1lb3V0CiAgICAgICAgd2hpbGUgdGltZS5tb25v -dG9uaWMoKSA8IGRlYWRsaW5lOgogICAgICAgICAgICBpZiBsaXN0ZW5pbmcocG9ydCk6CiAgICAgICAgICAgICAgICByZXR1cm4gVHJ1ZQogICAgICAgICAg -ICB0aW1lLnNsZWVwKDAuMSkKICAgICAgICByZXR1cm4gRmFsc2UKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgcHJvY2Vzc19hbGl2ZShwcm9jZXNzKToK -ICAgICAgICBwb2xsID0gZ2V0YXR0cihwcm9jZXNzLCAicG9sbCIsIE5vbmUpCiAgICAgICAgcmV0dXJuIHBvbGwgaXMgTm9uZSBvciBwb2xsKCkgaXMgTm9u -ZQoKICAgIGRlZiBjb21wb25lbnRzX2FsaXZlKHNlbGYsIG51bWJlcik6CiAgICAgICAgbmFtZWQgPSBzZWxmLm5hbWVkX3Byb2Nlc3Nlcy5nZXQobnVtYmVy -KQogICAgICAgIGlmIG5hbWVkIGlzIG5vdCBOb25lOgogICAgICAgICAgICByZXR1cm4gYm9vbChuYW1lZCkgYW5kIGFsbChzZWxmLnByb2Nlc3NfYWxpdmUo -cHJvY2VzcykgZm9yIHByb2Nlc3MgaW4gbmFtZWQudmFsdWVzKCkpIFwKICAgICAgICAgICAgICAgIGFuZCAoInh2bmMiIGluIG5hbWVkIG9yIHJmYl9yZWFk -eShudW1iZXIpKQogICAgICAgIHByb2Nlc3NlcyA9IHNlbGYucHJvY2Vzc2VzLmdldChudW1iZXIsIFtdKQogICAgICAgIHJldHVybiBib29sKHByb2Nlc3Nl -cykgYW5kIGFsbChzZWxmLnByb2Nlc3NfYWxpdmUocHJvY2VzcykgZm9yIHByb2Nlc3MgaW4gcHJvY2Vzc2VzKQoKICAgIGRlZiBzZXJ2aWNlX3JlYWR5KHNl -bGYsIG51bWJlcik6CiAgICAgICAgaWYgbm90IHJlYWR5KG51bWJlcikgb3Igbm90IHNlbGYuY29tcG9uZW50c19hbGl2ZShudW1iZXIpOgogICAgICAgICAg -ICByZXR1cm4gRmFsc2UKICAgICAgICB3ZWJzb2NraWZ5ID0gc2VsZi53ZWJzb2NraWZ5X3Byb2Nlc3Nlcy5nZXQobnVtYmVyKQogICAgICAgIHJldHVybiB3 -ZWJzb2NraWZ5IGlzIE5vbmUgb3Igc2VsZi5wcm9jZXNzX2FsaXZlKHdlYnNvY2tpZnkpCgogICAgZGVmIHRyYWNrX3Byb2Nlc3Moc2VsZiwgbnVtYmVyLCBu -YW1lLCBwcm9jZXNzKToKICAgICAgICBuYW1lZCA9IHNlbGYubmFtZWRfcHJvY2Vzc2VzLnNldGRlZmF1bHQobnVtYmVyLCB7fSkKICAgICAgICBwcmV2aW91 -cyA9IG5hbWVkLmdldChuYW1lKQogICAgICAgIGlmIHByZXZpb3VzIGlzIG5vdCBOb25lOgogICAgICAgICAgICBzZWxmLnByb2Nlc3Nlc1tudW1iZXJdID0g -WwogICAgICAgICAgICAgICAgaXRlbSBmb3IgaXRlbSBpbiBzZWxmLnByb2Nlc3Nlcy5nZXQobnVtYmVyLCBbXSkgaWYgaXRlbSBpcyBub3QgcHJldmlvdXMK -ICAgICAgICAgICAgXQogICAgICAgIHNlbGYucHJvY2Vzc2VzLnNldGRlZmF1bHQobnVtYmVyLCBbXSkuYXBwZW5kKHByb2Nlc3MpCiAgICAgICAgbmFtZWRb -bmFtZV0gPSBwcm9jZXNzCgogICAgZGVmIHJlbW92ZV9wcm9jZXNzKHNlbGYsIG51bWJlciwgbmFtZSk6CiAgICAgICAgcHJvY2VzcyA9IHNlbGYubmFtZWRf -cHJvY2Vzc2VzLnNldGRlZmF1bHQobnVtYmVyLCB7fSkucG9wKG5hbWUsIE5vbmUpCiAgICAgICAgaWYgcHJvY2VzcyBpcyBOb25lOgogICAgICAgICAgICBy -ZXR1cm4KICAgICAgICBzZWxmLnByb2Nlc3Nlc1tudW1iZXJdID0gW2l0ZW0gZm9yIGl0ZW0gaW4gc2VsZi5wcm9jZXNzZXMuZ2V0KG51bWJlciwgW10pIGlm -IGl0ZW0gaXMgbm90IHByb2Nlc3NdCiAgICAgICAgdHJ5OgogICAgICAgICAgICBwcm9jZXNzLnRlcm1pbmF0ZSgpCiAgICAgICAgZXhjZXB0IE9TRXJyb3I6 -CiAgICAgICAgICAgIHBhc3MKCiAgICBkZWYgcmVjb3Zlcl9wcm9jZXNzZXMoc2VsZiwgbnVtYmVyLCBydW50aW1lLCBlbnZpcm9ubWVudCk6CiAgICAgICAg -c2VsZi5wcm9jZXNzZXNbbnVtYmVyXSA9IFtdCiAgICAgICAgc2VsZi5uYW1lZF9wcm9jZXNzZXNbbnVtYmVyXSA9IHt9CiAgICAgICAgc2Vzc2lvbiA9IFBh -dGgocnVudGltZSkgLyAic2Vzc2lvbi5qc29uIgogICAgICAgIHRyeToKICAgICAgICAgICAgc2F2ZWQgPSBqc29uLmxvYWRzKHNlc3Npb24ucmVhZF90ZXh0 -KCkpCiAgICAgICAgICAgIGRidXNfcGlkID0gaW50KHNhdmVkLmdldCgicGlkIiwgMCkpCiAgICAgICAgICAgIGRidXMgPSBPd25lZFByb2Nlc3MoZGJ1c19w -aWQpCiAgICAgICAgICAgIGlmIGRidXNfcGlkID4gMSBhbmQgc2VsZi5wcm9jZXNzX2FsaXZlKGRidXMpOgogICAgICAgICAgICAgICAgaWYgc2F2ZWQuZ2V0 -KCJhZGRyZXNzIik6CiAgICAgICAgICAgICAgICAgICAgZW52aXJvbm1lbnRbIkRCVVNfU0VTU0lPTl9CVVNfQUREUkVTUyJdID0gc2F2ZWRbImFkZHJlc3Mi -XQogICAgICAgICAgICAgICAgZW52aXJvbm1lbnRbIkRCVVNfU0VTU0lPTl9CVVNfUElEIl0gPSBzdHIoZGJ1c19waWQpCiAgICAgICAgICAgICAgICBzZWxm -LnRyYWNrX3Byb2Nlc3MobnVtYmVyLCAiZGJ1cyIsIGRidXMpCiAgICAgICAgZXhjZXB0IChPU0Vycm9yLCBWYWx1ZUVycm9yLCBUeXBlRXJyb3IsIGpzb24u -SlNPTkRlY29kZUVycm9yKToKICAgICAgICAgICAgcGFzcwogICAgICAgIHVpZCA9IHN0cihvcy5nZXR1aWQoKSkKICAgICAgICBmb3IgbmFtZSBpbiAoIm9w -ZW5ib3giLCAidGludDIiLCAidm5jY29uZmlnIik6CiAgICAgICAgICAgIG1hcmtlciA9IHN0cihQYXRoKHJ1bnRpbWUpIC8gImJpbiIgLyBmImNtdXgtZGlz -cGxheS17bnVtYmVyfS17bmFtZX0iKQogICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICBwYXR0ZXJuID0gcmYiXntyZS5lc2NhcGUobWFya2VyKX0o -PzpbWzpzcGFjZTpdXXwkKSIKICAgICAgICAgICAgICAgIG91dHB1dCA9IHN1YnByb2Nlc3MuY2hlY2tfb3V0cHV0KFsicGdyZXAiLCAiLXUiLCB1aWQsICIt -ZiIsIHBhdHRlcm5dLCB0ZXh0PVRydWUpCiAgICAgICAgICAgICAgICBwaWQgPSBpbnQob3V0cHV0LnNwbGl0bGluZXMoKVswXSkKICAgICAgICAgICAgZXhj -ZXB0IChPU0Vycm9yLCBWYWx1ZUVycm9yLCBJbmRleEVycm9yLCBzdWJwcm9jZXNzLkNhbGxlZFByb2Nlc3NFcnJvcik6CiAgICAgICAgICAgICAgICBjb250 -aW51ZQogICAgICAgICAgICBzZWxmLnRyYWNrX3Byb2Nlc3MobnVtYmVyLCBuYW1lLCBPd25lZFByb2Nlc3MocGlkKSkKICAgICAgICB0cnk6CiAgICAgICAg -ICAgIG91dHB1dCA9IHN1YnByb2Nlc3MuY2hlY2tfb3V0cHV0KAogICAgICAgICAgICAgICAgWyJwZ3JlcCIsICItdSIsIHVpZCwgIi1mIiwKICAgICAgICAg -ICAgICAgICByZiJeKD86LipcLyk/KD86WHZuY3xYdGlnZXJ2bmMpLio6e251bWJlcn0uKi1yZmJwb3J0XHMrezU5MDAgKyBudW1iZXJ9KD86XHN8JCkiXSwK -ICAgICAgICAgICAgICAgIHRleHQ9VHJ1ZQogICAgICAgICAgICApCiAgICAgICAgICAgIHBpZCA9IGludChvdXRwdXQuc3BsaXRsaW5lcygpWzBdKQogICAg -ICAgIGV4Y2VwdCAoT1NFcnJvciwgVmFsdWVFcnJvciwgSW5kZXhFcnJvciwgc3VicHJvY2Vzcy5DYWxsZWRQcm9jZXNzRXJyb3IpOgogICAgICAgICAgICBw -aWQgPSAwCiAgICAgICAgaWYgcGlkID4gMToKICAgICAgICAgICAgc2VsZi50cmFja19wcm9jZXNzKG51bWJlciwgInh2bmMiLCBPd25lZFByb2Nlc3MocGlk -KSkKCiAgICBkZWYgc3RhcnRfZGJ1cyhzZWxmLCBudW1iZXIsIGVudmlyb25tZW50LCBydW50aW1lKToKICAgICAgICBidXMgPSBzdWJwcm9jZXNzLnJ1bihb -ImRidXMtbGF1bmNoIiwgIi0tc2gtc3ludGF4Il0sIGVudj1lbnZpcm9ubWVudCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdGRpbj1zdWJwcm9j -ZXNzLkRFVk5VTEwsIHN0ZG91dD1zdWJwcm9jZXNzLlBJUEUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3RkZXJyPXN1YnByb2Nlc3MuREVWTlVM -TCwgdGV4dD1UcnVlLCB0aW1lb3V0PTEwLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNoZWNrPVRydWUpLnN0ZG91dAogICAgICAgIGZvciBsaW5l -IGluIGJ1cy5zcGxpdGxpbmVzKCk6CiAgICAgICAgICAgIGlmICI9IiBub3QgaW4gbGluZToKICAgICAgICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgICAg -IGtleSwgdmFsdWUgPSBsaW5lLnNwbGl0KCI9IiwgMSkKICAgICAgICAgICAgZW52aXJvbm1lbnRba2V5XSA9IHZhbHVlLnN0cmlwKCkucnN0cmlwKCI7Iiku -c3RyaXAoKS5zdHJpcCgiJyIpCiAgICAgICAgdHJ5OgogICAgICAgICAgICBkYnVzX3BpZCA9IGludChlbnZpcm9ubWVudC5nZXQoIkRCVVNfU0VTU0lPTl9C -VVNfUElEIiwgIjAiKSkKICAgICAgICBleGNlcHQgVmFsdWVFcnJvcjoKICAgICAgICAgICAgZGJ1c19waWQgPSAwCiAgICAgICAgaWYgZGJ1c19waWQgPiAx -OgogICAgICAgICAgICBzZWxmLnRyYWNrX3Byb2Nlc3MobnVtYmVyLCAiZGJ1cyIsIE93bmVkUHJvY2VzcyhkYnVzX3BpZCkpCiAgICAgICAgICAgIChQYXRo -KHJ1bnRpbWUpIC8gInNlc3Npb24uanNvbiIpLndyaXRlX3RleHQoanNvbi5kdW1wcyh7CiAgICAgICAgICAgICAgICAicGlkIjogZGJ1c19waWQsCiAgICAg -ICAgICAgICAgICAiYWRkcmVzcyI6IGVudmlyb25tZW50LmdldCgiREJVU19TRVNTSU9OX0JVU19BRERSRVNTIiwgIiIpCiAgICAgICAgICAgIH0pKQoKICAg -IGRlZiBzdGFydF9zZXNzaW9uX2NvbXBvbmVudHMoc2VsZiwgbnVtYmVyLCBlbnZpcm9ubWVudCwgcnVudGltZSk6CiAgICAgICAgZm9yIGV4ZWN1dGFibGUs -IGFyZ3MgaW4gWwogICAgICAgICAgICAoIm9wZW5ib3giLCBbXSksICgidGludDIiLCBbIi1jIiwgIi9ldGMvY211eC90aW50MnJjIl0pLAogICAgICAgICAg -ICAoInZuY2NvbmZpZyIsIFsiLW5vd2luIl0pLAogICAgICAgIF06CiAgICAgICAgICAgIGV4aXN0aW5nID0gc2VsZi5uYW1lZF9wcm9jZXNzZXMuZ2V0KG51 -bWJlciwge30pLmdldChleGVjdXRhYmxlKQogICAgICAgICAgICBpZiBleGlzdGluZyBpcyBub3QgTm9uZSBhbmQgc2VsZi5wcm9jZXNzX2FsaXZlKGV4aXN0 -aW5nKToKICAgICAgICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgICAgIHBhdGggPSBleGVjdXRhYmxlIGlmICIvIiBpbiBleGVjdXRhYmxlIGVsc2Ugc2h1 -dGlsLndoaWNoKGV4ZWN1dGFibGUpCiAgICAgICAgICAgIGlmIG5vdCBwYXRoOgogICAgICAgICAgICAgICAgaWYgZXhlY3V0YWJsZSA9PSAidm5jY29uZmln -IjoKICAgICAgICAgICAgICAgICAgICBjb250aW51ZQogICAgICAgICAgICAgICAgcmFpc2UgUnVudGltZUVycm9yKGYibWlzc2luZyBkaXNwbGF5IGNvbXBv -bmVudDoge2V4ZWN1dGFibGV9IikKICAgICAgICAgICAgcGF0aCA9IHNlbGYuc2NvcGVkX2NvbXBvbmVudF9wYXRoKGV4ZWN1dGFibGUsIHBhdGgsIG51bWJl -ciwgcnVudGltZSkKICAgICAgICAgICAgcHJvY2VzcyA9IHN1YnByb2Nlc3MuUG9wZW4oW3BhdGhdICsgYXJncywgZW52PWVudmlyb25tZW50LAogICAgICAg -ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdGRpbj1zdWJwcm9jZXNzLkRFVk5VTEwsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg -ICAgICAgIHN0ZG91dD1zdWJwcm9jZXNzLkRFVk5VTEwsIHN0ZGVycj1zdWJwcm9jZXNzLkRFVk5VTEwpCiAgICAgICAgICAgIHNlbGYudHJhY2tfcHJvY2Vz -cyhudW1iZXIsIGV4ZWN1dGFibGUsIHByb2Nlc3MpCgogICAgZGVmIHN0YXJ0X3dlYnNvY2tpZnkoc2VsZiwgbnVtYmVyLCBlbnZpcm9ubWVudCwgZXhlY3V0 -YWJsZSk6CiAgICAgICAgcGF0aCA9IGV4ZWN1dGFibGUgaWYgIi8iIGluIGV4ZWN1dGFibGUgZWxzZSBzaHV0aWwud2hpY2goZXhlY3V0YWJsZSkKICAgICAg -ICBpZiBub3QgcGF0aDoKICAgICAgICAgICAgcmFpc2UgTm9WTkNVbmF2YWlsYWJsZSgibWlzc2luZyBkaXNwbGF5IGNvbXBvbmVudDogd2Vic29ja2lmeSIp -CiAgICAgICAgc2VsZi50ZXJtaW5hdGVfd2Vic29ja2lmeShudW1iZXIpCiAgICAgICAgcmZiX3BvcnQsIG5vdm5jX3BvcnQgPSA1OTAwICsgbnVtYmVyLCA2 -OTAwICsgbnVtYmVyCiAgICAgICAgcHJvY2VzcyA9IHN1YnByb2Nlc3MuUG9wZW4oCiAgICAgICAgICAgIFtwYXRoLCAiLS13ZWIiLCAiL3Vzci9zaGFyZS9u -b3ZuYyIsICItLWhlYXJ0YmVhdCIsICIzMCIsCiAgICAgICAgICAgICBmIjEyNy4wLjAuMTp7bm92bmNfcG9ydH0iLCBmIjEyNy4wLjAuMTp7cmZiX3BvcnR9 -Il0sCiAgICAgICAgICAgIGVudj1lbnZpcm9ubWVudCwgc3RkaW49c3VicHJvY2Vzcy5ERVZOVUxMLAogICAgICAgICAgICBzdGRvdXQ9c3VicHJvY2Vzcy5E -RVZOVUxMLCBzdGRlcnI9c3VicHJvY2Vzcy5ERVZOVUxMKQogICAgICAgIHNlbGYud2Vic29ja2lmeV9wcm9jZXNzZXNbbnVtYmVyXSA9IHByb2Nlc3MKICAg -ICAgICBpZiBub3Qgc2VsZi53YWl0X2Zvcl9wb3J0KG5vdm5jX3BvcnQsIHRpbWVvdXQ9MTApOgogICAgICAgICAgICBzZWxmLnRlcm1pbmF0ZV93ZWJzb2Nr -aWZ5KG51bWJlcikKICAgICAgICAgICAgcmFpc2UgTm9WTkNVbmF2YWlsYWJsZSgibm9WTkMgZGlkIG5vdCBiZWNvbWUgcmVhZHkiKQoKICAgIGRlZiBzdGFy -dF9jb21wb25lbnRzKHNlbGYsIG51bWJlciwgZW52aXJvbm1lbnQsIHJ1bnRpbWUpOgogICAgICAgIGlmIHNlbGYuc2VydmljZV9yZWFkeShudW1iZXIpOgog -ICAgICAgICAgICByZXR1cm4KICAgICAgICByZmJfaXNfcmVhZHkgPSByZmJfcmVhZHkobnVtYmVyKQogICAgICAgIHZuYyA9IHNodXRpbC53aGljaCgiWHZu -YyIpIG9yIHNodXRpbC53aGljaCgiWHRpZ2Vydm5jIikKICAgICAgICB3ZWJzb2NraWZ5ID0gc2h1dGlsLndoaWNoKCJ3ZWJzb2NraWZ5IikKICAgICAgICBp -ZiBub3Qgd2Vic29ja2lmeToKICAgICAgICAgICAgcmFpc2UgTm9WTkNVbmF2YWlsYWJsZSgiZGlzcGxheSBydW50aW1lIHVuYXZhaWxhYmxlIikKICAgICAg -ICBpZiBub3QgcmZiX2lzX3JlYWR5IGFuZCBub3Qgdm5jOgogICAgICAgICAgICByYWlzZSBSdW50aW1lRXJyb3IoImRpc3BsYXkgcnVudGltZSB1bmF2YWls -YWJsZSIpCiAgICAgICAgaWYgcmZiX2lzX3JlYWR5IGFuZCBudW1iZXIgbm90IGluIHNlbGYubmFtZWRfcHJvY2Vzc2VzOgogICAgICAgICAgICBzZWxmLnJl -Y292ZXJfcHJvY2Vzc2VzKG51bWJlciwgcnVudGltZSwgZW52aXJvbm1lbnQpCiAgICAgICAgaWYgbm90IHJmYl9pc19yZWFkeToKICAgICAgICAgICAgc2Vs -Zi50ZXJtaW5hdGUobnVtYmVyKQogICAgICAgICAgICByZmJfcG9ydCA9IDU5MDAgKyBudW1iZXIKICAgICAgICAgICAgc2VsZi5wcm9jZXNzZXNbbnVtYmVy -XSA9IFtdCiAgICAgICAgICAgIHNlbGYubmFtZWRfcHJvY2Vzc2VzW251bWJlcl0gPSB7fQogICAgICAgICAgICBzZWxmLnRyYWNrX3Byb2Nlc3MobnVtYmVy -LCAieHZuYyIsIHN1YnByb2Nlc3MuUG9wZW4oCiAgICAgICAgICAgICAgICBbdm5jLCBmIjp7bnVtYmVyfSIsICItZ2VvbWV0cnkiLCBvcy5lbnZpcm9uLmdl -dCgiQ01VWF9WTkNfR0VPTUVUUlkiLCAiMTQ0MHg5MDAiKSwKICAgICAgICAgICAgICAgICAiLWRlcHRoIiwgIjI0IiwgIi1yZmJwb3J0Iiwgc3RyKHJmYl9w -b3J0KSwgIi1sb2NhbGhvc3QiLAogICAgICAgICAgICAgICAgICItU2VjdXJpdHlUeXBlcyIsICJOb25lIiwgIi1BbHdheXNTaGFyZWQiXSwKICAgICAgICAg -ICAgICAgIGVudj1lbnZpcm9ubWVudCwgc3RkaW49c3VicHJvY2Vzcy5ERVZOVUxMLAogICAgICAgICAgICAgICAgc3Rkb3V0PXN1YnByb2Nlc3MuREVWTlVM -TCwgc3RkZXJyPXN1YnByb2Nlc3MuREVWTlVMTCkpCiAgICAgICAgICAgIGlmIG5vdCBzZWxmLndhaXRfZm9yX3BvcnQocmZiX3BvcnQpOgogICAgICAgICAg -ICAgICAgcmFpc2UgUnVudGltZUVycm9yKCJYdm5jIGRpZCBub3QgYmVjb21lIHJlYWR5IikKICAgICAgICAgICAgc2VsZi5zdGFydF9kYnVzKG51bWJlciwg -ZW52aXJvbm1lbnQsIHJ1bnRpbWUpCiAgICAgICAgICAgIHNlbGYuc3RhcnRfc2Vzc2lvbl9jb21wb25lbnRzKG51bWJlciwgZW52aXJvbm1lbnQsIHJ1bnRp -bWUpCiAgICAgICAgZWxpZiBudW1iZXIgaW4gc2VsZi5uYW1lZF9wcm9jZXNzZXM6CiAgICAgICAgICAgIG5hbWVkID0gc2VsZi5uYW1lZF9wcm9jZXNzZXNb -bnVtYmVyXQogICAgICAgICAgICBkYnVzID0gbmFtZWQuZ2V0KCJkYnVzIikKICAgICAgICAgICAgaWYgZGJ1cyBpcyBOb25lIG9yIG5vdCBzZWxmLnByb2Nl -c3NfYWxpdmUoZGJ1cyk6CiAgICAgICAgICAgICAgICBmb3IgbmFtZSBpbiAoIm9wZW5ib3giLCAidGludDIiLCAidm5jY29uZmlnIik6CiAgICAgICAgICAg -ICAgICAgICAgc2VsZi5yZW1vdmVfcHJvY2VzcyhudW1iZXIsIG5hbWUpCiAgICAgICAgICAgICAgICBzZWxmLnN0YXJ0X2RidXMobnVtYmVyLCBlbnZpcm9u -bWVudCwgcnVudGltZSkKICAgICAgICAgICAgc2VsZi5zdGFydF9zZXNzaW9uX2NvbXBvbmVudHMobnVtYmVyLCBlbnZpcm9ubWVudCwgcnVudGltZSkKICAg -ICAgICB3ZWJzb2NraWZ5X3Byb2Nlc3MgPSBzZWxmLndlYnNvY2tpZnlfcHJvY2Vzc2VzLmdldChudW1iZXIpCiAgICAgICAgaWYgKHdlYnNvY2tpZnlfcHJv -Y2VzcyBpcyBOb25lIG9yIG5vdCBzZWxmLnByb2Nlc3NfYWxpdmUod2Vic29ja2lmeV9wcm9jZXNzKQogICAgICAgICAgICAgICAgb3Igbm90IG5vdm5jX3Jl -YWR5KG51bWJlcikpOgogICAgICAgICAgICBzZWxmLnN0YXJ0X3dlYnNvY2tpZnkobnVtYmVyLCBlbnZpcm9ubWVudCwgd2Vic29ja2lmeSkKICAgICAgICAo -cnVudGltZSAvICJlbnYiKS53cml0ZV90ZXh0KGYiZXhwb3J0IERJU1BMQVk9OntudW1iZXJ9XG4iKQoKICAgIGRlZiBzdXBlcnZpc2Uoc2VsZiwgbnVtYmVy -LCBmaW5pc2hlZCk6CiAgICAgICAgcnVudGltZSA9IHNlbGYucnVudGltZSAvIHN0cihudW1iZXIpCiAgICAgICAgcnVudGltZS5ta2Rpcihtb2RlPTBvNzAw -LCBwYXJlbnRzPVRydWUsIGV4aXN0X29rPVRydWUpCiAgICAgICAgZW52aXJvbm1lbnQgPSBkaWN0KG9zLmVudmlyb24sIERJU1BMQVk9ZiI6e251bWJlcn0i -LCBDTVVYX0RFU0tUT1BfUlVOVElNRV9ESVI9c3RyKHJ1bnRpbWUpKQogICAgICAgIGVudmlyb25tZW50WyJDSFJPTUVfQ09ORklHX0hPTUUiXSA9IHN0cihy -dW50aW1lIC8gImNocm9tZS1jb25maWciKQogICAgICAgIGVudmlyb25tZW50WyJYREdfQ09ORklHX0hPTUUiXSA9IHN0cihydW50aW1lIC8gInhkZy1jb25m -aWciKQogICAgICAgIGZvciBrZXkgaW4gKCJOT1RJRllfU09DS0VUIiwgIkRCVVNfU0VTU0lPTl9CVVNfQUREUkVTUyIsICJEQlVTX1NFU1NJT05fQlVTX1BJ -RCIsCiAgICAgICAgICAgICAgICAgICAgIkFUX1NQSV9CVVNfQUREUkVTUyIsICJBVF9TUElfQlVTIik6CiAgICAgICAgICAgIGVudmlyb25tZW50LnBvcChr -ZXksIE5vbmUpCiAgICAgICAgd2hpbGUgbm90IHNlbGYuc2h1dGRvd24uaXNfc2V0KCk6CiAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgICMgVGhl -IHN1cGVydmlzb3IgaXMgYSBoZWFsdGggY2hlY2ssIG5vdCBhIHRpbWVyLWRyaXZlbiByZXN0YXJ0LgogICAgICAgICAgICAgICAgIyBQcmVzZXJ2ZSBhbiBh -Y3RpdmUgUkZCL25vVk5DIHNlc3Npb24gdW50aWwgaXRzIG93bmVkIGVuZHBvaW50cyBmYWlsLgogICAgICAgICAgICAgICAgaWYgbm90IHNlbGYuc2Vydmlj -ZV9yZWFkeShudW1iZXIpOgogICAgICAgICAgICAgICAgICAgIHNlbGYuc3RhcnRfY29tcG9uZW50cyhudW1iZXIsIGVudmlyb25tZW50LCBydW50aW1lKQog -ICAgICAgICAgICAgICAgc3RhdGUgPSAicnVubmluZyIgaWYgc2VsZi5zZXJ2aWNlX3JlYWR5KG51bWJlcikgZWxzZSAidW5hdmFpbGFibGUiCiAgICAgICAg -ICAgIGV4Y2VwdCBOb1ZOQ1VuYXZhaWxhYmxlOgogICAgICAgICAgICAgICAgc2VsZi50ZXJtaW5hdGVfd2Vic29ja2lmeShudW1iZXIpCiAgICAgICAgICAg -ICAgICBzdGF0ZSA9ICJ1bmF2YWlsYWJsZSIKICAgICAgICAgICAgZXhjZXB0IChPU0Vycm9yLCBSdW50aW1lRXJyb3IsIHN1YnByb2Nlc3MuU3VicHJvY2Vz -c0Vycm9yKToKICAgICAgICAgICAgICAgIHNlbGYudGVybWluYXRlKG51bWJlcikKICAgICAgICAgICAgICAgIHN0YXRlID0gInVuYXZhaWxhYmxlIgogICAg -ICAgICAgICB3aXRoIHNlbGYubG9jazoKICAgICAgICAgICAgICAgIHNlbGYuc3RhdGVzW251bWJlcl0gPSBzdGF0ZQogICAgICAgICAgICBmaW5pc2hlZC5z -ZXQoKQogICAgICAgICAgICBzZWxmLnNodXRkb3duLndhaXQoMzApCiAgICAgICAgc2VsZi50ZXJtaW5hdGUobnVtYmVyKQoKICAgIGRlZiBzbmFwc2hvdChz -ZWxmKToKICAgICAgICBwcmltYXJ5X3N0YXRlID0gInJ1bm5pbmciIGlmIHJlYWR5KDEpIGVsc2UgInVuYXZhaWxhYmxlIgogICAgICAgIHdpdGggc2VsZi5s -b2NrOgogICAgICAgICAgICByZXR1cm4geyJ2ZXJzaW9uIjogMSwgImNhbkNyZWF0ZSI6IGxlbihzZWxmLmpvYnMpIDwgTUFYX0RJU1BMQVlTIC0gMSwKICAg -ICAgICAgICAgICAgICAgICAiZGlzcGxheXMiOiBbZGVzY3JpcHRvcigxLCBwcmltYXJ5X3N0YXRlKV0KICAgICAgICAgICAgICAgICAgICArIFtkZXNjcmlw -dG9yKG4sIHNlbGYuc3RhdGVzLmdldChuLCAibGF1bmNoaW5nIikpIGZvciBuIGluIHNlbGYuY2F0YWxvZy5udW1iZXJzKCldfQoKICAgIGRlZiBoYW5kbGUo -c2VsZiwgcmVxdWVzdCk6CiAgICAgICAgaWYgcmVxdWVzdC5nZXQoImFjdGlvbiIpID09ICJsaXN0IjoKICAgICAgICAgICAgcmV0dXJuIHNlbGYuc25hcHNo -b3QoKQogICAgICAgIGlmIHJlcXVlc3QuZ2V0KCJhY3Rpb24iKSAhPSAiY3JlYXRlIjoKICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJvcigidW5zdXBwb3J0 -ZWQgZGlzcGxheSBhY3Rpb24iKQogICAgICAgIG51bWJlciA9IHNlbGYuY2F0YWxvZy5hbGxvY2F0ZShyZXF1ZXN0LmdldCgicmVxdWVzdCIpKQogICAgICAg -IHNlbGYuc3RhcnQobnVtYmVyKS53YWl0KDU1KQogICAgICAgIHJlc3VsdCA9IHNlbGYuc25hcHNob3QoKQogICAgICAgIHJlc3VsdFsiY3JlYXRlZCJdID0g -ZiJkaXNwbGF5OntudW1iZXJ9IgogICAgICAgIGlmIHNlbGYuc3RhdGVzLmdldChudW1iZXIpICE9ICJydW5uaW5nIjoKICAgICAgICAgICAgcmVzdWx0WyJl -cnJvciJdID0gImRpc3BsYXlfc3RhcnRfZmFpbGVkIgogICAgICAgIHJldHVybiByZXN1bHQKCgpkZWYgc2VydmUocnVudGltZSk6CiAgICBydW50aW1lID0g -UGF0aChydW50aW1lKQogICAgcnVudGltZS5ta2Rpcihtb2RlPTBvNzAwLCBwYXJlbnRzPVRydWUsIGV4aXN0X29rPVRydWUpCiAgICB3aXRoIChydW50aW1l -IC8gImRpc3BsYXktY29udHJvbC5sb2NrIikub3BlbigidyIpIGFzIGxvY2s6CiAgICAgICAgdHJ5OgogICAgICAgICAgICBmY250bC5mbG9jayhsb2NrLCBm -Y250bC5MT0NLX0VYIHwgZmNudGwuTE9DS19OQikKICAgICAgICBleGNlcHQgQmxvY2tpbmdJT0Vycm9yOgogICAgICAgICAgICByZXR1cm4KICAgICAgICBp -ZiBub3QgYWxsKHNodXRpbC53aGljaChuYW1lKSBmb3IgbmFtZSBpbiAoIndlYnNvY2tpZnkiLCAib3BlbmJveCIsICJkYnVzLWxhdW5jaCIpKToKICAgICAg -ICAgICAgcmFpc2UgVmFsdWVFcnJvcigiZGlzcGxheSBydW50aW1lIHVuYXZhaWxhYmxlIikKICAgICAgICBpZiBub3QgKHNodXRpbC53aGljaCgiWHZuYyIp -IG9yIHNodXRpbC53aGljaCgiWHRpZ2Vydm5jIikpOgogICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJkaXNwbGF5IHJ1bnRpbWUgdW5hdmFpbGFibGUi -KQogICAgICAgIHNlcnZpY2UgPSBEaXNwbGF5U2VydmljZShEaXNwbGF5Q2F0YWxvZyhQYXRoLmhvbWUoKSAvICIuY211eCIgLyAiZGlzcGxheXMiKSwgcnVu -dGltZSAvICJkaXNwbGF5cyIpCiAgICAgICAgZm9yIG51bWJlciBpbiBzZXJ2aWNlLmNhdGFsb2cubnVtYmVycygpOgogICAgICAgICAgICBzZXJ2aWNlLnN0 -YXJ0KG51bWJlcikKICAgICAgICBwYXRoID0gcnVudGltZSAvICJkaXNwbGF5LWNvbnRyb2wuc29jayIKICAgICAgICBpZiBwYXRoLmV4aXN0cygpOgogICAg -ICAgICAgICBwYXRoLnVubGluaygpCgogICAgICAgIGNsYXNzIEhhbmRsZXIoc29ja2V0c2VydmVyLlN0cmVhbVJlcXVlc3RIYW5kbGVyKToKICAgICAgICAg -ICAgZGVmIGhhbmRsZShzZWxmKToKICAgICAgICAgICAgICAgIHNlbGYuY29ubmVjdGlvbi5zZXR0aW1lb3V0KDYwKQogICAgICAgICAgICAgICAgdHJ5Ogog -ICAgICAgICAgICAgICAgICAgIHJhdyA9IHNlbGYucmZpbGUucmVhZGxpbmUoNDA5NykKICAgICAgICAgICAgICAgICAgICBpZiBsZW4ocmF3KSA+IDQwOTY6 -CiAgICAgICAgICAgICAgICAgICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoImRpc3BsYXkgcmVxdWVzdCB0b28gbGFyZ2UiKQogICAgICAgICAgICAgICAgICAg -IHJlc3VsdCA9IHNlcnZpY2UuaGFuZGxlKGpzb24ubG9hZHMocmF3KSkKICAgICAgICAgICAgICAgIGV4Y2VwdCAoVmFsdWVFcnJvciwgS2V5RXJyb3IsIFR5 -cGVFcnJvciwgT1NFcnJvcik6CiAgICAgICAgICAgICAgICAgICAgcmVzdWx0ID0geyJlcnJvciI6ICJkaXNwbGF5X3JlcXVlc3RfZmFpbGVkIn0KICAgICAg -ICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgICAgICBzZWxmLndmaWxlLndyaXRlKGpzb24uZHVtcHMocmVzdWx0KS5lbmNvZGUoKSArIGIiXG4iKQog -ICAgICAgICAgICAgICAgZXhjZXB0IE9TRXJyb3I6CiAgICAgICAgICAgICAgICAgICAgcGFzcyAgIyBUaGUgcGVyc2lzdGVkIHJlY2VpcHQgcmVtYWlucyBy -ZXBsYXlhYmxlIGFmdGVyIGRpc2Nvbm5lY3QuCgogICAgICAgIGNsYXNzIFNlcnZlcihzb2NrZXRzZXJ2ZXIuVGhyZWFkaW5nVW5peFN0cmVhbVNlcnZlcik6 -CiAgICAgICAgICAgIGRhZW1vbl90aHJlYWRzID0gVHJ1ZQoKICAgICAgICBwcmV2aW91c191bWFzayA9IG9zLnVtYXNrKDBvMDc3KQogICAgICAgIHRyeToK -ICAgICAgICAgICAgd2l0aCBTZXJ2ZXIoc3RyKHBhdGgpLCBIYW5kbGVyKSBhcyBzZXJ2ZXI6CiAgICAgICAgICAgICAgICBvcy51bWFzayhwcmV2aW91c191 -bWFzaykKICAgICAgICAgICAgICAgIHNlcnZlci5zZXJ2ZV9mb3JldmVyKCkKICAgICAgICBmaW5hbGx5OgogICAgICAgICAgICBvcy51bWFzayhwcmV2aW91 -c191bWFzaykKICAgICAgICAgICAgc2VydmljZS5zaHV0ZG93bi5zZXQoKQogICAgICAgICAgICBwYXRoLnVubGluayhtaXNzaW5nX29rPVRydWUpCgoKZGVm -IG1haW4oKToKICAgIHBhcnNlciA9IGFyZ3BhcnNlLkFyZ3VtZW50UGFyc2VyKGRlc2NyaXB0aW9uPV9fZG9jX18pCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50 -KCJhY3Rpb24iLCBjaG9pY2VzPVsibGlzdCIsICJjcmVhdGUiLCAic2VydmUiXSkKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoIi0tcmVxdWVzdC1pZCIpCiAg -ICBhcmdzID0gcGFyc2VyLnBhcnNlX2FyZ3MoKQogICAgcnVudGltZSA9IG9zLmVudmlyb24uZ2V0KCJDTVVYX0RFU0tUT1BfUlVOVElNRV9ESVIiLCAiL3J1 -bi9jbXV4LWRlc2t0b3AiKQogICAgaWYgYXJncy5hY3Rpb24gPT0gInNlcnZlIjoKICAgICAgICBzZXJ2ZShydW50aW1lKQogICAgICAgIHJldHVybiAwCiAg -ICBpZiBhcmdzLmFjdGlvbiA9PSAiY3JlYXRlIiBhbmQgbm90IGFyZ3MucmVxdWVzdF9pZDoKICAgICAgICBwYXJzZXIuZXJyb3IoImNyZWF0ZSByZXF1aXJl -cyAtLXJlcXVlc3QtaWQgKHJldGFpbiBpdCB3aGVuIHJldHJ5aW5nKSIpCiAgICB0cnk6CiAgICAgICAgd2l0aCBzb2NrZXQuc29ja2V0KHNvY2tldC5BRl9V -TklYLCBzb2NrZXQuU09DS19TVFJFQU0pIGFzIGNsaWVudDoKICAgICAgICAgICAgY2xpZW50LnNldHRpbWVvdXQoNjApCiAgICAgICAgICAgIGNsaWVudC5j -b25uZWN0KHN0cihQYXRoKHJ1bnRpbWUpIC8gImRpc3BsYXktY29udHJvbC5zb2NrIikpCiAgICAgICAgICAgIGNsaWVudC5zZW5kYWxsKGpzb24uZHVtcHMo -eyJhY3Rpb24iOiBhcmdzLmFjdGlvbiwgInJlcXVlc3QiOiBhcmdzLnJlcXVlc3RfaWR9KS5lbmNvZGUoKSArIGIiXG4iKQogICAgICAgICAgICB3aXRoIGNs -aWVudC5tYWtlZmlsZSgicmIiKSBhcyBzdHJlYW06CiAgICAgICAgICAgICAgICByZXN1bHQgPSBqc29uLmxvYWRzKHN0cmVhbS5yZWFkbGluZSg2NTUzNikp -CiAgICBleGNlcHQgKE9TRXJyb3IsIFZhbHVlRXJyb3IpOgogICAgICAgIHJlc3VsdCA9IHsiZXJyb3IiOiAiZGlzcGxheV9ydW50aW1lX3VuYXZhaWxhYmxl -In0KICAgIHByaW50KGpzb24uZHVtcHMocmVzdWx0KSkKICAgIHJldHVybiAxIGlmICJlcnJvciIgaW4gcmVzdWx0IGVsc2UgMAoKCmlmIF9fbmFtZV9fID09 -ICJfX21haW5fXyI6CiAgICByYWlzZSBTeXN0ZW1FeGl0KG1haW4oKSkK +dChwb3J0LCB0aW1lb3V0PTIwKToKICAgICAgICB0cnk6CiAgICAgICAgICAgIHdpdGggc29ja2V0LmNyZWF0ZV9jb25uZWN0aW9uKCgiMTI3LjAuMC4xIiwg +cG9ydCksIHRpbWVvdXQ9dGltZW91dCk6CiAgICAgICAgICAgICAgICByZXR1cm4gVHJ1ZQogICAgICAgIGV4Y2VwdCBPU0Vycm9yOgogICAgICAgICAgICBy +ZXR1cm4gRmFsc2UKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgcHJvY2Vzc19hbGl2ZShwcm9jZXNzKToKICAgICAgICBwb2xsID0gZ2V0YXR0cihwcm9j +ZXNzLCAicG9sbCIsIE5vbmUpCiAgICAgICAgcmV0dXJuIHBvbGwgaXMgTm9uZSBvciBwb2xsKCkgaXMgTm9uZQoKICAgIGRlZiBjb21wb25lbnRzX2FsaXZl +KHNlbGYsIG51bWJlcik6CiAgICAgICAgbmFtZWQgPSBzZWxmLm5hbWVkX3Byb2Nlc3Nlcy5nZXQobnVtYmVyKQogICAgICAgIGlmIG5hbWVkIGlzIG5vdCBO +b25lOgogICAgICAgICAgICByZXR1cm4gYm9vbChuYW1lZCkgYW5kIGFsbChzZWxmLnByb2Nlc3NfYWxpdmUocHJvY2VzcykgZm9yIHByb2Nlc3MgaW4gbmFt +ZWQudmFsdWVzKCkpIFwKICAgICAgICAgICAgICAgIGFuZCAoInh2bmMiIGluIG5hbWVkIG9yIHJmYl9yZWFkeShudW1iZXIpKQogICAgICAgIHByb2Nlc3Nl +cyA9IHNlbGYucHJvY2Vzc2VzLmdldChudW1iZXIsIFtdKQogICAgICAgIHJldHVybiBib29sKHByb2Nlc3NlcykgYW5kIGFsbChzZWxmLnByb2Nlc3NfYWxp +dmUocHJvY2VzcykgZm9yIHByb2Nlc3MgaW4gcHJvY2Vzc2VzKQoKICAgIGRlZiBzZXJ2aWNlX3JlYWR5KHNlbGYsIG51bWJlcik6CiAgICAgICAgaWYgbm90 +IHJlYWR5KG51bWJlcikgb3Igbm90IHNlbGYuY29tcG9uZW50c19hbGl2ZShudW1iZXIpOgogICAgICAgICAgICByZXR1cm4gRmFsc2UKICAgICAgICB3ZWJz +b2NraWZ5ID0gc2VsZi53ZWJzb2NraWZ5X3Byb2Nlc3Nlcy5nZXQobnVtYmVyKQogICAgICAgIHJldHVybiB3ZWJzb2NraWZ5IGlzIE5vbmUgb3Igc2VsZi5w +cm9jZXNzX2FsaXZlKHdlYnNvY2tpZnkpCgogICAgZGVmIHRyYWNrX3Byb2Nlc3Moc2VsZiwgbnVtYmVyLCBuYW1lLCBwcm9jZXNzKToKICAgICAgICBuYW1l +ZCA9IHNlbGYubmFtZWRfcHJvY2Vzc2VzLnNldGRlZmF1bHQobnVtYmVyLCB7fSkKICAgICAgICBwcmV2aW91cyA9IG5hbWVkLmdldChuYW1lKQogICAgICAg +IGlmIHByZXZpb3VzIGlzIG5vdCBOb25lOgogICAgICAgICAgICBzZWxmLnByb2Nlc3Nlc1tudW1iZXJdID0gWwogICAgICAgICAgICAgICAgaXRlbSBmb3Ig +aXRlbSBpbiBzZWxmLnByb2Nlc3Nlcy5nZXQobnVtYmVyLCBbXSkgaWYgaXRlbSBpcyBub3QgcHJldmlvdXMKICAgICAgICAgICAgXQogICAgICAgIHNlbGYu +cHJvY2Vzc2VzLnNldGRlZmF1bHQobnVtYmVyLCBbXSkuYXBwZW5kKHByb2Nlc3MpCiAgICAgICAgbmFtZWRbbmFtZV0gPSBwcm9jZXNzCgogICAgZGVmIHJl +bW92ZV9wcm9jZXNzKHNlbGYsIG51bWJlciwgbmFtZSk6CiAgICAgICAgcHJvY2VzcyA9IHNlbGYubmFtZWRfcHJvY2Vzc2VzLnNldGRlZmF1bHQobnVtYmVy +LCB7fSkucG9wKG5hbWUsIE5vbmUpCiAgICAgICAgaWYgcHJvY2VzcyBpcyBOb25lOgogICAgICAgICAgICByZXR1cm4KICAgICAgICBzZWxmLnByb2Nlc3Nl +c1tudW1iZXJdID0gW2l0ZW0gZm9yIGl0ZW0gaW4gc2VsZi5wcm9jZXNzZXMuZ2V0KG51bWJlciwgW10pIGlmIGl0ZW0gaXMgbm90IHByb2Nlc3NdCiAgICAg +ICAgdHJ5OgogICAgICAgICAgICBwcm9jZXNzLnRlcm1pbmF0ZSgpCiAgICAgICAgZXhjZXB0IE9TRXJyb3I6CiAgICAgICAgICAgIHBhc3MKCiAgICBkZWYg +cmVjb3Zlcl9wcm9jZXNzZXMoc2VsZiwgbnVtYmVyLCBydW50aW1lLCBlbnZpcm9ubWVudCk6CiAgICAgICAgc2VsZi5wcm9jZXNzZXNbbnVtYmVyXSA9IFtd +CiAgICAgICAgc2VsZi5uYW1lZF9wcm9jZXNzZXNbbnVtYmVyXSA9IHt9CiAgICAgICAgc2Vzc2lvbiA9IFBhdGgocnVudGltZSkgLyAic2Vzc2lvbi5qc29u +IgogICAgICAgIHRyeToKICAgICAgICAgICAgc2F2ZWQgPSBqc29uLmxvYWRzKHNlc3Npb24ucmVhZF90ZXh0KCkpCiAgICAgICAgICAgIGRidXNfcGlkID0g +aW50KHNhdmVkLmdldCgicGlkIiwgMCkpCiAgICAgICAgICAgIGRidXMgPSBPd25lZFByb2Nlc3MoZGJ1c19waWQpCiAgICAgICAgICAgIGlmIGRidXNfcGlk +ID4gMSBhbmQgc2VsZi5wcm9jZXNzX2FsaXZlKGRidXMpOgogICAgICAgICAgICAgICAgaWYgc2F2ZWQuZ2V0KCJhZGRyZXNzIik6CiAgICAgICAgICAgICAg +ICAgICAgZW52aXJvbm1lbnRbIkRCVVNfU0VTU0lPTl9CVVNfQUREUkVTUyJdID0gc2F2ZWRbImFkZHJlc3MiXQogICAgICAgICAgICAgICAgZW52aXJvbm1l +bnRbIkRCVVNfU0VTU0lPTl9CVVNfUElEIl0gPSBzdHIoZGJ1c19waWQpCiAgICAgICAgICAgICAgICBzZWxmLnRyYWNrX3Byb2Nlc3MobnVtYmVyLCAiZGJ1 +cyIsIGRidXMpCiAgICAgICAgZXhjZXB0IChPU0Vycm9yLCBWYWx1ZUVycm9yLCBUeXBlRXJyb3IsIGpzb24uSlNPTkRlY29kZUVycm9yKToKICAgICAgICAg +ICAgcGFzcwogICAgICAgIHVpZCA9IHN0cihvcy5nZXR1aWQoKSkKICAgICAgICBmb3IgbmFtZSBpbiAoIm9wZW5ib3giLCAidGludDIiLCAidm5jY29uZmln +Iik6CiAgICAgICAgICAgIG1hcmtlciA9IHN0cihQYXRoKHJ1bnRpbWUpIC8gImJpbiIgLyBmImNtdXgtZGlzcGxheS17bnVtYmVyfS17bmFtZX0iKQogICAg +ICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICBwYXR0ZXJuID0gcmYiXntyZS5lc2NhcGUobWFya2VyKX0oPzpbWzpzcGFjZTpdXXwkKSIKICAgICAgICAg +ICAgICAgIG91dHB1dCA9IHN1YnByb2Nlc3MuY2hlY2tfb3V0cHV0KFsicGdyZXAiLCAiLXUiLCB1aWQsICItZiIsIHBhdHRlcm5dLCB0ZXh0PVRydWUpCiAg +ICAgICAgICAgICAgICBwaWQgPSBpbnQob3V0cHV0LnNwbGl0bGluZXMoKVswXSkKICAgICAgICAgICAgZXhjZXB0IChPU0Vycm9yLCBWYWx1ZUVycm9yLCBJ +bmRleEVycm9yLCBzdWJwcm9jZXNzLkNhbGxlZFByb2Nlc3NFcnJvcik6CiAgICAgICAgICAgICAgICBjb250aW51ZQogICAgICAgICAgICBzZWxmLnRyYWNr +X3Byb2Nlc3MobnVtYmVyLCBuYW1lLCBPd25lZFByb2Nlc3MocGlkKSkKICAgICAgICB0cnk6CiAgICAgICAgICAgIG91dHB1dCA9IHN1YnByb2Nlc3MuY2hl +Y2tfb3V0cHV0KAogICAgICAgICAgICAgICAgWyJwZ3JlcCIsICItdSIsIHVpZCwgIi1mIiwKICAgICAgICAgICAgICAgICByZiJeKD86LipcLyk/KD86WHZu +Y3xYdGlnZXJ2bmMpLio6e251bWJlcn0uKi1yZmJwb3J0XHMrezU5MDAgKyBudW1iZXJ9KD86XHN8JCkiXSwKICAgICAgICAgICAgICAgIHRleHQ9VHJ1ZQog +ICAgICAgICAgICApCiAgICAgICAgICAgIHBpZCA9IGludChvdXRwdXQuc3BsaXRsaW5lcygpWzBdKQogICAgICAgIGV4Y2VwdCAoT1NFcnJvciwgVmFsdWVF +cnJvciwgSW5kZXhFcnJvciwgc3VicHJvY2Vzcy5DYWxsZWRQcm9jZXNzRXJyb3IpOgogICAgICAgICAgICBwaWQgPSAwCiAgICAgICAgaWYgcGlkID4gMToK +ICAgICAgICAgICAgc2VsZi50cmFja19wcm9jZXNzKG51bWJlciwgInh2bmMiLCBPd25lZFByb2Nlc3MocGlkKSkKCiAgICBkZWYgc3RhcnRfZGJ1cyhzZWxm +LCBudW1iZXIsIGVudmlyb25tZW50LCBydW50aW1lKToKICAgICAgICBidXMgPSBzdWJwcm9jZXNzLnJ1bihbImRidXMtbGF1bmNoIiwgIi0tc2gtc3ludGF4 +Il0sIGVudj1lbnZpcm9ubWVudCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdGRpbj1zdWJwcm9jZXNzLkRFVk5VTEwsIHN0ZG91dD1zdWJwcm9j +ZXNzLlBJUEUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3RkZXJyPXN1YnByb2Nlc3MuREVWTlVMTCwgdGV4dD1UcnVlLCB0aW1lb3V0PTEwLAog +ICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNoZWNrPVRydWUpLnN0ZG91dAogICAgICAgIGZvciBsaW5lIGluIGJ1cy5zcGxpdGxpbmVzKCk6CiAgICAg +ICAgICAgIGlmICI9IiBub3QgaW4gbGluZToKICAgICAgICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgICAgIGtleSwgdmFsdWUgPSBsaW5lLnNwbGl0KCI9 +IiwgMSkKICAgICAgICAgICAgZW52aXJvbm1lbnRba2V5XSA9IHZhbHVlLnN0cmlwKCkucnN0cmlwKCI7Iikuc3RyaXAoKS5zdHJpcCgiJyIpCiAgICAgICAg +dHJ5OgogICAgICAgICAgICBkYnVzX3BpZCA9IGludChlbnZpcm9ubWVudC5nZXQoIkRCVVNfU0VTU0lPTl9CVVNfUElEIiwgIjAiKSkKICAgICAgICBleGNl +cHQgVmFsdWVFcnJvcjoKICAgICAgICAgICAgZGJ1c19waWQgPSAwCiAgICAgICAgaWYgZGJ1c19waWQgPiAxOgogICAgICAgICAgICBzZWxmLnRyYWNrX3By +b2Nlc3MobnVtYmVyLCAiZGJ1cyIsIE93bmVkUHJvY2VzcyhkYnVzX3BpZCkpCiAgICAgICAgICAgIChQYXRoKHJ1bnRpbWUpIC8gInNlc3Npb24uanNvbiIp +LndyaXRlX3RleHQoanNvbi5kdW1wcyh7CiAgICAgICAgICAgICAgICAicGlkIjogZGJ1c19waWQsCiAgICAgICAgICAgICAgICAiYWRkcmVzcyI6IGVudmly +b25tZW50LmdldCgiREJVU19TRVNTSU9OX0JVU19BRERSRVNTIiwgIiIpCiAgICAgICAgICAgIH0pKQoKICAgIGRlZiBzdGFydF9zZXNzaW9uX2NvbXBvbmVu +dHMoc2VsZiwgbnVtYmVyLCBlbnZpcm9ubWVudCwgcnVudGltZSk6CiAgICAgICAgZm9yIGV4ZWN1dGFibGUsIGFyZ3MgaW4gWwogICAgICAgICAgICAoIm9w +ZW5ib3giLCBbXSksICgidGludDIiLCBbIi1jIiwgIi9ldGMvY211eC90aW50MnJjIl0pLAogICAgICAgICAgICAoInZuY2NvbmZpZyIsIFsiLW5vd2luIl0p +LAogICAgICAgIF06CiAgICAgICAgICAgIGV4aXN0aW5nID0gc2VsZi5uYW1lZF9wcm9jZXNzZXMuZ2V0KG51bWJlciwge30pLmdldChleGVjdXRhYmxlKQog +ICAgICAgICAgICBpZiBleGlzdGluZyBpcyBub3QgTm9uZSBhbmQgc2VsZi5wcm9jZXNzX2FsaXZlKGV4aXN0aW5nKToKICAgICAgICAgICAgICAgIGNvbnRp +bnVlCiAgICAgICAgICAgIHBhdGggPSBleGVjdXRhYmxlIGlmICIvIiBpbiBleGVjdXRhYmxlIGVsc2Ugc2h1dGlsLndoaWNoKGV4ZWN1dGFibGUpCiAgICAg +ICAgICAgIGlmIG5vdCBwYXRoOgogICAgICAgICAgICAgICAgaWYgZXhlY3V0YWJsZSA9PSAidm5jY29uZmlnIjoKICAgICAgICAgICAgICAgICAgICBjb250 +aW51ZQogICAgICAgICAgICAgICAgcmFpc2UgUnVudGltZUVycm9yKGYibWlzc2luZyBkaXNwbGF5IGNvbXBvbmVudDoge2V4ZWN1dGFibGV9IikKICAgICAg +ICAgICAgcGF0aCA9IHNlbGYuc2NvcGVkX2NvbXBvbmVudF9wYXRoKGV4ZWN1dGFibGUsIHBhdGgsIG51bWJlciwgcnVudGltZSkKICAgICAgICAgICAgcHJv +Y2VzcyA9IHN1YnByb2Nlc3MuUG9wZW4oW3BhdGhdICsgYXJncywgZW52PWVudmlyb25tZW50LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICBzdGRpbj1zdWJwcm9jZXNzLkRFVk5VTEwsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0ZG91dD1zdWJwcm9jZXNzLkRF +Vk5VTEwsIHN0ZGVycj1zdWJwcm9jZXNzLkRFVk5VTEwpCiAgICAgICAgICAgIHNlbGYudHJhY2tfcHJvY2VzcyhudW1iZXIsIGV4ZWN1dGFibGUsIHByb2Nl +c3MpCgogICAgZGVmIHN0YXJ0X3dlYnNvY2tpZnkoc2VsZiwgbnVtYmVyLCBlbnZpcm9ubWVudCwgZXhlY3V0YWJsZSk6CiAgICAgICAgcGF0aCA9IGV4ZWN1 +dGFibGUgaWYgIi8iIGluIGV4ZWN1dGFibGUgZWxzZSBzaHV0aWwud2hpY2goZXhlY3V0YWJsZSkKICAgICAgICBpZiBub3QgcGF0aDoKICAgICAgICAgICAg +cmFpc2UgTm9WTkNVbmF2YWlsYWJsZSgibWlzc2luZyBkaXNwbGF5IGNvbXBvbmVudDogd2Vic29ja2lmeSIpCiAgICAgICAgc2VsZi50ZXJtaW5hdGVfd2Vi +c29ja2lmeShudW1iZXIpCiAgICAgICAgcmZiX3BvcnQsIG5vdm5jX3BvcnQgPSA1OTAwICsgbnVtYmVyLCA2OTAwICsgbnVtYmVyCiAgICAgICAgcHJvY2Vz +cyA9IHN1YnByb2Nlc3MuUG9wZW4oCiAgICAgICAgICAgIFtwYXRoLCAiLS13ZWIiLCAiL3Vzci9zaGFyZS9ub3ZuYyIsICItLWhlYXJ0YmVhdCIsICIzMCIs +CiAgICAgICAgICAgICBmIjEyNy4wLjAuMTp7bm92bmNfcG9ydH0iLCBmIjEyNy4wLjAuMTp7cmZiX3BvcnR9Il0sCiAgICAgICAgICAgIGVudj1lbnZpcm9u +bWVudCwgc3RkaW49c3VicHJvY2Vzcy5ERVZOVUxMLAogICAgICAgICAgICBzdGRvdXQ9c3VicHJvY2Vzcy5ERVZOVUxMLCBzdGRlcnI9c3VicHJvY2Vzcy5E +RVZOVUxMKQogICAgICAgIHNlbGYud2Vic29ja2lmeV9wcm9jZXNzZXNbbnVtYmVyXSA9IHByb2Nlc3MKICAgICAgICBpZiBub3Qgc2VsZi53YWl0X2Zvcl9w +b3J0KG5vdm5jX3BvcnQsIHRpbWVvdXQ9MTApOgogICAgICAgICAgICBzZWxmLnRlcm1pbmF0ZV93ZWJzb2NraWZ5KG51bWJlcikKICAgICAgICAgICAgcmFp +c2UgTm9WTkNVbmF2YWlsYWJsZSgibm9WTkMgZGlkIG5vdCBiZWNvbWUgcmVhZHkiKQoKICAgIGRlZiBzdGFydF9jb21wb25lbnRzKHNlbGYsIG51bWJlciwg +ZW52aXJvbm1lbnQsIHJ1bnRpbWUpOgogICAgICAgIGlmIHNlbGYuc2VydmljZV9yZWFkeShudW1iZXIpOgogICAgICAgICAgICByZXR1cm4KICAgICAgICBy +ZmJfaXNfcmVhZHkgPSByZmJfcmVhZHkobnVtYmVyKQogICAgICAgIHZuYyA9IHNodXRpbC53aGljaCgiWHZuYyIpIG9yIHNodXRpbC53aGljaCgiWHRpZ2Vy +dm5jIikKICAgICAgICB3ZWJzb2NraWZ5ID0gc2h1dGlsLndoaWNoKCJ3ZWJzb2NraWZ5IikKICAgICAgICBpZiBub3Qgd2Vic29ja2lmeToKICAgICAgICAg +ICAgcmFpc2UgTm9WTkNVbmF2YWlsYWJsZSgiZGlzcGxheSBydW50aW1lIHVuYXZhaWxhYmxlIikKICAgICAgICBpZiBub3QgcmZiX2lzX3JlYWR5IGFuZCBu +b3Qgdm5jOgogICAgICAgICAgICByYWlzZSBSdW50aW1lRXJyb3IoImRpc3BsYXkgcnVudGltZSB1bmF2YWlsYWJsZSIpCiAgICAgICAgaWYgcmZiX2lzX3Jl +YWR5IGFuZCBudW1iZXIgbm90IGluIHNlbGYubmFtZWRfcHJvY2Vzc2VzOgogICAgICAgICAgICBzZWxmLnJlY292ZXJfcHJvY2Vzc2VzKG51bWJlciwgcnVu +dGltZSwgZW52aXJvbm1lbnQpCiAgICAgICAgaWYgbm90IHJmYl9pc19yZWFkeToKICAgICAgICAgICAgc2VsZi50ZXJtaW5hdGUobnVtYmVyKQogICAgICAg +ICAgICByZmJfcG9ydCA9IDU5MDAgKyBudW1iZXIKICAgICAgICAgICAgc2VsZi5wcm9jZXNzZXNbbnVtYmVyXSA9IFtdCiAgICAgICAgICAgIHNlbGYubmFt +ZWRfcHJvY2Vzc2VzW251bWJlcl0gPSB7fQogICAgICAgICAgICBzZWxmLnRyYWNrX3Byb2Nlc3MobnVtYmVyLCAieHZuYyIsIHN1YnByb2Nlc3MuUG9wZW4o +CiAgICAgICAgICAgICAgICBbdm5jLCBmIjp7bnVtYmVyfSIsICItZ2VvbWV0cnkiLCBvcy5lbnZpcm9uLmdldCgiQ01VWF9WTkNfR0VPTUVUUlkiLCAiMTQ0 +MHg5MDAiKSwKICAgICAgICAgICAgICAgICAiLWRlcHRoIiwgIjI0IiwgIi1yZmJwb3J0Iiwgc3RyKHJmYl9wb3J0KSwgIi1sb2NhbGhvc3QiLAogICAgICAg +ICAgICAgICAgICItU2VjdXJpdHlUeXBlcyIsICJOb25lIiwgIi1BbHdheXNTaGFyZWQiXSwKICAgICAgICAgICAgICAgIGVudj1lbnZpcm9ubWVudCwgc3Rk +aW49c3VicHJvY2Vzcy5ERVZOVUxMLAogICAgICAgICAgICAgICAgc3Rkb3V0PXN1YnByb2Nlc3MuREVWTlVMTCwgc3RkZXJyPXN1YnByb2Nlc3MuREVWTlVM +TCkpCiAgICAgICAgICAgIGlmIG5vdCBzZWxmLndhaXRfZm9yX3BvcnQocmZiX3BvcnQpOgogICAgICAgICAgICAgICAgcmFpc2UgUnVudGltZUVycm9yKCJY +dm5jIGRpZCBub3QgYmVjb21lIHJlYWR5IikKICAgICAgICAgICAgc2VsZi5zdGFydF9kYnVzKG51bWJlciwgZW52aXJvbm1lbnQsIHJ1bnRpbWUpCiAgICAg +ICAgICAgIHNlbGYuc3RhcnRfc2Vzc2lvbl9jb21wb25lbnRzKG51bWJlciwgZW52aXJvbm1lbnQsIHJ1bnRpbWUpCiAgICAgICAgZWxpZiBudW1iZXIgaW4g +c2VsZi5uYW1lZF9wcm9jZXNzZXM6CiAgICAgICAgICAgIG5hbWVkID0gc2VsZi5uYW1lZF9wcm9jZXNzZXNbbnVtYmVyXQogICAgICAgICAgICBkYnVzID0g +bmFtZWQuZ2V0KCJkYnVzIikKICAgICAgICAgICAgaWYgZGJ1cyBpcyBOb25lIG9yIG5vdCBzZWxmLnByb2Nlc3NfYWxpdmUoZGJ1cyk6CiAgICAgICAgICAg +ICAgICBmb3IgbmFtZSBpbiAoIm9wZW5ib3giLCAidGludDIiLCAidm5jY29uZmlnIik6CiAgICAgICAgICAgICAgICAgICAgc2VsZi5yZW1vdmVfcHJvY2Vz +cyhudW1iZXIsIG5hbWUpCiAgICAgICAgICAgICAgICBzZWxmLnN0YXJ0X2RidXMobnVtYmVyLCBlbnZpcm9ubWVudCwgcnVudGltZSkKICAgICAgICAgICAg +c2VsZi5zdGFydF9zZXNzaW9uX2NvbXBvbmVudHMobnVtYmVyLCBlbnZpcm9ubWVudCwgcnVudGltZSkKICAgICAgICB3ZWJzb2NraWZ5X3Byb2Nlc3MgPSBz +ZWxmLndlYnNvY2tpZnlfcHJvY2Vzc2VzLmdldChudW1iZXIpCiAgICAgICAgaWYgKHdlYnNvY2tpZnlfcHJvY2VzcyBpcyBOb25lIG9yIG5vdCBzZWxmLnBy +b2Nlc3NfYWxpdmUod2Vic29ja2lmeV9wcm9jZXNzKQogICAgICAgICAgICAgICAgb3Igbm90IG5vdm5jX3JlYWR5KG51bWJlcikpOgogICAgICAgICAgICBz +ZWxmLnN0YXJ0X3dlYnNvY2tpZnkobnVtYmVyLCBlbnZpcm9ubWVudCwgd2Vic29ja2lmeSkKICAgICAgICAocnVudGltZSAvICJlbnYiKS53cml0ZV90ZXh0 +KGYiZXhwb3J0IERJU1BMQVk9OntudW1iZXJ9XG4iKQoKICAgIGRlZiBzdXBlcnZpc2Uoc2VsZiwgbnVtYmVyLCBmaW5pc2hlZCk6CiAgICAgICAgcnVudGlt +ZSA9IHNlbGYucnVudGltZSAvIHN0cihudW1iZXIpCiAgICAgICAgcnVudGltZS5ta2Rpcihtb2RlPTBvNzAwLCBwYXJlbnRzPVRydWUsIGV4aXN0X29rPVRy +dWUpCiAgICAgICAgZW52aXJvbm1lbnQgPSBkaWN0KG9zLmVudmlyb24sIERJU1BMQVk9ZiI6e251bWJlcn0iLCBDTVVYX0RFU0tUT1BfUlVOVElNRV9ESVI9 +c3RyKHJ1bnRpbWUpKQogICAgICAgIGVudmlyb25tZW50WyJDSFJPTUVfQ09ORklHX0hPTUUiXSA9IHN0cihydW50aW1lIC8gImNocm9tZS1jb25maWciKQog +ICAgICAgIGVudmlyb25tZW50WyJYREdfQ09ORklHX0hPTUUiXSA9IHN0cihydW50aW1lIC8gInhkZy1jb25maWciKQogICAgICAgIGZvciBrZXkgaW4gKCJO +T1RJRllfU09DS0VUIiwgIkRCVVNfU0VTU0lPTl9CVVNfQUREUkVTUyIsICJEQlVTX1NFU1NJT05fQlVTX1BJRCIsCiAgICAgICAgICAgICAgICAgICAgIkFU +X1NQSV9CVVNfQUREUkVTUyIsICJBVF9TUElfQlVTIik6CiAgICAgICAgICAgIGVudmlyb25tZW50LnBvcChrZXksIE5vbmUpCiAgICAgICAgd2hpbGUgbm90 +IHNlbGYuc2h1dGRvd24uaXNfc2V0KCk6CiAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgICMgVGhlIHN1cGVydmlzb3IgaXMgYSBoZWFsdGggY2hl +Y2ssIG5vdCBhIHRpbWVyLWRyaXZlbiByZXN0YXJ0LgogICAgICAgICAgICAgICAgIyBQcmVzZXJ2ZSBhbiBhY3RpdmUgUkZCL25vVk5DIHNlc3Npb24gdW50 +aWwgaXRzIG93bmVkIGVuZHBvaW50cyBmYWlsLgogICAgICAgICAgICAgICAgaWYgbm90IHNlbGYuc2VydmljZV9yZWFkeShudW1iZXIpOgogICAgICAgICAg +ICAgICAgICAgIHNlbGYuc3RhcnRfY29tcG9uZW50cyhudW1iZXIsIGVudmlyb25tZW50LCBydW50aW1lKQogICAgICAgICAgICAgICAgc3RhdGUgPSAicnVu +bmluZyIgaWYgc2VsZi5zZXJ2aWNlX3JlYWR5KG51bWJlcikgZWxzZSAidW5hdmFpbGFibGUiCiAgICAgICAgICAgIGV4Y2VwdCBOb1ZOQ1VuYXZhaWxhYmxl +OgogICAgICAgICAgICAgICAgc2VsZi50ZXJtaW5hdGVfd2Vic29ja2lmeShudW1iZXIpCiAgICAgICAgICAgICAgICBzdGF0ZSA9ICJ1bmF2YWlsYWJsZSIK +ICAgICAgICAgICAgZXhjZXB0IChPU0Vycm9yLCBSdW50aW1lRXJyb3IsIHN1YnByb2Nlc3MuU3VicHJvY2Vzc0Vycm9yKToKICAgICAgICAgICAgICAgIHNl +bGYudGVybWluYXRlKG51bWJlcikKICAgICAgICAgICAgICAgIHN0YXRlID0gInVuYXZhaWxhYmxlIgogICAgICAgICAgICB3aXRoIHNlbGYubG9jazoKICAg +ICAgICAgICAgICAgIHNlbGYuc3RhdGVzW251bWJlcl0gPSBzdGF0ZQogICAgICAgICAgICBmaW5pc2hlZC5zZXQoKQogICAgICAgICAgICBzZWxmLnNodXRk +b3duLndhaXQoMzApCiAgICAgICAgc2VsZi50ZXJtaW5hdGUobnVtYmVyKQoKICAgIGRlZiBzbmFwc2hvdChzZWxmKToKICAgICAgICBwcmltYXJ5X3N0YXRl +ID0gInJ1bm5pbmciIGlmIHJlYWR5KDEpIGVsc2UgInVuYXZhaWxhYmxlIgogICAgICAgIHdpdGggc2VsZi5sb2NrOgogICAgICAgICAgICByZXR1cm4geyJ2 +ZXJzaW9uIjogMSwgImNhbkNyZWF0ZSI6IGxlbihzZWxmLmpvYnMpIDwgTUFYX0RJU1BMQVlTIC0gMSwKICAgICAgICAgICAgICAgICAgICAiZGlzcGxheXMi +OiBbZGVzY3JpcHRvcigxLCBwcmltYXJ5X3N0YXRlKV0KICAgICAgICAgICAgICAgICAgICArIFtkZXNjcmlwdG9yKG4sIHNlbGYuc3RhdGVzLmdldChuLCAi +bGF1bmNoaW5nIikpIGZvciBuIGluIHNlbGYuY2F0YWxvZy5udW1iZXJzKCldfQoKICAgIGRlZiBoYW5kbGUoc2VsZiwgcmVxdWVzdCk6CiAgICAgICAgaWYg +cmVxdWVzdC5nZXQoImFjdGlvbiIpID09ICJsaXN0IjoKICAgICAgICAgICAgcmV0dXJuIHNlbGYuc25hcHNob3QoKQogICAgICAgIGlmIHJlcXVlc3QuZ2V0 +KCJhY3Rpb24iKSAhPSAiY3JlYXRlIjoKICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJvcigidW5zdXBwb3J0ZWQgZGlzcGxheSBhY3Rpb24iKQogICAgICAg +IG51bWJlciA9IHNlbGYuY2F0YWxvZy5hbGxvY2F0ZShyZXF1ZXN0LmdldCgicmVxdWVzdCIpKQogICAgICAgIHNlbGYuc3RhcnQobnVtYmVyKS53YWl0KDU1 +KQogICAgICAgIHJlc3VsdCA9IHNlbGYuc25hcHNob3QoKQogICAgICAgIHJlc3VsdFsiY3JlYXRlZCJdID0gZiJkaXNwbGF5OntudW1iZXJ9IgogICAgICAg +IGlmIHNlbGYuc3RhdGVzLmdldChudW1iZXIpICE9ICJydW5uaW5nIjoKICAgICAgICAgICAgcmVzdWx0WyJlcnJvciJdID0gImRpc3BsYXlfc3RhcnRfZmFp +bGVkIgogICAgICAgIHJldHVybiByZXN1bHQKCgpkZWYgc2VydmUocnVudGltZSk6CiAgICBydW50aW1lID0gUGF0aChydW50aW1lKQogICAgcnVudGltZS5t +a2Rpcihtb2RlPTBvNzAwLCBwYXJlbnRzPVRydWUsIGV4aXN0X29rPVRydWUpCiAgICB3aXRoIChydW50aW1lIC8gImRpc3BsYXktY29udHJvbC5sb2NrIiku +b3BlbigidyIpIGFzIGxvY2s6CiAgICAgICAgdHJ5OgogICAgICAgICAgICBmY250bC5mbG9jayhsb2NrLCBmY250bC5MT0NLX0VYIHwgZmNudGwuTE9DS19O +QikKICAgICAgICBleGNlcHQgQmxvY2tpbmdJT0Vycm9yOgogICAgICAgICAgICByZXR1cm4KICAgICAgICBpZiBub3QgYWxsKHNodXRpbC53aGljaChuYW1l +KSBmb3IgbmFtZSBpbiAoIndlYnNvY2tpZnkiLCAib3BlbmJveCIsICJkYnVzLWxhdW5jaCIpKToKICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJvcigiZGlz +cGxheSBydW50aW1lIHVuYXZhaWxhYmxlIikKICAgICAgICBpZiBub3QgKHNodXRpbC53aGljaCgiWHZuYyIpIG9yIHNodXRpbC53aGljaCgiWHRpZ2Vydm5j +IikpOgogICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJkaXNwbGF5IHJ1bnRpbWUgdW5hdmFpbGFibGUiKQogICAgICAgIHNlcnZpY2UgPSBEaXNwbGF5 +U2VydmljZShEaXNwbGF5Q2F0YWxvZyhQYXRoLmhvbWUoKSAvICIuY211eCIgLyAiZGlzcGxheXMiKSwgcnVudGltZSAvICJkaXNwbGF5cyIpCiAgICAgICAg +Zm9yIG51bWJlciBpbiBzZXJ2aWNlLmNhdGFsb2cubnVtYmVycygpOgogICAgICAgICAgICBzZXJ2aWNlLnN0YXJ0KG51bWJlcikKICAgICAgICBwYXRoID0g +cnVudGltZSAvICJkaXNwbGF5LWNvbnRyb2wuc29jayIKICAgICAgICBpZiBwYXRoLmV4aXN0cygpOgogICAgICAgICAgICBwYXRoLnVubGluaygpCgogICAg +ICAgIGNsYXNzIEhhbmRsZXIoc29ja2V0c2VydmVyLlN0cmVhbVJlcXVlc3RIYW5kbGVyKToKICAgICAgICAgICAgZGVmIGhhbmRsZShzZWxmKToKICAgICAg +ICAgICAgICAgIHNlbGYuY29ubmVjdGlvbi5zZXR0aW1lb3V0KDYwKQogICAgICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgIHJhdyA9IHNl +bGYucmZpbGUucmVhZGxpbmUoNDA5NykKICAgICAgICAgICAgICAgICAgICBpZiBsZW4ocmF3KSA+IDQwOTY6CiAgICAgICAgICAgICAgICAgICAgICAgIHJh +aXNlIFZhbHVlRXJyb3IoImRpc3BsYXkgcmVxdWVzdCB0b28gbGFyZ2UiKQogICAgICAgICAgICAgICAgICAgIHJlc3VsdCA9IHNlcnZpY2UuaGFuZGxlKGpz +b24ubG9hZHMocmF3KSkKICAgICAgICAgICAgICAgIGV4Y2VwdCAoVmFsdWVFcnJvciwgS2V5RXJyb3IsIFR5cGVFcnJvciwgT1NFcnJvcik6CiAgICAgICAg +ICAgICAgICAgICAgcmVzdWx0ID0geyJlcnJvciI6ICJkaXNwbGF5X3JlcXVlc3RfZmFpbGVkIn0KICAgICAgICAgICAgICAgIHRyeToKICAgICAgICAgICAg +ICAgICAgICBzZWxmLndmaWxlLndyaXRlKGpzb24uZHVtcHMocmVzdWx0KS5lbmNvZGUoKSArIGIiXG4iKQogICAgICAgICAgICAgICAgZXhjZXB0IE9TRXJy +b3I6CiAgICAgICAgICAgICAgICAgICAgcGFzcyAgIyBUaGUgcGVyc2lzdGVkIHJlY2VpcHQgcmVtYWlucyByZXBsYXlhYmxlIGFmdGVyIGRpc2Nvbm5lY3Qu +CgogICAgICAgIGNsYXNzIFNlcnZlcihzb2NrZXRzZXJ2ZXIuVGhyZWFkaW5nVW5peFN0cmVhbVNlcnZlcik6CiAgICAgICAgICAgIGRhZW1vbl90aHJlYWRz +ID0gVHJ1ZQoKICAgICAgICBwcmV2aW91c191bWFzayA9IG9zLnVtYXNrKDBvMDc3KQogICAgICAgIHRyeToKICAgICAgICAgICAgd2l0aCBTZXJ2ZXIoc3Ry +KHBhdGgpLCBIYW5kbGVyKSBhcyBzZXJ2ZXI6CiAgICAgICAgICAgICAgICBvcy51bWFzayhwcmV2aW91c191bWFzaykKICAgICAgICAgICAgICAgIHNlcnZl +ci5zZXJ2ZV9mb3JldmVyKCkKICAgICAgICBmaW5hbGx5OgogICAgICAgICAgICBvcy51bWFzayhwcmV2aW91c191bWFzaykKICAgICAgICAgICAgc2Vydmlj +ZS5zaHV0ZG93bi5zZXQoKQogICAgICAgICAgICBwYXRoLnVubGluayhtaXNzaW5nX29rPVRydWUpCgoKZGVmIG1haW4oKToKICAgIHBhcnNlciA9IGFyZ3Bh +cnNlLkFyZ3VtZW50UGFyc2VyKGRlc2NyaXB0aW9uPV9fZG9jX18pCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50KCJhY3Rpb24iLCBjaG9pY2VzPVsibGlzdCIs +ICJjcmVhdGUiLCAic2VydmUiXSkKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoIi0tcmVxdWVzdC1pZCIpCiAgICBhcmdzID0gcGFyc2VyLnBhcnNlX2FyZ3Mo +KQogICAgcnVudGltZSA9IG9zLmVudmlyb24uZ2V0KCJDTVVYX0RFU0tUT1BfUlVOVElNRV9ESVIiLCAiL3J1bi9jbXV4LWRlc2t0b3AiKQogICAgaWYgYXJn +cy5hY3Rpb24gPT0gInNlcnZlIjoKICAgICAgICBzZXJ2ZShydW50aW1lKQogICAgICAgIHJldHVybiAwCiAgICBpZiBhcmdzLmFjdGlvbiA9PSAiY3JlYXRl +IiBhbmQgbm90IGFyZ3MucmVxdWVzdF9pZDoKICAgICAgICBwYXJzZXIuZXJyb3IoImNyZWF0ZSByZXF1aXJlcyAtLXJlcXVlc3QtaWQgKHJldGFpbiBpdCB3 +aGVuIHJldHJ5aW5nKSIpCiAgICB0cnk6CiAgICAgICAgd2l0aCBzb2NrZXQuc29ja2V0KHNvY2tldC5BRl9VTklYLCBzb2NrZXQuU09DS19TVFJFQU0pIGFz +IGNsaWVudDoKICAgICAgICAgICAgY2xpZW50LnNldHRpbWVvdXQoNjApCiAgICAgICAgICAgIGNsaWVudC5jb25uZWN0KHN0cihQYXRoKHJ1bnRpbWUpIC8g +ImRpc3BsYXktY29udHJvbC5zb2NrIikpCiAgICAgICAgICAgIGNsaWVudC5zZW5kYWxsKGpzb24uZHVtcHMoeyJhY3Rpb24iOiBhcmdzLmFjdGlvbiwgInJl +cXVlc3QiOiBhcmdzLnJlcXVlc3RfaWR9KS5lbmNvZGUoKSArIGIiXG4iKQogICAgICAgICAgICB3aXRoIGNsaWVudC5tYWtlZmlsZSgicmIiKSBhcyBzdHJl +YW06CiAgICAgICAgICAgICAgICByZXN1bHQgPSBqc29uLmxvYWRzKHN0cmVhbS5yZWFkbGluZSg2NTUzNikpCiAgICBleGNlcHQgKE9TRXJyb3IsIFZhbHVl +RXJyb3IpOgogICAgICAgIHJlc3VsdCA9IHsiZXJyb3IiOiAiZGlzcGxheV9ydW50aW1lX3VuYXZhaWxhYmxlIn0KICAgIHByaW50KGpzb24uZHVtcHMocmVz +dWx0KSkKICAgIHJldHVybiAxIGlmICJlcnJvciIgaW4gcmVzdWx0IGVsc2UgMAoKCmlmIF9fbmFtZV9fID09ICJfX21haW5fXyI6CiAgICByYWlzZSBTeXN0 +ZW1FeGl0KG1haW4oKSkK """ private static let path = "$HOME/.cmux/cmux-display" diff --git a/Sources/Cloud/CloudTreeNodeActions.swift b/Sources/Cloud/CloudTreeNodeActions.swift index 2b66890f134a..5e61a691f287 100644 --- a/Sources/Cloud/CloudTreeNodeActions.swift +++ b/Sources/Cloud/CloudTreeNodeActions.swift @@ -415,7 +415,13 @@ struct CloudTreeNodeActions { actions.newDisplay = { machine in let target = Result { try destination(.split) } run(String(format: String(localized: "cloud.display.creating", defaultValue: "Creating a display on %@…"), machineName(machine))) { catalog in - try await catalog.createDisplay(on: machine, into: target.get()) + do { + try await catalog.createDisplay(on: machine, into: target.get()) + } catch is CancellationError { + throw CancellationError() + } catch { + throw SurfaceCatalogError.unsupported(String(localized: "cloud.display.creationFailed", defaultValue: "The new display could not start. Refresh Displays, then retry. Existing displays are unchanged.")) + } } } let navigationRun: CloudTreeTerminalNavigationCoordinator.Run = { label, operation in diff --git a/tests/fixtures/cmux-display b/tests/fixtures/cmux-display index 86cfff96d804..54746769dcd5 100755 --- a/tests/fixtures/cmux-display +++ b/tests/fixtures/cmux-display @@ -214,12 +214,11 @@ class DisplayService: @staticmethod def wait_for_port(port, timeout=20): - deadline = time.monotonic() + timeout - while time.monotonic() < deadline: - if listening(port): + try: + with socket.create_connection(("127.0.0.1", port), timeout=timeout): return True - time.sleep(0.1) - return False + except OSError: + return False @staticmethod def process_alive(process): From 60c550c98310d5dafbd3fe61f980241c6b22dc76 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 18:58:00 -0700 Subject: [PATCH 52/64] fix: restore Cloud resources in Dock scopes --- Sources/Panels/BrowserPanel+CloudConnection.swift | 7 ++++++- Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift | 5 ++++- Sources/Surfaces/SurfaceCatalog+CloudPorts.swift | 3 +++ Sources/Surfaces/SurfaceCatalog.swift | 6 ++++-- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/Sources/Panels/BrowserPanel+CloudConnection.swift b/Sources/Panels/BrowserPanel+CloudConnection.swift index 1f7153281be9..96bf591abc37 100644 --- a/Sources/Panels/BrowserPanel+CloudConnection.swift +++ b/Sources/Panels/BrowserPanel+CloudConnection.swift @@ -39,7 +39,12 @@ extension BrowserPanel { func restoreCloudResource(_ resource: SurfaceResourceID, preferredURL: URL? = nil) { pendingCloudRestoreURL = preferredURL let catalog = SurfaceCatalog.shared - do { try catalog.validateOwnership(of: [resource], at: .workspace(id: workspaceId, placement: .tab)) } + let isGlobalDock = DockSplitStore.liveStore(containingPanel: id)?.scope == .global + do { + if !isGlobalDock { + try catalog.validateOwnership(of: [resource], at: .workspace(id: workspaceId, placement: .tab)) + } + } catch { cloudAccess.showUnavailable(SurfaceTransferRejection.cloudMachineMismatch.message); return } cloudAccess.retainResource(resource) retainTransferredSurfaceMachine(resource.machine) diff --git a/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift b/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift index b88c42e9d4ad..f8df73549be9 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift @@ -82,9 +82,12 @@ extension CmuxTuiSurfaceProvider { return false } let resourceID = explicitResource ?? fallbackID + let isGlobalDock = DockSplitStore.liveStore(containingPanel: browser.id)?.scope == .global + let destinationOwned = isGlobalDock + || (try? catalog.validateOwnership(of: [resourceID], at: .workspace(id: browser.workspaceId, placement: .tab))) != nil guard resourceID.machine == machine, browser.cloudAccess.resourceID?.machine == nil || browser.cloudAccess.resourceID?.machine == machine, - (try? catalog.validateOwnership(of: [resourceID], at: .workspace(id: browser.workspaceId, placement: .tab))) != nil else { + destinationOwned else { browser.cloudAccess.showUnavailable(SurfaceTransferRejection.cloudMachineMismatch.message) return false } diff --git a/Sources/Surfaces/SurfaceCatalog+CloudPorts.swift b/Sources/Surfaces/SurfaceCatalog+CloudPorts.swift index d71683b7cf88..045b612f4fba 100644 --- a/Sources/Surfaces/SurfaceCatalog+CloudPorts.swift +++ b/Sources/Surfaces/SurfaceCatalog+CloudPorts.swift @@ -246,10 +246,13 @@ extension SurfaceCatalog { let refreshedIDs = Set(refreshed.map(\.id)) let previousIDs = Set(previous.map(\.id)) let projectedResourceIDs = Set(projections.map(\.resource)) + let displayPortsOwned = machineInfo(for: machine)?.hasDesktop == true + || previous.contains { $0.kind == .display } var result = refreshed for candidate in snapshot.resources(on: machine) where candidate.id.isForwardedPort && !CmuxTuiSnapshotParser.internalPorts.contains(candidate.id.forwardedPort ?? -1) + && (!displayPortsOwned || !CmuxTuiSnapshotParser.displayPorts.contains(candidate.id.forwardedPort ?? -1)) && !refreshedIDs.contains(candidate.id) { let wasAddedDuringRefresh = !previousIDs.contains(candidate.id) let remainsProjected = projectedResourceIDs.contains(candidate.id) diff --git a/Sources/Surfaces/SurfaceCatalog.swift b/Sources/Surfaces/SurfaceCatalog.swift index 625b8e27b27b..da22174d5e19 100644 --- a/Sources/Surfaces/SurfaceCatalog.swift +++ b/Sources/Surfaces/SurfaceCatalog.swift @@ -1329,8 +1329,10 @@ final class SurfaceCatalog { /// after the link reconnects); local resources are re-registered by the local provider /// with the same panel-derived key, so they resolve immediately. func restore(_ records: [SurfaceProjectionRecord], workspaceID: UUID) { - do { try validateOwnership(of: records.map(\.resource), at: .workspace(id: workspaceID, placement: .tab)) } - catch { return } + for record in records where DockSplitStore.liveStore(containingPanel: record.panelID)?.scope != .global { + do { try validateOwnership(of: [record.resource], at: .workspace(id: workspaceID, placement: .tab)) } + catch { return } + } for record in records { if resources[record.resource] != nil { pendingRestoredProjections.remove(panelID: record.panelID) From 3df7c564887455fd6602a903920baae5394b5cd5 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 19:26:58 -0700 Subject: [PATCH 53/64] fix: recover global Dock projections and daemon readiness --- Sources/Cloud/CloudDisplayCoordinator.swift | 17 ++++++++++++++++- Sources/Surfaces/SurfaceCatalog+Ownership.swift | 6 ++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Sources/Cloud/CloudDisplayCoordinator.swift b/Sources/Cloud/CloudDisplayCoordinator.swift index 857dbeeebb5a..ec7d40a82af1 100644 --- a/Sources/Cloud/CloudDisplayCoordinator.swift +++ b/Sources/Cloud/CloudDisplayCoordinator.swift @@ -29,7 +29,22 @@ final class CloudDisplayCoordinator { let token = generation let task = Task { [weak self, execute] in do { - let response = try await execute(CloudGuestDisplayScript.command(action: "list"), 10_000) + var response: VMExecResult? + var lastError: (any Error)? + for attempt in 0..<20 { + do { + let candidate = try await execute(CloudGuestDisplayScript.command(action: "list"), 10_000) + if candidate.exitCode == 0 { + response = candidate + break + } + lastError = SurfaceCatalogError.unsupported(CloudGuestDisplaySnapshot.unavailableMessage) + } catch { + lastError = error + } + if attempt < 19 { try await Task.sleep(for: .milliseconds(100)) } + } + guard let response else { throw lastError ?? SurfaceCatalogError.unsupported(CloudGuestDisplaySnapshot.unavailableMessage) } guard response.exitCode == 0 else { throw SurfaceCatalogError.unsupported(CloudGuestDisplaySnapshot.unavailableMessage) } diff --git a/Sources/Surfaces/SurfaceCatalog+Ownership.swift b/Sources/Surfaces/SurfaceCatalog+Ownership.swift index c65ebd8f6347..971f9c7cea83 100644 --- a/Sources/Surfaces/SurfaceCatalog+Ownership.swift +++ b/Sources/Surfaces/SurfaceCatalog+Ownership.swift @@ -51,6 +51,9 @@ extension SurfaceCatalog { /// receipt can enter the catalog, and retire only the just-created view. func validateMaterializationOwnership(_ projection: SurfaceProjection, provider: any SurfaceProvider) throws { do { + if DockSplitStore.liveStore(containingPanel: projection.panelID)?.scope == .global { + return + } try validateOwnership(of: [projection.resource], at: .workspace(id: projection.workspaceID, placement: .tab)) } catch { provider.discardMaterialization(projection) @@ -59,6 +62,9 @@ extension SurfaceCatalog { } func canRestoreProjection(_ projection: SurfaceProjection) -> Bool { + if DockSplitStore.liveStore(containingPanel: projection.panelID)?.scope == .global { + return true + } do { try validateOwnership(of: [projection.resource], at: .workspace(id: projection.workspaceID, placement: .tab)) return true From 4d657c181f71cd9a30658e7ee82d05989049f34d Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 19:46:36 -0700 Subject: [PATCH 54/64] fix: preserve Dock connections across Cloud route changes --- .../PortForward/CloudBrowserAccessState.swift | 17 +++++++++++++++++ Sources/DockSplitStore+BrowserActions.swift | 9 ++++++++- .../Panels/BrowserPanel+CloudConnection.swift | 2 +- .../CmuxTuiSurfaceProvider+PortForward.swift | 15 ++++++++++----- 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/Sources/Cloud/PortForward/CloudBrowserAccessState.swift b/Sources/Cloud/PortForward/CloudBrowserAccessState.swift index ebde49e9fd6b..ce8231ca4d70 100644 --- a/Sources/Cloud/PortForward/CloudBrowserAccessState.swift +++ b/Sources/Cloud/PortForward/CloudBrowserAccessState.swift @@ -42,6 +42,23 @@ final class CloudBrowserAccessState { observeRoute() } + /// Rebinds ownership to a committed same-VM service without restarting the + /// current WebKit navigation (for example, a POST redirect to another port). + func adoptCommittedRoute(model: CloudPortAccessModel, url: URL, resourceID: SurfaceResourceID) { + observationGeneration &+= 1 + unavailable = nil + self.resourceID = resourceID + self.model = model + remoteURL = url + navigationURL = nil + error = nil + desktopFailure = nil + dismissedFailure = nil + activeNavigationID = nil + connectionDeadline.cancel() + trace("route_adopted") + } + func routeDidConfigure() { observeRoute() } func retainResource(_ resource: SurfaceResourceID) { resourceID = resource } diff --git a/Sources/DockSplitStore+BrowserActions.swift b/Sources/DockSplitStore+BrowserActions.swift index 7078734aee9b..c6b36dad073a 100644 --- a/Sources/DockSplitStore+BrowserActions.swift +++ b/Sources/DockSplitStore+BrowserActions.swift @@ -48,7 +48,14 @@ extension DockSplitStore { duplicatedPanel.retainTransferredSurfaceMachine(resource.machine) catalog.restore([SurfaceProjectionRecord(panelID: duplicatedPanel.id, resource: resource, remoteWorkspaceID: record?.remoteWorkspaceID)], workspaceID: workspaceId) - duplicatedPanel.restoreCloudResource(resource, preferredURL: browser.currentURLForTabDuplication) + if let model = browser.cloudAccess.model, let url = browser.cloudAccess.remoteURL { + duplicatedPanel.prepareCloudBrowserStore(machineID: resource.machine.rawValue) + duplicatedPanel.cloudAccess.configure(model: model, url: url, resourceID: resource) + duplicatedPanel.showCloudAddress(url) + model.connect() + } else { + duplicatedPanel.restoreCloudResource(resource, preferredURL: browser.currentURLForTabDuplication) + } } if focus { noteKeyboardFocusIntent(window: focusWindow) diff --git a/Sources/Panels/BrowserPanel+CloudConnection.swift b/Sources/Panels/BrowserPanel+CloudConnection.swift index 96bf591abc37..cdb8f35014a4 100644 --- a/Sources/Panels/BrowserPanel+CloudConnection.swift +++ b/Sources/Panels/BrowserPanel+CloudConnection.swift @@ -97,7 +97,7 @@ extension BrowserPanel { }).flatMap({ SurfaceCatalog.shared.provider(for: $0.id) as? CmuxTuiSurfaceProvider }) else { return false } - return provider.configureBrowser(self, url: url) + return provider.configureBrowser(self, url: url, preserveCurrentNavigation: true) } /// Cloud panes use their own persistent data store so configuring one VM cannot reroute another. diff --git a/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift b/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift index f8df73549be9..577e701d4bec 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift @@ -46,7 +46,8 @@ extension CmuxTuiSurfaceProvider { /// Bind the page to its machine proxy without activating a system VPN. @discardableResult - func configureBrowser(_ browser: BrowserPanel, url: URL, resourceID: SurfaceResourceID? = nil) -> Bool { + func configureBrowser(_ browser: BrowserPanel, url: URL, resourceID: SurfaceResourceID? = nil, + preserveCurrentNavigation: Bool = false) -> Bool { let requestedPort = url.port ?? (url.scheme?.lowercased() == "https" ? 443 : 80) let fallbackID: SurfaceResourceID = if info.hasDesktop, (CmuxTuiSnapshotParser.desktopPort...6916).contains(requestedPort) { SurfaceResourceID(machine: machine, kind: .display, key: "display:\(requestedPort - 6900)") @@ -107,14 +108,18 @@ extension CmuxTuiSurfaceProvider { catalog.restore([SurfaceProjectionRecord(panelID: browser.id, resource: resourceID)], workspaceID: browser.workspaceId) } let port = privateURL.port ?? (privateURL.scheme?.lowercased() == "https" ? 443 : 80) - browser.webView.stopLoading() let model = accessModel(port: port, address: address, scheme: privateURL.scheme ?? "http") browser.retainTransferredSurfaceMachine(machine) - browser.cloudAccess.configure(model: model, url: privateURL, resourceID: resourceID) + if preserveCurrentNavigation { + browser.cloudAccess.adoptCommittedRoute(model: model, url: privateURL, resourceID: resourceID) + } else { + browser.webView.stopLoading() + browser.cloudAccess.configure(model: model, url: privateURL, resourceID: resourceID) + } browser.prepareCloudBrowserStore(machineID: machineID) - browser.showCloudAddress(privateURL) + if !preserveCurrentNavigation { browser.showCloudAddress(privateURL) } model.connect() - browser.cloudAccess.routeDidConfigure() + if !preserveCurrentNavigation { browser.cloudAccess.routeDidConfigure() } materializedPanels.insert(browser.id) return true } From 812b4ef07da1d3f782e902f5ac57c4efcf169e94 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 20:18:09 -0700 Subject: [PATCH 55/64] fix: bound display startup and preserve duplicate URLs --- Sources/Cloud/CloudDisplayCoordinator.swift | 4 +- Sources/Cloud/CloudGuestDisplayScript.swift | 317 +++++++++--------- Sources/DockSplitStore+BrowserActions.swift | 5 +- .../Workspace+BrowserDuplication.swift | 5 +- tests/fixtures/cmux-display | 13 +- 5 files changed, 175 insertions(+), 169 deletions(-) diff --git a/Sources/Cloud/CloudDisplayCoordinator.swift b/Sources/Cloud/CloudDisplayCoordinator.swift index ec7d40a82af1..7e54d206af96 100644 --- a/Sources/Cloud/CloudDisplayCoordinator.swift +++ b/Sources/Cloud/CloudDisplayCoordinator.swift @@ -31,7 +31,7 @@ final class CloudDisplayCoordinator { do { var response: VMExecResult? var lastError: (any Error)? - for attempt in 0..<20 { + for attempt in 0..<3 { do { let candidate = try await execute(CloudGuestDisplayScript.command(action: "list"), 10_000) if candidate.exitCode == 0 { @@ -42,7 +42,7 @@ final class CloudDisplayCoordinator { } catch { lastError = error } - if attempt < 19 { try await Task.sleep(for: .milliseconds(100)) } + if attempt < 2 { try await Task.sleep(for: .milliseconds(100)) } } guard let response else { throw lastError ?? SurfaceCatalogError.unsupported(CloudGuestDisplaySnapshot.unavailableMessage) } guard response.exitCode == 0 else { diff --git a/Sources/Cloud/CloudGuestDisplayScript.swift b/Sources/Cloud/CloudGuestDisplayScript.swift index 453e65c782b7..686071bfa3af 100644 --- a/Sources/Cloud/CloudGuestDisplayScript.swift +++ b/Sources/Cloud/CloudGuestDisplayScript.swift @@ -86,164 +86,165 @@ ICAgICAgIGRpcmVjdG9yeS5ta2Rpcihtb2RlPTBvNzAwLCBwYXJlbnRzPVRydWUsIGV4aXN0X29rPVRy IC8gZiJjbXV4LWRpc3BsYXkte251bWJlcn0te3NvdXJjZS5uYW1lfSIKICAgICAgICBpZiBub3QgdGFyZ2V0LmV4aXN0cygpIG9yIHRhcmdldC5zdGF0KCku c3RfbXRpbWVfbnMgPCBzb3VyY2Uuc3RhdCgpLnN0X210aW1lX25zOgogICAgICAgICAgICBzaHV0aWwuY29weTIoc291cmNlLCB0YXJnZXQpCiAgICAgICAg ICAgIHRhcmdldC5jaG1vZCgwbzcwMCkKICAgICAgICByZXR1cm4gc3RyKHRhcmdldCkKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgd2FpdF9mb3JfcG9y -dChwb3J0LCB0aW1lb3V0PTIwKToKICAgICAgICB0cnk6CiAgICAgICAgICAgIHdpdGggc29ja2V0LmNyZWF0ZV9jb25uZWN0aW9uKCgiMTI3LjAuMC4xIiwg -cG9ydCksIHRpbWVvdXQ9dGltZW91dCk6CiAgICAgICAgICAgICAgICByZXR1cm4gVHJ1ZQogICAgICAgIGV4Y2VwdCBPU0Vycm9yOgogICAgICAgICAgICBy -ZXR1cm4gRmFsc2UKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgcHJvY2Vzc19hbGl2ZShwcm9jZXNzKToKICAgICAgICBwb2xsID0gZ2V0YXR0cihwcm9j -ZXNzLCAicG9sbCIsIE5vbmUpCiAgICAgICAgcmV0dXJuIHBvbGwgaXMgTm9uZSBvciBwb2xsKCkgaXMgTm9uZQoKICAgIGRlZiBjb21wb25lbnRzX2FsaXZl -KHNlbGYsIG51bWJlcik6CiAgICAgICAgbmFtZWQgPSBzZWxmLm5hbWVkX3Byb2Nlc3Nlcy5nZXQobnVtYmVyKQogICAgICAgIGlmIG5hbWVkIGlzIG5vdCBO -b25lOgogICAgICAgICAgICByZXR1cm4gYm9vbChuYW1lZCkgYW5kIGFsbChzZWxmLnByb2Nlc3NfYWxpdmUocHJvY2VzcykgZm9yIHByb2Nlc3MgaW4gbmFt -ZWQudmFsdWVzKCkpIFwKICAgICAgICAgICAgICAgIGFuZCAoInh2bmMiIGluIG5hbWVkIG9yIHJmYl9yZWFkeShudW1iZXIpKQogICAgICAgIHByb2Nlc3Nl -cyA9IHNlbGYucHJvY2Vzc2VzLmdldChudW1iZXIsIFtdKQogICAgICAgIHJldHVybiBib29sKHByb2Nlc3NlcykgYW5kIGFsbChzZWxmLnByb2Nlc3NfYWxp -dmUocHJvY2VzcykgZm9yIHByb2Nlc3MgaW4gcHJvY2Vzc2VzKQoKICAgIGRlZiBzZXJ2aWNlX3JlYWR5KHNlbGYsIG51bWJlcik6CiAgICAgICAgaWYgbm90 -IHJlYWR5KG51bWJlcikgb3Igbm90IHNlbGYuY29tcG9uZW50c19hbGl2ZShudW1iZXIpOgogICAgICAgICAgICByZXR1cm4gRmFsc2UKICAgICAgICB3ZWJz -b2NraWZ5ID0gc2VsZi53ZWJzb2NraWZ5X3Byb2Nlc3Nlcy5nZXQobnVtYmVyKQogICAgICAgIHJldHVybiB3ZWJzb2NraWZ5IGlzIE5vbmUgb3Igc2VsZi5w -cm9jZXNzX2FsaXZlKHdlYnNvY2tpZnkpCgogICAgZGVmIHRyYWNrX3Byb2Nlc3Moc2VsZiwgbnVtYmVyLCBuYW1lLCBwcm9jZXNzKToKICAgICAgICBuYW1l -ZCA9IHNlbGYubmFtZWRfcHJvY2Vzc2VzLnNldGRlZmF1bHQobnVtYmVyLCB7fSkKICAgICAgICBwcmV2aW91cyA9IG5hbWVkLmdldChuYW1lKQogICAgICAg -IGlmIHByZXZpb3VzIGlzIG5vdCBOb25lOgogICAgICAgICAgICBzZWxmLnByb2Nlc3Nlc1tudW1iZXJdID0gWwogICAgICAgICAgICAgICAgaXRlbSBmb3Ig -aXRlbSBpbiBzZWxmLnByb2Nlc3Nlcy5nZXQobnVtYmVyLCBbXSkgaWYgaXRlbSBpcyBub3QgcHJldmlvdXMKICAgICAgICAgICAgXQogICAgICAgIHNlbGYu -cHJvY2Vzc2VzLnNldGRlZmF1bHQobnVtYmVyLCBbXSkuYXBwZW5kKHByb2Nlc3MpCiAgICAgICAgbmFtZWRbbmFtZV0gPSBwcm9jZXNzCgogICAgZGVmIHJl -bW92ZV9wcm9jZXNzKHNlbGYsIG51bWJlciwgbmFtZSk6CiAgICAgICAgcHJvY2VzcyA9IHNlbGYubmFtZWRfcHJvY2Vzc2VzLnNldGRlZmF1bHQobnVtYmVy -LCB7fSkucG9wKG5hbWUsIE5vbmUpCiAgICAgICAgaWYgcHJvY2VzcyBpcyBOb25lOgogICAgICAgICAgICByZXR1cm4KICAgICAgICBzZWxmLnByb2Nlc3Nl -c1tudW1iZXJdID0gW2l0ZW0gZm9yIGl0ZW0gaW4gc2VsZi5wcm9jZXNzZXMuZ2V0KG51bWJlciwgW10pIGlmIGl0ZW0gaXMgbm90IHByb2Nlc3NdCiAgICAg -ICAgdHJ5OgogICAgICAgICAgICBwcm9jZXNzLnRlcm1pbmF0ZSgpCiAgICAgICAgZXhjZXB0IE9TRXJyb3I6CiAgICAgICAgICAgIHBhc3MKCiAgICBkZWYg -cmVjb3Zlcl9wcm9jZXNzZXMoc2VsZiwgbnVtYmVyLCBydW50aW1lLCBlbnZpcm9ubWVudCk6CiAgICAgICAgc2VsZi5wcm9jZXNzZXNbbnVtYmVyXSA9IFtd -CiAgICAgICAgc2VsZi5uYW1lZF9wcm9jZXNzZXNbbnVtYmVyXSA9IHt9CiAgICAgICAgc2Vzc2lvbiA9IFBhdGgocnVudGltZSkgLyAic2Vzc2lvbi5qc29u -IgogICAgICAgIHRyeToKICAgICAgICAgICAgc2F2ZWQgPSBqc29uLmxvYWRzKHNlc3Npb24ucmVhZF90ZXh0KCkpCiAgICAgICAgICAgIGRidXNfcGlkID0g -aW50KHNhdmVkLmdldCgicGlkIiwgMCkpCiAgICAgICAgICAgIGRidXMgPSBPd25lZFByb2Nlc3MoZGJ1c19waWQpCiAgICAgICAgICAgIGlmIGRidXNfcGlk -ID4gMSBhbmQgc2VsZi5wcm9jZXNzX2FsaXZlKGRidXMpOgogICAgICAgICAgICAgICAgaWYgc2F2ZWQuZ2V0KCJhZGRyZXNzIik6CiAgICAgICAgICAgICAg -ICAgICAgZW52aXJvbm1lbnRbIkRCVVNfU0VTU0lPTl9CVVNfQUREUkVTUyJdID0gc2F2ZWRbImFkZHJlc3MiXQogICAgICAgICAgICAgICAgZW52aXJvbm1l -bnRbIkRCVVNfU0VTU0lPTl9CVVNfUElEIl0gPSBzdHIoZGJ1c19waWQpCiAgICAgICAgICAgICAgICBzZWxmLnRyYWNrX3Byb2Nlc3MobnVtYmVyLCAiZGJ1 -cyIsIGRidXMpCiAgICAgICAgZXhjZXB0IChPU0Vycm9yLCBWYWx1ZUVycm9yLCBUeXBlRXJyb3IsIGpzb24uSlNPTkRlY29kZUVycm9yKToKICAgICAgICAg -ICAgcGFzcwogICAgICAgIHVpZCA9IHN0cihvcy5nZXR1aWQoKSkKICAgICAgICBmb3IgbmFtZSBpbiAoIm9wZW5ib3giLCAidGludDIiLCAidm5jY29uZmln -Iik6CiAgICAgICAgICAgIG1hcmtlciA9IHN0cihQYXRoKHJ1bnRpbWUpIC8gImJpbiIgLyBmImNtdXgtZGlzcGxheS17bnVtYmVyfS17bmFtZX0iKQogICAg -ICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICBwYXR0ZXJuID0gcmYiXntyZS5lc2NhcGUobWFya2VyKX0oPzpbWzpzcGFjZTpdXXwkKSIKICAgICAgICAg -ICAgICAgIG91dHB1dCA9IHN1YnByb2Nlc3MuY2hlY2tfb3V0cHV0KFsicGdyZXAiLCAiLXUiLCB1aWQsICItZiIsIHBhdHRlcm5dLCB0ZXh0PVRydWUpCiAg -ICAgICAgICAgICAgICBwaWQgPSBpbnQob3V0cHV0LnNwbGl0bGluZXMoKVswXSkKICAgICAgICAgICAgZXhjZXB0IChPU0Vycm9yLCBWYWx1ZUVycm9yLCBJ -bmRleEVycm9yLCBzdWJwcm9jZXNzLkNhbGxlZFByb2Nlc3NFcnJvcik6CiAgICAgICAgICAgICAgICBjb250aW51ZQogICAgICAgICAgICBzZWxmLnRyYWNr -X3Byb2Nlc3MobnVtYmVyLCBuYW1lLCBPd25lZFByb2Nlc3MocGlkKSkKICAgICAgICB0cnk6CiAgICAgICAgICAgIG91dHB1dCA9IHN1YnByb2Nlc3MuY2hl -Y2tfb3V0cHV0KAogICAgICAgICAgICAgICAgWyJwZ3JlcCIsICItdSIsIHVpZCwgIi1mIiwKICAgICAgICAgICAgICAgICByZiJeKD86LipcLyk/KD86WHZu -Y3xYdGlnZXJ2bmMpLio6e251bWJlcn0uKi1yZmJwb3J0XHMrezU5MDAgKyBudW1iZXJ9KD86XHN8JCkiXSwKICAgICAgICAgICAgICAgIHRleHQ9VHJ1ZQog -ICAgICAgICAgICApCiAgICAgICAgICAgIHBpZCA9IGludChvdXRwdXQuc3BsaXRsaW5lcygpWzBdKQogICAgICAgIGV4Y2VwdCAoT1NFcnJvciwgVmFsdWVF -cnJvciwgSW5kZXhFcnJvciwgc3VicHJvY2Vzcy5DYWxsZWRQcm9jZXNzRXJyb3IpOgogICAgICAgICAgICBwaWQgPSAwCiAgICAgICAgaWYgcGlkID4gMToK -ICAgICAgICAgICAgc2VsZi50cmFja19wcm9jZXNzKG51bWJlciwgInh2bmMiLCBPd25lZFByb2Nlc3MocGlkKSkKCiAgICBkZWYgc3RhcnRfZGJ1cyhzZWxm -LCBudW1iZXIsIGVudmlyb25tZW50LCBydW50aW1lKToKICAgICAgICBidXMgPSBzdWJwcm9jZXNzLnJ1bihbImRidXMtbGF1bmNoIiwgIi0tc2gtc3ludGF4 -Il0sIGVudj1lbnZpcm9ubWVudCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdGRpbj1zdWJwcm9jZXNzLkRFVk5VTEwsIHN0ZG91dD1zdWJwcm9j -ZXNzLlBJUEUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3RkZXJyPXN1YnByb2Nlc3MuREVWTlVMTCwgdGV4dD1UcnVlLCB0aW1lb3V0PTEwLAog -ICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNoZWNrPVRydWUpLnN0ZG91dAogICAgICAgIGZvciBsaW5lIGluIGJ1cy5zcGxpdGxpbmVzKCk6CiAgICAg -ICAgICAgIGlmICI9IiBub3QgaW4gbGluZToKICAgICAgICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgICAgIGtleSwgdmFsdWUgPSBsaW5lLnNwbGl0KCI9 -IiwgMSkKICAgICAgICAgICAgZW52aXJvbm1lbnRba2V5XSA9IHZhbHVlLnN0cmlwKCkucnN0cmlwKCI7Iikuc3RyaXAoKS5zdHJpcCgiJyIpCiAgICAgICAg -dHJ5OgogICAgICAgICAgICBkYnVzX3BpZCA9IGludChlbnZpcm9ubWVudC5nZXQoIkRCVVNfU0VTU0lPTl9CVVNfUElEIiwgIjAiKSkKICAgICAgICBleGNl -cHQgVmFsdWVFcnJvcjoKICAgICAgICAgICAgZGJ1c19waWQgPSAwCiAgICAgICAgaWYgZGJ1c19waWQgPiAxOgogICAgICAgICAgICBzZWxmLnRyYWNrX3By -b2Nlc3MobnVtYmVyLCAiZGJ1cyIsIE93bmVkUHJvY2VzcyhkYnVzX3BpZCkpCiAgICAgICAgICAgIChQYXRoKHJ1bnRpbWUpIC8gInNlc3Npb24uanNvbiIp -LndyaXRlX3RleHQoanNvbi5kdW1wcyh7CiAgICAgICAgICAgICAgICAicGlkIjogZGJ1c19waWQsCiAgICAgICAgICAgICAgICAiYWRkcmVzcyI6IGVudmly -b25tZW50LmdldCgiREJVU19TRVNTSU9OX0JVU19BRERSRVNTIiwgIiIpCiAgICAgICAgICAgIH0pKQoKICAgIGRlZiBzdGFydF9zZXNzaW9uX2NvbXBvbmVu -dHMoc2VsZiwgbnVtYmVyLCBlbnZpcm9ubWVudCwgcnVudGltZSk6CiAgICAgICAgZm9yIGV4ZWN1dGFibGUsIGFyZ3MgaW4gWwogICAgICAgICAgICAoIm9w -ZW5ib3giLCBbXSksICgidGludDIiLCBbIi1jIiwgIi9ldGMvY211eC90aW50MnJjIl0pLAogICAgICAgICAgICAoInZuY2NvbmZpZyIsIFsiLW5vd2luIl0p -LAogICAgICAgIF06CiAgICAgICAgICAgIGV4aXN0aW5nID0gc2VsZi5uYW1lZF9wcm9jZXNzZXMuZ2V0KG51bWJlciwge30pLmdldChleGVjdXRhYmxlKQog -ICAgICAgICAgICBpZiBleGlzdGluZyBpcyBub3QgTm9uZSBhbmQgc2VsZi5wcm9jZXNzX2FsaXZlKGV4aXN0aW5nKToKICAgICAgICAgICAgICAgIGNvbnRp -bnVlCiAgICAgICAgICAgIHBhdGggPSBleGVjdXRhYmxlIGlmICIvIiBpbiBleGVjdXRhYmxlIGVsc2Ugc2h1dGlsLndoaWNoKGV4ZWN1dGFibGUpCiAgICAg -ICAgICAgIGlmIG5vdCBwYXRoOgogICAgICAgICAgICAgICAgaWYgZXhlY3V0YWJsZSA9PSAidm5jY29uZmlnIjoKICAgICAgICAgICAgICAgICAgICBjb250 -aW51ZQogICAgICAgICAgICAgICAgcmFpc2UgUnVudGltZUVycm9yKGYibWlzc2luZyBkaXNwbGF5IGNvbXBvbmVudDoge2V4ZWN1dGFibGV9IikKICAgICAg -ICAgICAgcGF0aCA9IHNlbGYuc2NvcGVkX2NvbXBvbmVudF9wYXRoKGV4ZWN1dGFibGUsIHBhdGgsIG51bWJlciwgcnVudGltZSkKICAgICAgICAgICAgcHJv -Y2VzcyA9IHN1YnByb2Nlc3MuUG9wZW4oW3BhdGhdICsgYXJncywgZW52PWVudmlyb25tZW50LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg -ICAgICBzdGRpbj1zdWJwcm9jZXNzLkRFVk5VTEwsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0ZG91dD1zdWJwcm9jZXNzLkRF -Vk5VTEwsIHN0ZGVycj1zdWJwcm9jZXNzLkRFVk5VTEwpCiAgICAgICAgICAgIHNlbGYudHJhY2tfcHJvY2VzcyhudW1iZXIsIGV4ZWN1dGFibGUsIHByb2Nl -c3MpCgogICAgZGVmIHN0YXJ0X3dlYnNvY2tpZnkoc2VsZiwgbnVtYmVyLCBlbnZpcm9ubWVudCwgZXhlY3V0YWJsZSk6CiAgICAgICAgcGF0aCA9IGV4ZWN1 -dGFibGUgaWYgIi8iIGluIGV4ZWN1dGFibGUgZWxzZSBzaHV0aWwud2hpY2goZXhlY3V0YWJsZSkKICAgICAgICBpZiBub3QgcGF0aDoKICAgICAgICAgICAg -cmFpc2UgTm9WTkNVbmF2YWlsYWJsZSgibWlzc2luZyBkaXNwbGF5IGNvbXBvbmVudDogd2Vic29ja2lmeSIpCiAgICAgICAgc2VsZi50ZXJtaW5hdGVfd2Vi -c29ja2lmeShudW1iZXIpCiAgICAgICAgcmZiX3BvcnQsIG5vdm5jX3BvcnQgPSA1OTAwICsgbnVtYmVyLCA2OTAwICsgbnVtYmVyCiAgICAgICAgcHJvY2Vz -cyA9IHN1YnByb2Nlc3MuUG9wZW4oCiAgICAgICAgICAgIFtwYXRoLCAiLS13ZWIiLCAiL3Vzci9zaGFyZS9ub3ZuYyIsICItLWhlYXJ0YmVhdCIsICIzMCIs -CiAgICAgICAgICAgICBmIjEyNy4wLjAuMTp7bm92bmNfcG9ydH0iLCBmIjEyNy4wLjAuMTp7cmZiX3BvcnR9Il0sCiAgICAgICAgICAgIGVudj1lbnZpcm9u -bWVudCwgc3RkaW49c3VicHJvY2Vzcy5ERVZOVUxMLAogICAgICAgICAgICBzdGRvdXQ9c3VicHJvY2Vzcy5ERVZOVUxMLCBzdGRlcnI9c3VicHJvY2Vzcy5E -RVZOVUxMKQogICAgICAgIHNlbGYud2Vic29ja2lmeV9wcm9jZXNzZXNbbnVtYmVyXSA9IHByb2Nlc3MKICAgICAgICBpZiBub3Qgc2VsZi53YWl0X2Zvcl9w -b3J0KG5vdm5jX3BvcnQsIHRpbWVvdXQ9MTApOgogICAgICAgICAgICBzZWxmLnRlcm1pbmF0ZV93ZWJzb2NraWZ5KG51bWJlcikKICAgICAgICAgICAgcmFp -c2UgTm9WTkNVbmF2YWlsYWJsZSgibm9WTkMgZGlkIG5vdCBiZWNvbWUgcmVhZHkiKQoKICAgIGRlZiBzdGFydF9jb21wb25lbnRzKHNlbGYsIG51bWJlciwg -ZW52aXJvbm1lbnQsIHJ1bnRpbWUpOgogICAgICAgIGlmIHNlbGYuc2VydmljZV9yZWFkeShudW1iZXIpOgogICAgICAgICAgICByZXR1cm4KICAgICAgICBy -ZmJfaXNfcmVhZHkgPSByZmJfcmVhZHkobnVtYmVyKQogICAgICAgIHZuYyA9IHNodXRpbC53aGljaCgiWHZuYyIpIG9yIHNodXRpbC53aGljaCgiWHRpZ2Vy -dm5jIikKICAgICAgICB3ZWJzb2NraWZ5ID0gc2h1dGlsLndoaWNoKCJ3ZWJzb2NraWZ5IikKICAgICAgICBpZiBub3Qgd2Vic29ja2lmeToKICAgICAgICAg -ICAgcmFpc2UgTm9WTkNVbmF2YWlsYWJsZSgiZGlzcGxheSBydW50aW1lIHVuYXZhaWxhYmxlIikKICAgICAgICBpZiBub3QgcmZiX2lzX3JlYWR5IGFuZCBu -b3Qgdm5jOgogICAgICAgICAgICByYWlzZSBSdW50aW1lRXJyb3IoImRpc3BsYXkgcnVudGltZSB1bmF2YWlsYWJsZSIpCiAgICAgICAgaWYgcmZiX2lzX3Jl -YWR5IGFuZCBudW1iZXIgbm90IGluIHNlbGYubmFtZWRfcHJvY2Vzc2VzOgogICAgICAgICAgICBzZWxmLnJlY292ZXJfcHJvY2Vzc2VzKG51bWJlciwgcnVu -dGltZSwgZW52aXJvbm1lbnQpCiAgICAgICAgaWYgbm90IHJmYl9pc19yZWFkeToKICAgICAgICAgICAgc2VsZi50ZXJtaW5hdGUobnVtYmVyKQogICAgICAg -ICAgICByZmJfcG9ydCA9IDU5MDAgKyBudW1iZXIKICAgICAgICAgICAgc2VsZi5wcm9jZXNzZXNbbnVtYmVyXSA9IFtdCiAgICAgICAgICAgIHNlbGYubmFt -ZWRfcHJvY2Vzc2VzW251bWJlcl0gPSB7fQogICAgICAgICAgICBzZWxmLnRyYWNrX3Byb2Nlc3MobnVtYmVyLCAieHZuYyIsIHN1YnByb2Nlc3MuUG9wZW4o -CiAgICAgICAgICAgICAgICBbdm5jLCBmIjp7bnVtYmVyfSIsICItZ2VvbWV0cnkiLCBvcy5lbnZpcm9uLmdldCgiQ01VWF9WTkNfR0VPTUVUUlkiLCAiMTQ0 -MHg5MDAiKSwKICAgICAgICAgICAgICAgICAiLWRlcHRoIiwgIjI0IiwgIi1yZmJwb3J0Iiwgc3RyKHJmYl9wb3J0KSwgIi1sb2NhbGhvc3QiLAogICAgICAg -ICAgICAgICAgICItU2VjdXJpdHlUeXBlcyIsICJOb25lIiwgIi1BbHdheXNTaGFyZWQiXSwKICAgICAgICAgICAgICAgIGVudj1lbnZpcm9ubWVudCwgc3Rk -aW49c3VicHJvY2Vzcy5ERVZOVUxMLAogICAgICAgICAgICAgICAgc3Rkb3V0PXN1YnByb2Nlc3MuREVWTlVMTCwgc3RkZXJyPXN1YnByb2Nlc3MuREVWTlVM -TCkpCiAgICAgICAgICAgIGlmIG5vdCBzZWxmLndhaXRfZm9yX3BvcnQocmZiX3BvcnQpOgogICAgICAgICAgICAgICAgcmFpc2UgUnVudGltZUVycm9yKCJY -dm5jIGRpZCBub3QgYmVjb21lIHJlYWR5IikKICAgICAgICAgICAgc2VsZi5zdGFydF9kYnVzKG51bWJlciwgZW52aXJvbm1lbnQsIHJ1bnRpbWUpCiAgICAg -ICAgICAgIHNlbGYuc3RhcnRfc2Vzc2lvbl9jb21wb25lbnRzKG51bWJlciwgZW52aXJvbm1lbnQsIHJ1bnRpbWUpCiAgICAgICAgZWxpZiBudW1iZXIgaW4g -c2VsZi5uYW1lZF9wcm9jZXNzZXM6CiAgICAgICAgICAgIG5hbWVkID0gc2VsZi5uYW1lZF9wcm9jZXNzZXNbbnVtYmVyXQogICAgICAgICAgICBkYnVzID0g -bmFtZWQuZ2V0KCJkYnVzIikKICAgICAgICAgICAgaWYgZGJ1cyBpcyBOb25lIG9yIG5vdCBzZWxmLnByb2Nlc3NfYWxpdmUoZGJ1cyk6CiAgICAgICAgICAg -ICAgICBmb3IgbmFtZSBpbiAoIm9wZW5ib3giLCAidGludDIiLCAidm5jY29uZmlnIik6CiAgICAgICAgICAgICAgICAgICAgc2VsZi5yZW1vdmVfcHJvY2Vz -cyhudW1iZXIsIG5hbWUpCiAgICAgICAgICAgICAgICBzZWxmLnN0YXJ0X2RidXMobnVtYmVyLCBlbnZpcm9ubWVudCwgcnVudGltZSkKICAgICAgICAgICAg -c2VsZi5zdGFydF9zZXNzaW9uX2NvbXBvbmVudHMobnVtYmVyLCBlbnZpcm9ubWVudCwgcnVudGltZSkKICAgICAgICB3ZWJzb2NraWZ5X3Byb2Nlc3MgPSBz -ZWxmLndlYnNvY2tpZnlfcHJvY2Vzc2VzLmdldChudW1iZXIpCiAgICAgICAgaWYgKHdlYnNvY2tpZnlfcHJvY2VzcyBpcyBOb25lIG9yIG5vdCBzZWxmLnBy -b2Nlc3NfYWxpdmUod2Vic29ja2lmeV9wcm9jZXNzKQogICAgICAgICAgICAgICAgb3Igbm90IG5vdm5jX3JlYWR5KG51bWJlcikpOgogICAgICAgICAgICBz -ZWxmLnN0YXJ0X3dlYnNvY2tpZnkobnVtYmVyLCBlbnZpcm9ubWVudCwgd2Vic29ja2lmeSkKICAgICAgICAocnVudGltZSAvICJlbnYiKS53cml0ZV90ZXh0 -KGYiZXhwb3J0IERJU1BMQVk9OntudW1iZXJ9XG4iKQoKICAgIGRlZiBzdXBlcnZpc2Uoc2VsZiwgbnVtYmVyLCBmaW5pc2hlZCk6CiAgICAgICAgcnVudGlt -ZSA9IHNlbGYucnVudGltZSAvIHN0cihudW1iZXIpCiAgICAgICAgcnVudGltZS5ta2Rpcihtb2RlPTBvNzAwLCBwYXJlbnRzPVRydWUsIGV4aXN0X29rPVRy -dWUpCiAgICAgICAgZW52aXJvbm1lbnQgPSBkaWN0KG9zLmVudmlyb24sIERJU1BMQVk9ZiI6e251bWJlcn0iLCBDTVVYX0RFU0tUT1BfUlVOVElNRV9ESVI9 -c3RyKHJ1bnRpbWUpKQogICAgICAgIGVudmlyb25tZW50WyJDSFJPTUVfQ09ORklHX0hPTUUiXSA9IHN0cihydW50aW1lIC8gImNocm9tZS1jb25maWciKQog -ICAgICAgIGVudmlyb25tZW50WyJYREdfQ09ORklHX0hPTUUiXSA9IHN0cihydW50aW1lIC8gInhkZy1jb25maWciKQogICAgICAgIGZvciBrZXkgaW4gKCJO -T1RJRllfU09DS0VUIiwgIkRCVVNfU0VTU0lPTl9CVVNfQUREUkVTUyIsICJEQlVTX1NFU1NJT05fQlVTX1BJRCIsCiAgICAgICAgICAgICAgICAgICAgIkFU -X1NQSV9CVVNfQUREUkVTUyIsICJBVF9TUElfQlVTIik6CiAgICAgICAgICAgIGVudmlyb25tZW50LnBvcChrZXksIE5vbmUpCiAgICAgICAgd2hpbGUgbm90 -IHNlbGYuc2h1dGRvd24uaXNfc2V0KCk6CiAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgICMgVGhlIHN1cGVydmlzb3IgaXMgYSBoZWFsdGggY2hl -Y2ssIG5vdCBhIHRpbWVyLWRyaXZlbiByZXN0YXJ0LgogICAgICAgICAgICAgICAgIyBQcmVzZXJ2ZSBhbiBhY3RpdmUgUkZCL25vVk5DIHNlc3Npb24gdW50 -aWwgaXRzIG93bmVkIGVuZHBvaW50cyBmYWlsLgogICAgICAgICAgICAgICAgaWYgbm90IHNlbGYuc2VydmljZV9yZWFkeShudW1iZXIpOgogICAgICAgICAg -ICAgICAgICAgIHNlbGYuc3RhcnRfY29tcG9uZW50cyhudW1iZXIsIGVudmlyb25tZW50LCBydW50aW1lKQogICAgICAgICAgICAgICAgc3RhdGUgPSAicnVu -bmluZyIgaWYgc2VsZi5zZXJ2aWNlX3JlYWR5KG51bWJlcikgZWxzZSAidW5hdmFpbGFibGUiCiAgICAgICAgICAgIGV4Y2VwdCBOb1ZOQ1VuYXZhaWxhYmxl -OgogICAgICAgICAgICAgICAgc2VsZi50ZXJtaW5hdGVfd2Vic29ja2lmeShudW1iZXIpCiAgICAgICAgICAgICAgICBzdGF0ZSA9ICJ1bmF2YWlsYWJsZSIK -ICAgICAgICAgICAgZXhjZXB0IChPU0Vycm9yLCBSdW50aW1lRXJyb3IsIHN1YnByb2Nlc3MuU3VicHJvY2Vzc0Vycm9yKToKICAgICAgICAgICAgICAgIHNl -bGYudGVybWluYXRlKG51bWJlcikKICAgICAgICAgICAgICAgIHN0YXRlID0gInVuYXZhaWxhYmxlIgogICAgICAgICAgICB3aXRoIHNlbGYubG9jazoKICAg -ICAgICAgICAgICAgIHNlbGYuc3RhdGVzW251bWJlcl0gPSBzdGF0ZQogICAgICAgICAgICBmaW5pc2hlZC5zZXQoKQogICAgICAgICAgICBzZWxmLnNodXRk -b3duLndhaXQoMzApCiAgICAgICAgc2VsZi50ZXJtaW5hdGUobnVtYmVyKQoKICAgIGRlZiBzbmFwc2hvdChzZWxmKToKICAgICAgICBwcmltYXJ5X3N0YXRl -ID0gInJ1bm5pbmciIGlmIHJlYWR5KDEpIGVsc2UgInVuYXZhaWxhYmxlIgogICAgICAgIHdpdGggc2VsZi5sb2NrOgogICAgICAgICAgICByZXR1cm4geyJ2 -ZXJzaW9uIjogMSwgImNhbkNyZWF0ZSI6IGxlbihzZWxmLmpvYnMpIDwgTUFYX0RJU1BMQVlTIC0gMSwKICAgICAgICAgICAgICAgICAgICAiZGlzcGxheXMi -OiBbZGVzY3JpcHRvcigxLCBwcmltYXJ5X3N0YXRlKV0KICAgICAgICAgICAgICAgICAgICArIFtkZXNjcmlwdG9yKG4sIHNlbGYuc3RhdGVzLmdldChuLCAi -bGF1bmNoaW5nIikpIGZvciBuIGluIHNlbGYuY2F0YWxvZy5udW1iZXJzKCldfQoKICAgIGRlZiBoYW5kbGUoc2VsZiwgcmVxdWVzdCk6CiAgICAgICAgaWYg -cmVxdWVzdC5nZXQoImFjdGlvbiIpID09ICJsaXN0IjoKICAgICAgICAgICAgcmV0dXJuIHNlbGYuc25hcHNob3QoKQogICAgICAgIGlmIHJlcXVlc3QuZ2V0 -KCJhY3Rpb24iKSAhPSAiY3JlYXRlIjoKICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJvcigidW5zdXBwb3J0ZWQgZGlzcGxheSBhY3Rpb24iKQogICAgICAg -IG51bWJlciA9IHNlbGYuY2F0YWxvZy5hbGxvY2F0ZShyZXF1ZXN0LmdldCgicmVxdWVzdCIpKQogICAgICAgIHNlbGYuc3RhcnQobnVtYmVyKS53YWl0KDU1 -KQogICAgICAgIHJlc3VsdCA9IHNlbGYuc25hcHNob3QoKQogICAgICAgIHJlc3VsdFsiY3JlYXRlZCJdID0gZiJkaXNwbGF5OntudW1iZXJ9IgogICAgICAg -IGlmIHNlbGYuc3RhdGVzLmdldChudW1iZXIpICE9ICJydW5uaW5nIjoKICAgICAgICAgICAgcmVzdWx0WyJlcnJvciJdID0gImRpc3BsYXlfc3RhcnRfZmFp -bGVkIgogICAgICAgIHJldHVybiByZXN1bHQKCgpkZWYgc2VydmUocnVudGltZSk6CiAgICBydW50aW1lID0gUGF0aChydW50aW1lKQogICAgcnVudGltZS5t -a2Rpcihtb2RlPTBvNzAwLCBwYXJlbnRzPVRydWUsIGV4aXN0X29rPVRydWUpCiAgICB3aXRoIChydW50aW1lIC8gImRpc3BsYXktY29udHJvbC5sb2NrIiku -b3BlbigidyIpIGFzIGxvY2s6CiAgICAgICAgdHJ5OgogICAgICAgICAgICBmY250bC5mbG9jayhsb2NrLCBmY250bC5MT0NLX0VYIHwgZmNudGwuTE9DS19O -QikKICAgICAgICBleGNlcHQgQmxvY2tpbmdJT0Vycm9yOgogICAgICAgICAgICByZXR1cm4KICAgICAgICBpZiBub3QgYWxsKHNodXRpbC53aGljaChuYW1l -KSBmb3IgbmFtZSBpbiAoIndlYnNvY2tpZnkiLCAib3BlbmJveCIsICJkYnVzLWxhdW5jaCIpKToKICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJvcigiZGlz -cGxheSBydW50aW1lIHVuYXZhaWxhYmxlIikKICAgICAgICBpZiBub3QgKHNodXRpbC53aGljaCgiWHZuYyIpIG9yIHNodXRpbC53aGljaCgiWHRpZ2Vydm5j -IikpOgogICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJkaXNwbGF5IHJ1bnRpbWUgdW5hdmFpbGFibGUiKQogICAgICAgIHNlcnZpY2UgPSBEaXNwbGF5 -U2VydmljZShEaXNwbGF5Q2F0YWxvZyhQYXRoLmhvbWUoKSAvICIuY211eCIgLyAiZGlzcGxheXMiKSwgcnVudGltZSAvICJkaXNwbGF5cyIpCiAgICAgICAg -Zm9yIG51bWJlciBpbiBzZXJ2aWNlLmNhdGFsb2cubnVtYmVycygpOgogICAgICAgICAgICBzZXJ2aWNlLnN0YXJ0KG51bWJlcikKICAgICAgICBwYXRoID0g -cnVudGltZSAvICJkaXNwbGF5LWNvbnRyb2wuc29jayIKICAgICAgICBpZiBwYXRoLmV4aXN0cygpOgogICAgICAgICAgICBwYXRoLnVubGluaygpCgogICAg -ICAgIGNsYXNzIEhhbmRsZXIoc29ja2V0c2VydmVyLlN0cmVhbVJlcXVlc3RIYW5kbGVyKToKICAgICAgICAgICAgZGVmIGhhbmRsZShzZWxmKToKICAgICAg -ICAgICAgICAgIHNlbGYuY29ubmVjdGlvbi5zZXR0aW1lb3V0KDYwKQogICAgICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgIHJhdyA9IHNl -bGYucmZpbGUucmVhZGxpbmUoNDA5NykKICAgICAgICAgICAgICAgICAgICBpZiBsZW4ocmF3KSA+IDQwOTY6CiAgICAgICAgICAgICAgICAgICAgICAgIHJh -aXNlIFZhbHVlRXJyb3IoImRpc3BsYXkgcmVxdWVzdCB0b28gbGFyZ2UiKQogICAgICAgICAgICAgICAgICAgIHJlc3VsdCA9IHNlcnZpY2UuaGFuZGxlKGpz -b24ubG9hZHMocmF3KSkKICAgICAgICAgICAgICAgIGV4Y2VwdCAoVmFsdWVFcnJvciwgS2V5RXJyb3IsIFR5cGVFcnJvciwgT1NFcnJvcik6CiAgICAgICAg -ICAgICAgICAgICAgcmVzdWx0ID0geyJlcnJvciI6ICJkaXNwbGF5X3JlcXVlc3RfZmFpbGVkIn0KICAgICAgICAgICAgICAgIHRyeToKICAgICAgICAgICAg -ICAgICAgICBzZWxmLndmaWxlLndyaXRlKGpzb24uZHVtcHMocmVzdWx0KS5lbmNvZGUoKSArIGIiXG4iKQogICAgICAgICAgICAgICAgZXhjZXB0IE9TRXJy -b3I6CiAgICAgICAgICAgICAgICAgICAgcGFzcyAgIyBUaGUgcGVyc2lzdGVkIHJlY2VpcHQgcmVtYWlucyByZXBsYXlhYmxlIGFmdGVyIGRpc2Nvbm5lY3Qu -CgogICAgICAgIGNsYXNzIFNlcnZlcihzb2NrZXRzZXJ2ZXIuVGhyZWFkaW5nVW5peFN0cmVhbVNlcnZlcik6CiAgICAgICAgICAgIGRhZW1vbl90aHJlYWRz -ID0gVHJ1ZQoKICAgICAgICBwcmV2aW91c191bWFzayA9IG9zLnVtYXNrKDBvMDc3KQogICAgICAgIHRyeToKICAgICAgICAgICAgd2l0aCBTZXJ2ZXIoc3Ry -KHBhdGgpLCBIYW5kbGVyKSBhcyBzZXJ2ZXI6CiAgICAgICAgICAgICAgICBvcy51bWFzayhwcmV2aW91c191bWFzaykKICAgICAgICAgICAgICAgIHNlcnZl -ci5zZXJ2ZV9mb3JldmVyKCkKICAgICAgICBmaW5hbGx5OgogICAgICAgICAgICBvcy51bWFzayhwcmV2aW91c191bWFzaykKICAgICAgICAgICAgc2Vydmlj -ZS5zaHV0ZG93bi5zZXQoKQogICAgICAgICAgICBwYXRoLnVubGluayhtaXNzaW5nX29rPVRydWUpCgoKZGVmIG1haW4oKToKICAgIHBhcnNlciA9IGFyZ3Bh -cnNlLkFyZ3VtZW50UGFyc2VyKGRlc2NyaXB0aW9uPV9fZG9jX18pCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50KCJhY3Rpb24iLCBjaG9pY2VzPVsibGlzdCIs -ICJjcmVhdGUiLCAic2VydmUiXSkKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoIi0tcmVxdWVzdC1pZCIpCiAgICBhcmdzID0gcGFyc2VyLnBhcnNlX2FyZ3Mo -KQogICAgcnVudGltZSA9IG9zLmVudmlyb24uZ2V0KCJDTVVYX0RFU0tUT1BfUlVOVElNRV9ESVIiLCAiL3J1bi9jbXV4LWRlc2t0b3AiKQogICAgaWYgYXJn -cy5hY3Rpb24gPT0gInNlcnZlIjoKICAgICAgICBzZXJ2ZShydW50aW1lKQogICAgICAgIHJldHVybiAwCiAgICBpZiBhcmdzLmFjdGlvbiA9PSAiY3JlYXRl -IiBhbmQgbm90IGFyZ3MucmVxdWVzdF9pZDoKICAgICAgICBwYXJzZXIuZXJyb3IoImNyZWF0ZSByZXF1aXJlcyAtLXJlcXVlc3QtaWQgKHJldGFpbiBpdCB3 -aGVuIHJldHJ5aW5nKSIpCiAgICB0cnk6CiAgICAgICAgd2l0aCBzb2NrZXQuc29ja2V0KHNvY2tldC5BRl9VTklYLCBzb2NrZXQuU09DS19TVFJFQU0pIGFz -IGNsaWVudDoKICAgICAgICAgICAgY2xpZW50LnNldHRpbWVvdXQoNjApCiAgICAgICAgICAgIGNsaWVudC5jb25uZWN0KHN0cihQYXRoKHJ1bnRpbWUpIC8g -ImRpc3BsYXktY29udHJvbC5zb2NrIikpCiAgICAgICAgICAgIGNsaWVudC5zZW5kYWxsKGpzb24uZHVtcHMoeyJhY3Rpb24iOiBhcmdzLmFjdGlvbiwgInJl -cXVlc3QiOiBhcmdzLnJlcXVlc3RfaWR9KS5lbmNvZGUoKSArIGIiXG4iKQogICAgICAgICAgICB3aXRoIGNsaWVudC5tYWtlZmlsZSgicmIiKSBhcyBzdHJl -YW06CiAgICAgICAgICAgICAgICByZXN1bHQgPSBqc29uLmxvYWRzKHN0cmVhbS5yZWFkbGluZSg2NTUzNikpCiAgICBleGNlcHQgKE9TRXJyb3IsIFZhbHVl -RXJyb3IpOgogICAgICAgIHJlc3VsdCA9IHsiZXJyb3IiOiAiZGlzcGxheV9ydW50aW1lX3VuYXZhaWxhYmxlIn0KICAgIHByaW50KGpzb24uZHVtcHMocmVz -dWx0KSkKICAgIHJldHVybiAxIGlmICJlcnJvciIgaW4gcmVzdWx0IGVsc2UgMAoKCmlmIF9fbmFtZV9fID09ICJfX21haW5fXyI6CiAgICByYWlzZSBTeXN0 -ZW1FeGl0KG1haW4oKSkK +dChwb3J0LCB0aW1lb3V0PTIwKToKICAgICAgICBkZWFkbGluZSA9IHRpbWUubW9ub3RvbmljKCkgKyB0aW1lb3V0CiAgICAgICAgd2hpbGUgdGltZS5tb25v +dG9uaWMoKSA8IGRlYWRsaW5lOgogICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICB3aXRoIHNvY2tldC5jcmVhdGVfY29ubmVjdGlvbigoIjEyNy4w +LjAuMSIsIHBvcnQpLCB0aW1lb3V0PTAuNSk6CiAgICAgICAgICAgICAgICAgICAgcmV0dXJuIFRydWUKICAgICAgICAgICAgZXhjZXB0IE9TRXJyb3I6CiAg +ICAgICAgICAgICAgICB0aW1lLnNsZWVwKDAuMSkKICAgICAgICByZXR1cm4gRmFsc2UKCiAgICBAc3RhdGljbWV0aG9kCiAgICBkZWYgcHJvY2Vzc19hbGl2 +ZShwcm9jZXNzKToKICAgICAgICBwb2xsID0gZ2V0YXR0cihwcm9jZXNzLCAicG9sbCIsIE5vbmUpCiAgICAgICAgcmV0dXJuIHBvbGwgaXMgTm9uZSBvciBw +b2xsKCkgaXMgTm9uZQoKICAgIGRlZiBjb21wb25lbnRzX2FsaXZlKHNlbGYsIG51bWJlcik6CiAgICAgICAgbmFtZWQgPSBzZWxmLm5hbWVkX3Byb2Nlc3Nl +cy5nZXQobnVtYmVyKQogICAgICAgIGlmIG5hbWVkIGlzIG5vdCBOb25lOgogICAgICAgICAgICByZXR1cm4gYm9vbChuYW1lZCkgYW5kIGFsbChzZWxmLnBy +b2Nlc3NfYWxpdmUocHJvY2VzcykgZm9yIHByb2Nlc3MgaW4gbmFtZWQudmFsdWVzKCkpIFwKICAgICAgICAgICAgICAgIGFuZCAoInh2bmMiIGluIG5hbWVk +IG9yIHJmYl9yZWFkeShudW1iZXIpKQogICAgICAgIHByb2Nlc3NlcyA9IHNlbGYucHJvY2Vzc2VzLmdldChudW1iZXIsIFtdKQogICAgICAgIHJldHVybiBi +b29sKHByb2Nlc3NlcykgYW5kIGFsbChzZWxmLnByb2Nlc3NfYWxpdmUocHJvY2VzcykgZm9yIHByb2Nlc3MgaW4gcHJvY2Vzc2VzKQoKICAgIGRlZiBzZXJ2 +aWNlX3JlYWR5KHNlbGYsIG51bWJlcik6CiAgICAgICAgaWYgbm90IHJlYWR5KG51bWJlcikgb3Igbm90IHNlbGYuY29tcG9uZW50c19hbGl2ZShudW1iZXIp +OgogICAgICAgICAgICByZXR1cm4gRmFsc2UKICAgICAgICB3ZWJzb2NraWZ5ID0gc2VsZi53ZWJzb2NraWZ5X3Byb2Nlc3Nlcy5nZXQobnVtYmVyKQogICAg +ICAgIHJldHVybiB3ZWJzb2NraWZ5IGlzIE5vbmUgb3Igc2VsZi5wcm9jZXNzX2FsaXZlKHdlYnNvY2tpZnkpCgogICAgZGVmIHRyYWNrX3Byb2Nlc3Moc2Vs +ZiwgbnVtYmVyLCBuYW1lLCBwcm9jZXNzKToKICAgICAgICBuYW1lZCA9IHNlbGYubmFtZWRfcHJvY2Vzc2VzLnNldGRlZmF1bHQobnVtYmVyLCB7fSkKICAg +ICAgICBwcmV2aW91cyA9IG5hbWVkLmdldChuYW1lKQogICAgICAgIGlmIHByZXZpb3VzIGlzIG5vdCBOb25lOgogICAgICAgICAgICBzZWxmLnByb2Nlc3Nl +c1tudW1iZXJdID0gWwogICAgICAgICAgICAgICAgaXRlbSBmb3IgaXRlbSBpbiBzZWxmLnByb2Nlc3Nlcy5nZXQobnVtYmVyLCBbXSkgaWYgaXRlbSBpcyBu +b3QgcHJldmlvdXMKICAgICAgICAgICAgXQogICAgICAgIHNlbGYucHJvY2Vzc2VzLnNldGRlZmF1bHQobnVtYmVyLCBbXSkuYXBwZW5kKHByb2Nlc3MpCiAg +ICAgICAgbmFtZWRbbmFtZV0gPSBwcm9jZXNzCgogICAgZGVmIHJlbW92ZV9wcm9jZXNzKHNlbGYsIG51bWJlciwgbmFtZSk6CiAgICAgICAgcHJvY2VzcyA9 +IHNlbGYubmFtZWRfcHJvY2Vzc2VzLnNldGRlZmF1bHQobnVtYmVyLCB7fSkucG9wKG5hbWUsIE5vbmUpCiAgICAgICAgaWYgcHJvY2VzcyBpcyBOb25lOgog +ICAgICAgICAgICByZXR1cm4KICAgICAgICBzZWxmLnByb2Nlc3Nlc1tudW1iZXJdID0gW2l0ZW0gZm9yIGl0ZW0gaW4gc2VsZi5wcm9jZXNzZXMuZ2V0KG51 +bWJlciwgW10pIGlmIGl0ZW0gaXMgbm90IHByb2Nlc3NdCiAgICAgICAgdHJ5OgogICAgICAgICAgICBwcm9jZXNzLnRlcm1pbmF0ZSgpCiAgICAgICAgZXhj +ZXB0IE9TRXJyb3I6CiAgICAgICAgICAgIHBhc3MKCiAgICBkZWYgcmVjb3Zlcl9wcm9jZXNzZXMoc2VsZiwgbnVtYmVyLCBydW50aW1lLCBlbnZpcm9ubWVu +dCk6CiAgICAgICAgc2VsZi5wcm9jZXNzZXNbbnVtYmVyXSA9IFtdCiAgICAgICAgc2VsZi5uYW1lZF9wcm9jZXNzZXNbbnVtYmVyXSA9IHt9CiAgICAgICAg +c2Vzc2lvbiA9IFBhdGgocnVudGltZSkgLyAic2Vzc2lvbi5qc29uIgogICAgICAgIHRyeToKICAgICAgICAgICAgc2F2ZWQgPSBqc29uLmxvYWRzKHNlc3Np +b24ucmVhZF90ZXh0KCkpCiAgICAgICAgICAgIGRidXNfcGlkID0gaW50KHNhdmVkLmdldCgicGlkIiwgMCkpCiAgICAgICAgICAgIGRidXMgPSBPd25lZFBy +b2Nlc3MoZGJ1c19waWQpCiAgICAgICAgICAgIGlmIGRidXNfcGlkID4gMSBhbmQgc2VsZi5wcm9jZXNzX2FsaXZlKGRidXMpOgogICAgICAgICAgICAgICAg +aWYgc2F2ZWQuZ2V0KCJhZGRyZXNzIik6CiAgICAgICAgICAgICAgICAgICAgZW52aXJvbm1lbnRbIkRCVVNfU0VTU0lPTl9CVVNfQUREUkVTUyJdID0gc2F2 +ZWRbImFkZHJlc3MiXQogICAgICAgICAgICAgICAgZW52aXJvbm1lbnRbIkRCVVNfU0VTU0lPTl9CVVNfUElEIl0gPSBzdHIoZGJ1c19waWQpCiAgICAgICAg +ICAgICAgICBzZWxmLnRyYWNrX3Byb2Nlc3MobnVtYmVyLCAiZGJ1cyIsIGRidXMpCiAgICAgICAgZXhjZXB0IChPU0Vycm9yLCBWYWx1ZUVycm9yLCBUeXBl +RXJyb3IsIGpzb24uSlNPTkRlY29kZUVycm9yKToKICAgICAgICAgICAgcGFzcwogICAgICAgIHVpZCA9IHN0cihvcy5nZXR1aWQoKSkKICAgICAgICBmb3Ig +bmFtZSBpbiAoIm9wZW5ib3giLCAidGludDIiLCAidm5jY29uZmlnIik6CiAgICAgICAgICAgIG1hcmtlciA9IHN0cihQYXRoKHJ1bnRpbWUpIC8gImJpbiIg +LyBmImNtdXgtZGlzcGxheS17bnVtYmVyfS17bmFtZX0iKQogICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICBwYXR0ZXJuID0gcmYiXntyZS5lc2Nh +cGUobWFya2VyKX0oPzpbWzpzcGFjZTpdXXwkKSIKICAgICAgICAgICAgICAgIG91dHB1dCA9IHN1YnByb2Nlc3MuY2hlY2tfb3V0cHV0KFsicGdyZXAiLCAi +LXUiLCB1aWQsICItZiIsIHBhdHRlcm5dLCB0ZXh0PVRydWUpCiAgICAgICAgICAgICAgICBwaWQgPSBpbnQob3V0cHV0LnNwbGl0bGluZXMoKVswXSkKICAg +ICAgICAgICAgZXhjZXB0IChPU0Vycm9yLCBWYWx1ZUVycm9yLCBJbmRleEVycm9yLCBzdWJwcm9jZXNzLkNhbGxlZFByb2Nlc3NFcnJvcik6CiAgICAgICAg +ICAgICAgICBjb250aW51ZQogICAgICAgICAgICBzZWxmLnRyYWNrX3Byb2Nlc3MobnVtYmVyLCBuYW1lLCBPd25lZFByb2Nlc3MocGlkKSkKICAgICAgICB0 +cnk6CiAgICAgICAgICAgIG91dHB1dCA9IHN1YnByb2Nlc3MuY2hlY2tfb3V0cHV0KAogICAgICAgICAgICAgICAgWyJwZ3JlcCIsICItdSIsIHVpZCwgIi1m +IiwKICAgICAgICAgICAgICAgICByZiJeKD86LipcLyk/KD86WHZuY3xYdGlnZXJ2bmMpLio6e251bWJlcn0uKi1yZmJwb3J0XHMrezU5MDAgKyBudW1iZXJ9 +KD86XHN8JCkiXSwKICAgICAgICAgICAgICAgIHRleHQ9VHJ1ZQogICAgICAgICAgICApCiAgICAgICAgICAgIHBpZCA9IGludChvdXRwdXQuc3BsaXRsaW5l +cygpWzBdKQogICAgICAgIGV4Y2VwdCAoT1NFcnJvciwgVmFsdWVFcnJvciwgSW5kZXhFcnJvciwgc3VicHJvY2Vzcy5DYWxsZWRQcm9jZXNzRXJyb3IpOgog +ICAgICAgICAgICBwaWQgPSAwCiAgICAgICAgaWYgcGlkID4gMToKICAgICAgICAgICAgc2VsZi50cmFja19wcm9jZXNzKG51bWJlciwgInh2bmMiLCBPd25l +ZFByb2Nlc3MocGlkKSkKCiAgICBkZWYgc3RhcnRfZGJ1cyhzZWxmLCBudW1iZXIsIGVudmlyb25tZW50LCBydW50aW1lKToKICAgICAgICBidXMgPSBzdWJw +cm9jZXNzLnJ1bihbImRidXMtbGF1bmNoIiwgIi0tc2gtc3ludGF4Il0sIGVudj1lbnZpcm9ubWVudCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBz +dGRpbj1zdWJwcm9jZXNzLkRFVk5VTEwsIHN0ZG91dD1zdWJwcm9jZXNzLlBJUEUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3RkZXJyPXN1YnBy +b2Nlc3MuREVWTlVMTCwgdGV4dD1UcnVlLCB0aW1lb3V0PTEwLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNoZWNrPVRydWUpLnN0ZG91dAogICAg +ICAgIGZvciBsaW5lIGluIGJ1cy5zcGxpdGxpbmVzKCk6CiAgICAgICAgICAgIGlmICI9IiBub3QgaW4gbGluZToKICAgICAgICAgICAgICAgIGNvbnRpbnVl +CiAgICAgICAgICAgIGtleSwgdmFsdWUgPSBsaW5lLnNwbGl0KCI9IiwgMSkKICAgICAgICAgICAgZW52aXJvbm1lbnRba2V5XSA9IHZhbHVlLnN0cmlwKCku +cnN0cmlwKCI7Iikuc3RyaXAoKS5zdHJpcCgiJyIpCiAgICAgICAgdHJ5OgogICAgICAgICAgICBkYnVzX3BpZCA9IGludChlbnZpcm9ubWVudC5nZXQoIkRC +VVNfU0VTU0lPTl9CVVNfUElEIiwgIjAiKSkKICAgICAgICBleGNlcHQgVmFsdWVFcnJvcjoKICAgICAgICAgICAgZGJ1c19waWQgPSAwCiAgICAgICAgaWYg +ZGJ1c19waWQgPiAxOgogICAgICAgICAgICBzZWxmLnRyYWNrX3Byb2Nlc3MobnVtYmVyLCAiZGJ1cyIsIE93bmVkUHJvY2VzcyhkYnVzX3BpZCkpCiAgICAg +ICAgICAgIChQYXRoKHJ1bnRpbWUpIC8gInNlc3Npb24uanNvbiIpLndyaXRlX3RleHQoanNvbi5kdW1wcyh7CiAgICAgICAgICAgICAgICAicGlkIjogZGJ1 +c19waWQsCiAgICAgICAgICAgICAgICAiYWRkcmVzcyI6IGVudmlyb25tZW50LmdldCgiREJVU19TRVNTSU9OX0JVU19BRERSRVNTIiwgIiIpCiAgICAgICAg +ICAgIH0pKQoKICAgIGRlZiBzdGFydF9zZXNzaW9uX2NvbXBvbmVudHMoc2VsZiwgbnVtYmVyLCBlbnZpcm9ubWVudCwgcnVudGltZSk6CiAgICAgICAgZm9y +IGV4ZWN1dGFibGUsIGFyZ3MgaW4gWwogICAgICAgICAgICAoIm9wZW5ib3giLCBbXSksICgidGludDIiLCBbIi1jIiwgIi9ldGMvY211eC90aW50MnJjIl0p +LAogICAgICAgICAgICAoInZuY2NvbmZpZyIsIFsiLW5vd2luIl0pLAogICAgICAgIF06CiAgICAgICAgICAgIGV4aXN0aW5nID0gc2VsZi5uYW1lZF9wcm9j +ZXNzZXMuZ2V0KG51bWJlciwge30pLmdldChleGVjdXRhYmxlKQogICAgICAgICAgICBpZiBleGlzdGluZyBpcyBub3QgTm9uZSBhbmQgc2VsZi5wcm9jZXNz +X2FsaXZlKGV4aXN0aW5nKToKICAgICAgICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgICAgIHBhdGggPSBleGVjdXRhYmxlIGlmICIvIiBpbiBleGVjdXRh +YmxlIGVsc2Ugc2h1dGlsLndoaWNoKGV4ZWN1dGFibGUpCiAgICAgICAgICAgIGlmIG5vdCBwYXRoOgogICAgICAgICAgICAgICAgaWYgZXhlY3V0YWJsZSA9 +PSAidm5jY29uZmlnIjoKICAgICAgICAgICAgICAgICAgICBjb250aW51ZQogICAgICAgICAgICAgICAgcmFpc2UgUnVudGltZUVycm9yKGYibWlzc2luZyBk +aXNwbGF5IGNvbXBvbmVudDoge2V4ZWN1dGFibGV9IikKICAgICAgICAgICAgcGF0aCA9IHNlbGYuc2NvcGVkX2NvbXBvbmVudF9wYXRoKGV4ZWN1dGFibGUs +IHBhdGgsIG51bWJlciwgcnVudGltZSkKICAgICAgICAgICAgcHJvY2VzcyA9IHN1YnByb2Nlc3MuUG9wZW4oW3BhdGhdICsgYXJncywgZW52PWVudmlyb25t +ZW50LAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdGRpbj1zdWJwcm9jZXNzLkRFVk5VTEwsCiAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgIHN0ZG91dD1zdWJwcm9jZXNzLkRFVk5VTEwsIHN0ZGVycj1zdWJwcm9jZXNzLkRFVk5VTEwpCiAgICAgICAgICAgIHNlbGYu +dHJhY2tfcHJvY2VzcyhudW1iZXIsIGV4ZWN1dGFibGUsIHByb2Nlc3MpCgogICAgZGVmIHN0YXJ0X3dlYnNvY2tpZnkoc2VsZiwgbnVtYmVyLCBlbnZpcm9u +bWVudCwgZXhlY3V0YWJsZSk6CiAgICAgICAgcGF0aCA9IGV4ZWN1dGFibGUgaWYgIi8iIGluIGV4ZWN1dGFibGUgZWxzZSBzaHV0aWwud2hpY2goZXhlY3V0 +YWJsZSkKICAgICAgICBpZiBub3QgcGF0aDoKICAgICAgICAgICAgcmFpc2UgTm9WTkNVbmF2YWlsYWJsZSgibWlzc2luZyBkaXNwbGF5IGNvbXBvbmVudDog +d2Vic29ja2lmeSIpCiAgICAgICAgc2VsZi50ZXJtaW5hdGVfd2Vic29ja2lmeShudW1iZXIpCiAgICAgICAgcmZiX3BvcnQsIG5vdm5jX3BvcnQgPSA1OTAw +ICsgbnVtYmVyLCA2OTAwICsgbnVtYmVyCiAgICAgICAgcHJvY2VzcyA9IHN1YnByb2Nlc3MuUG9wZW4oCiAgICAgICAgICAgIFtwYXRoLCAiLS13ZWIiLCAi +L3Vzci9zaGFyZS9ub3ZuYyIsICItLWhlYXJ0YmVhdCIsICIzMCIsCiAgICAgICAgICAgICBmIjEyNy4wLjAuMTp7bm92bmNfcG9ydH0iLCBmIjEyNy4wLjAu +MTp7cmZiX3BvcnR9Il0sCiAgICAgICAgICAgIGVudj1lbnZpcm9ubWVudCwgc3RkaW49c3VicHJvY2Vzcy5ERVZOVUxMLAogICAgICAgICAgICBzdGRvdXQ9 +c3VicHJvY2Vzcy5ERVZOVUxMLCBzdGRlcnI9c3VicHJvY2Vzcy5ERVZOVUxMKQogICAgICAgIHNlbGYud2Vic29ja2lmeV9wcm9jZXNzZXNbbnVtYmVyXSA9 +IHByb2Nlc3MKICAgICAgICBpZiBub3Qgc2VsZi53YWl0X2Zvcl9wb3J0KG5vdm5jX3BvcnQsIHRpbWVvdXQ9MTApOgogICAgICAgICAgICBzZWxmLnRlcm1p +bmF0ZV93ZWJzb2NraWZ5KG51bWJlcikKICAgICAgICAgICAgcmFpc2UgTm9WTkNVbmF2YWlsYWJsZSgibm9WTkMgZGlkIG5vdCBiZWNvbWUgcmVhZHkiKQoK +ICAgIGRlZiBzdGFydF9jb21wb25lbnRzKHNlbGYsIG51bWJlciwgZW52aXJvbm1lbnQsIHJ1bnRpbWUpOgogICAgICAgIGlmIHNlbGYuc2VydmljZV9yZWFk +eShudW1iZXIpOgogICAgICAgICAgICByZXR1cm4KICAgICAgICByZmJfaXNfcmVhZHkgPSByZmJfcmVhZHkobnVtYmVyKQogICAgICAgIHZuYyA9IHNodXRp +bC53aGljaCgiWHZuYyIpIG9yIHNodXRpbC53aGljaCgiWHRpZ2Vydm5jIikKICAgICAgICB3ZWJzb2NraWZ5ID0gc2h1dGlsLndoaWNoKCJ3ZWJzb2NraWZ5 +IikKICAgICAgICBpZiBub3Qgd2Vic29ja2lmeToKICAgICAgICAgICAgcmFpc2UgTm9WTkNVbmF2YWlsYWJsZSgiZGlzcGxheSBydW50aW1lIHVuYXZhaWxh +YmxlIikKICAgICAgICBpZiBub3QgcmZiX2lzX3JlYWR5IGFuZCBub3Qgdm5jOgogICAgICAgICAgICByYWlzZSBSdW50aW1lRXJyb3IoImRpc3BsYXkgcnVu +dGltZSB1bmF2YWlsYWJsZSIpCiAgICAgICAgaWYgcmZiX2lzX3JlYWR5IGFuZCBudW1iZXIgbm90IGluIHNlbGYubmFtZWRfcHJvY2Vzc2VzOgogICAgICAg +ICAgICBzZWxmLnJlY292ZXJfcHJvY2Vzc2VzKG51bWJlciwgcnVudGltZSwgZW52aXJvbm1lbnQpCiAgICAgICAgaWYgbm90IHJmYl9pc19yZWFkeToKICAg +ICAgICAgICAgc2VsZi50ZXJtaW5hdGUobnVtYmVyKQogICAgICAgICAgICByZmJfcG9ydCA9IDU5MDAgKyBudW1iZXIKICAgICAgICAgICAgc2VsZi5wcm9j +ZXNzZXNbbnVtYmVyXSA9IFtdCiAgICAgICAgICAgIHNlbGYubmFtZWRfcHJvY2Vzc2VzW251bWJlcl0gPSB7fQogICAgICAgICAgICBzZWxmLnRyYWNrX3By +b2Nlc3MobnVtYmVyLCAieHZuYyIsIHN1YnByb2Nlc3MuUG9wZW4oCiAgICAgICAgICAgICAgICBbdm5jLCBmIjp7bnVtYmVyfSIsICItZ2VvbWV0cnkiLCBv +cy5lbnZpcm9uLmdldCgiQ01VWF9WTkNfR0VPTUVUUlkiLCAiMTQ0MHg5MDAiKSwKICAgICAgICAgICAgICAgICAiLWRlcHRoIiwgIjI0IiwgIi1yZmJwb3J0 +Iiwgc3RyKHJmYl9wb3J0KSwgIi1sb2NhbGhvc3QiLAogICAgICAgICAgICAgICAgICItU2VjdXJpdHlUeXBlcyIsICJOb25lIiwgIi1BbHdheXNTaGFyZWQi +XSwKICAgICAgICAgICAgICAgIGVudj1lbnZpcm9ubWVudCwgc3RkaW49c3VicHJvY2Vzcy5ERVZOVUxMLAogICAgICAgICAgICAgICAgc3Rkb3V0PXN1YnBy +b2Nlc3MuREVWTlVMTCwgc3RkZXJyPXN1YnByb2Nlc3MuREVWTlVMTCkpCiAgICAgICAgICAgIGlmIG5vdCBzZWxmLndhaXRfZm9yX3BvcnQocmZiX3BvcnQp +OgogICAgICAgICAgICAgICAgcmFpc2UgUnVudGltZUVycm9yKCJYdm5jIGRpZCBub3QgYmVjb21lIHJlYWR5IikKICAgICAgICAgICAgc2VsZi5zdGFydF9k +YnVzKG51bWJlciwgZW52aXJvbm1lbnQsIHJ1bnRpbWUpCiAgICAgICAgICAgIHNlbGYuc3RhcnRfc2Vzc2lvbl9jb21wb25lbnRzKG51bWJlciwgZW52aXJv +bm1lbnQsIHJ1bnRpbWUpCiAgICAgICAgZWxpZiBudW1iZXIgaW4gc2VsZi5uYW1lZF9wcm9jZXNzZXM6CiAgICAgICAgICAgIG5hbWVkID0gc2VsZi5uYW1l +ZF9wcm9jZXNzZXNbbnVtYmVyXQogICAgICAgICAgICBkYnVzID0gbmFtZWQuZ2V0KCJkYnVzIikKICAgICAgICAgICAgaWYgZGJ1cyBpcyBOb25lIG9yIG5v +dCBzZWxmLnByb2Nlc3NfYWxpdmUoZGJ1cyk6CiAgICAgICAgICAgICAgICBmb3IgbmFtZSBpbiAoIm9wZW5ib3giLCAidGludDIiLCAidm5jY29uZmlnIik6 +CiAgICAgICAgICAgICAgICAgICAgc2VsZi5yZW1vdmVfcHJvY2VzcyhudW1iZXIsIG5hbWUpCiAgICAgICAgICAgICAgICBzZWxmLnN0YXJ0X2RidXMobnVt +YmVyLCBlbnZpcm9ubWVudCwgcnVudGltZSkKICAgICAgICAgICAgc2VsZi5zdGFydF9zZXNzaW9uX2NvbXBvbmVudHMobnVtYmVyLCBlbnZpcm9ubWVudCwg +cnVudGltZSkKICAgICAgICB3ZWJzb2NraWZ5X3Byb2Nlc3MgPSBzZWxmLndlYnNvY2tpZnlfcHJvY2Vzc2VzLmdldChudW1iZXIpCiAgICAgICAgaWYgKHdl +YnNvY2tpZnlfcHJvY2VzcyBpcyBOb25lIG9yIG5vdCBzZWxmLnByb2Nlc3NfYWxpdmUod2Vic29ja2lmeV9wcm9jZXNzKQogICAgICAgICAgICAgICAgb3Ig +bm90IG5vdm5jX3JlYWR5KG51bWJlcikpOgogICAgICAgICAgICBzZWxmLnN0YXJ0X3dlYnNvY2tpZnkobnVtYmVyLCBlbnZpcm9ubWVudCwgd2Vic29ja2lm +eSkKICAgICAgICAocnVudGltZSAvICJlbnYiKS53cml0ZV90ZXh0KGYiZXhwb3J0IERJU1BMQVk9OntudW1iZXJ9XG4iKQoKICAgIGRlZiBzdXBlcnZpc2Uo +c2VsZiwgbnVtYmVyLCBmaW5pc2hlZCk6CiAgICAgICAgcnVudGltZSA9IHNlbGYucnVudGltZSAvIHN0cihudW1iZXIpCiAgICAgICAgcnVudGltZS5ta2Rp +cihtb2RlPTBvNzAwLCBwYXJlbnRzPVRydWUsIGV4aXN0X29rPVRydWUpCiAgICAgICAgZW52aXJvbm1lbnQgPSBkaWN0KG9zLmVudmlyb24sIERJU1BMQVk9 +ZiI6e251bWJlcn0iLCBDTVVYX0RFU0tUT1BfUlVOVElNRV9ESVI9c3RyKHJ1bnRpbWUpKQogICAgICAgIGVudmlyb25tZW50WyJDSFJPTUVfQ09ORklHX0hP +TUUiXSA9IHN0cihydW50aW1lIC8gImNocm9tZS1jb25maWciKQogICAgICAgIGVudmlyb25tZW50WyJYREdfQ09ORklHX0hPTUUiXSA9IHN0cihydW50aW1l +IC8gInhkZy1jb25maWciKQogICAgICAgIGZvciBrZXkgaW4gKCJOT1RJRllfU09DS0VUIiwgIkRCVVNfU0VTU0lPTl9CVVNfQUREUkVTUyIsICJEQlVTX1NF +U1NJT05fQlVTX1BJRCIsCiAgICAgICAgICAgICAgICAgICAgIkFUX1NQSV9CVVNfQUREUkVTUyIsICJBVF9TUElfQlVTIik6CiAgICAgICAgICAgIGVudmly +b25tZW50LnBvcChrZXksIE5vbmUpCiAgICAgICAgd2hpbGUgbm90IHNlbGYuc2h1dGRvd24uaXNfc2V0KCk6CiAgICAgICAgICAgIHRyeToKICAgICAgICAg +ICAgICAgICMgVGhlIHN1cGVydmlzb3IgaXMgYSBoZWFsdGggY2hlY2ssIG5vdCBhIHRpbWVyLWRyaXZlbiByZXN0YXJ0LgogICAgICAgICAgICAgICAgIyBQ +cmVzZXJ2ZSBhbiBhY3RpdmUgUkZCL25vVk5DIHNlc3Npb24gdW50aWwgaXRzIG93bmVkIGVuZHBvaW50cyBmYWlsLgogICAgICAgICAgICAgICAgaWYgbm90 +IHNlbGYuc2VydmljZV9yZWFkeShudW1iZXIpOgogICAgICAgICAgICAgICAgICAgIHNlbGYuc3RhcnRfY29tcG9uZW50cyhudW1iZXIsIGVudmlyb25tZW50 +LCBydW50aW1lKQogICAgICAgICAgICAgICAgc3RhdGUgPSAicnVubmluZyIgaWYgc2VsZi5zZXJ2aWNlX3JlYWR5KG51bWJlcikgZWxzZSAidW5hdmFpbGFi +bGUiCiAgICAgICAgICAgIGV4Y2VwdCBOb1ZOQ1VuYXZhaWxhYmxlOgogICAgICAgICAgICAgICAgc2VsZi50ZXJtaW5hdGVfd2Vic29ja2lmeShudW1iZXIp +CiAgICAgICAgICAgICAgICBzdGF0ZSA9ICJ1bmF2YWlsYWJsZSIKICAgICAgICAgICAgZXhjZXB0IChPU0Vycm9yLCBSdW50aW1lRXJyb3IsIHN1YnByb2Nl +c3MuU3VicHJvY2Vzc0Vycm9yKToKICAgICAgICAgICAgICAgIHNlbGYudGVybWluYXRlKG51bWJlcikKICAgICAgICAgICAgICAgIHN0YXRlID0gInVuYXZh +aWxhYmxlIgogICAgICAgICAgICB3aXRoIHNlbGYubG9jazoKICAgICAgICAgICAgICAgIHNlbGYuc3RhdGVzW251bWJlcl0gPSBzdGF0ZQogICAgICAgICAg +ICBmaW5pc2hlZC5zZXQoKQogICAgICAgICAgICBzZWxmLnNodXRkb3duLndhaXQoMzApCiAgICAgICAgc2VsZi50ZXJtaW5hdGUobnVtYmVyKQoKICAgIGRl +ZiBzbmFwc2hvdChzZWxmKToKICAgICAgICBwcmltYXJ5X3N0YXRlID0gInJ1bm5pbmciIGlmIHJlYWR5KDEpIGVsc2UgInVuYXZhaWxhYmxlIgogICAgICAg +IHdpdGggc2VsZi5sb2NrOgogICAgICAgICAgICByZXR1cm4geyJ2ZXJzaW9uIjogMSwgImNhbkNyZWF0ZSI6IGxlbihzZWxmLmpvYnMpIDwgTUFYX0RJU1BM +QVlTIC0gMSwKICAgICAgICAgICAgICAgICAgICAiZGlzcGxheXMiOiBbZGVzY3JpcHRvcigxLCBwcmltYXJ5X3N0YXRlKV0KICAgICAgICAgICAgICAgICAg +ICArIFtkZXNjcmlwdG9yKG4sIHNlbGYuc3RhdGVzLmdldChuLCAibGF1bmNoaW5nIikpIGZvciBuIGluIHNlbGYuY2F0YWxvZy5udW1iZXJzKCldfQoKICAg +IGRlZiBoYW5kbGUoc2VsZiwgcmVxdWVzdCk6CiAgICAgICAgaWYgcmVxdWVzdC5nZXQoImFjdGlvbiIpID09ICJsaXN0IjoKICAgICAgICAgICAgcmV0dXJu +IHNlbGYuc25hcHNob3QoKQogICAgICAgIGlmIHJlcXVlc3QuZ2V0KCJhY3Rpb24iKSAhPSAiY3JlYXRlIjoKICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJv +cigidW5zdXBwb3J0ZWQgZGlzcGxheSBhY3Rpb24iKQogICAgICAgIG51bWJlciA9IHNlbGYuY2F0YWxvZy5hbGxvY2F0ZShyZXF1ZXN0LmdldCgicmVxdWVz +dCIpKQogICAgICAgIHNlbGYuc3RhcnQobnVtYmVyKS53YWl0KDU1KQogICAgICAgIHJlc3VsdCA9IHNlbGYuc25hcHNob3QoKQogICAgICAgIHJlc3VsdFsi +Y3JlYXRlZCJdID0gZiJkaXNwbGF5OntudW1iZXJ9IgogICAgICAgIGlmIHNlbGYuc3RhdGVzLmdldChudW1iZXIpICE9ICJydW5uaW5nIjoKICAgICAgICAg +ICAgcmVzdWx0WyJlcnJvciJdID0gImRpc3BsYXlfc3RhcnRfZmFpbGVkIgogICAgICAgIHJldHVybiByZXN1bHQKCgpkZWYgc2VydmUocnVudGltZSk6CiAg +ICBydW50aW1lID0gUGF0aChydW50aW1lKQogICAgcnVudGltZS5ta2Rpcihtb2RlPTBvNzAwLCBwYXJlbnRzPVRydWUsIGV4aXN0X29rPVRydWUpCiAgICB3 +aXRoIChydW50aW1lIC8gImRpc3BsYXktY29udHJvbC5sb2NrIikub3BlbigidyIpIGFzIGxvY2s6CiAgICAgICAgdHJ5OgogICAgICAgICAgICBmY250bC5m +bG9jayhsb2NrLCBmY250bC5MT0NLX0VYIHwgZmNudGwuTE9DS19OQikKICAgICAgICBleGNlcHQgQmxvY2tpbmdJT0Vycm9yOgogICAgICAgICAgICByZXR1 +cm4KICAgICAgICBpZiBub3QgYWxsKHNodXRpbC53aGljaChuYW1lKSBmb3IgbmFtZSBpbiAoIndlYnNvY2tpZnkiLCAib3BlbmJveCIsICJkYnVzLWxhdW5j +aCIpKToKICAgICAgICAgICAgcmFpc2UgVmFsdWVFcnJvcigiZGlzcGxheSBydW50aW1lIHVuYXZhaWxhYmxlIikKICAgICAgICBpZiBub3QgKHNodXRpbC53 +aGljaCgiWHZuYyIpIG9yIHNodXRpbC53aGljaCgiWHRpZ2Vydm5jIikpOgogICAgICAgICAgICByYWlzZSBWYWx1ZUVycm9yKCJkaXNwbGF5IHJ1bnRpbWUg +dW5hdmFpbGFibGUiKQogICAgICAgIHNlcnZpY2UgPSBEaXNwbGF5U2VydmljZShEaXNwbGF5Q2F0YWxvZyhQYXRoLmhvbWUoKSAvICIuY211eCIgLyAiZGlz +cGxheXMiKSwgcnVudGltZSAvICJkaXNwbGF5cyIpCiAgICAgICAgZm9yIG51bWJlciBpbiBzZXJ2aWNlLmNhdGFsb2cubnVtYmVycygpOgogICAgICAgICAg +ICBzZXJ2aWNlLnN0YXJ0KG51bWJlcikKICAgICAgICBwYXRoID0gcnVudGltZSAvICJkaXNwbGF5LWNvbnRyb2wuc29jayIKICAgICAgICBpZiBwYXRoLmV4 +aXN0cygpOgogICAgICAgICAgICBwYXRoLnVubGluaygpCgogICAgICAgIGNsYXNzIEhhbmRsZXIoc29ja2V0c2VydmVyLlN0cmVhbVJlcXVlc3RIYW5kbGVy +KToKICAgICAgICAgICAgZGVmIGhhbmRsZShzZWxmKToKICAgICAgICAgICAgICAgIHNlbGYuY29ubmVjdGlvbi5zZXR0aW1lb3V0KDYwKQogICAgICAgICAg +ICAgICAgdHJ5OgogICAgICAgICAgICAgICAgICAgIHJhdyA9IHNlbGYucmZpbGUucmVhZGxpbmUoNDA5NykKICAgICAgICAgICAgICAgICAgICBpZiBsZW4o +cmF3KSA+IDQwOTY6CiAgICAgICAgICAgICAgICAgICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoImRpc3BsYXkgcmVxdWVzdCB0b28gbGFyZ2UiKQogICAgICAg +ICAgICAgICAgICAgIHJlc3VsdCA9IHNlcnZpY2UuaGFuZGxlKGpzb24ubG9hZHMocmF3KSkKICAgICAgICAgICAgICAgIGV4Y2VwdCAoVmFsdWVFcnJvciwg +S2V5RXJyb3IsIFR5cGVFcnJvciwgT1NFcnJvcik6CiAgICAgICAgICAgICAgICAgICAgcmVzdWx0ID0geyJlcnJvciI6ICJkaXNwbGF5X3JlcXVlc3RfZmFp +bGVkIn0KICAgICAgICAgICAgICAgIHRyeToKICAgICAgICAgICAgICAgICAgICBzZWxmLndmaWxlLndyaXRlKGpzb24uZHVtcHMocmVzdWx0KS5lbmNvZGUo +KSArIGIiXG4iKQogICAgICAgICAgICAgICAgZXhjZXB0IE9TRXJyb3I6CiAgICAgICAgICAgICAgICAgICAgcGFzcyAgIyBUaGUgcGVyc2lzdGVkIHJlY2Vp +cHQgcmVtYWlucyByZXBsYXlhYmxlIGFmdGVyIGRpc2Nvbm5lY3QuCgogICAgICAgIGNsYXNzIFNlcnZlcihzb2NrZXRzZXJ2ZXIuVGhyZWFkaW5nVW5peFN0 +cmVhbVNlcnZlcik6CiAgICAgICAgICAgIGRhZW1vbl90aHJlYWRzID0gVHJ1ZQoKICAgICAgICBwcmV2aW91c191bWFzayA9IG9zLnVtYXNrKDBvMDc3KQog +ICAgICAgIHRyeToKICAgICAgICAgICAgd2l0aCBTZXJ2ZXIoc3RyKHBhdGgpLCBIYW5kbGVyKSBhcyBzZXJ2ZXI6CiAgICAgICAgICAgICAgICBvcy51bWFz +ayhwcmV2aW91c191bWFzaykKICAgICAgICAgICAgICAgIHNlcnZlci5zZXJ2ZV9mb3JldmVyKCkKICAgICAgICBmaW5hbGx5OgogICAgICAgICAgICBvcy51 +bWFzayhwcmV2aW91c191bWFzaykKICAgICAgICAgICAgc2VydmljZS5zaHV0ZG93bi5zZXQoKQogICAgICAgICAgICBwYXRoLnVubGluayhtaXNzaW5nX29r +PVRydWUpCgoKZGVmIG1haW4oKToKICAgIHBhcnNlciA9IGFyZ3BhcnNlLkFyZ3VtZW50UGFyc2VyKGRlc2NyaXB0aW9uPV9fZG9jX18pCiAgICBwYXJzZXIu +YWRkX2FyZ3VtZW50KCJhY3Rpb24iLCBjaG9pY2VzPVsibGlzdCIsICJjcmVhdGUiLCAic2VydmUiXSkKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoIi0tcmVx +dWVzdC1pZCIpCiAgICBhcmdzID0gcGFyc2VyLnBhcnNlX2FyZ3MoKQogICAgcnVudGltZSA9IG9zLmVudmlyb24uZ2V0KCJDTVVYX0RFU0tUT1BfUlVOVElN +RV9ESVIiLCAiL3J1bi9jbXV4LWRlc2t0b3AiKQogICAgaWYgYXJncy5hY3Rpb24gPT0gInNlcnZlIjoKICAgICAgICBzZXJ2ZShydW50aW1lKQogICAgICAg +IHJldHVybiAwCiAgICBpZiBhcmdzLmFjdGlvbiA9PSAiY3JlYXRlIiBhbmQgbm90IGFyZ3MucmVxdWVzdF9pZDoKICAgICAgICBwYXJzZXIuZXJyb3IoImNy +ZWF0ZSByZXF1aXJlcyAtLXJlcXVlc3QtaWQgKHJldGFpbiBpdCB3aGVuIHJldHJ5aW5nKSIpCiAgICB0cnk6CiAgICAgICAgd2l0aCBzb2NrZXQuc29ja2V0 +KHNvY2tldC5BRl9VTklYLCBzb2NrZXQuU09DS19TVFJFQU0pIGFzIGNsaWVudDoKICAgICAgICAgICAgY2xpZW50LnNldHRpbWVvdXQoNjApCiAgICAgICAg +ICAgIGNsaWVudC5jb25uZWN0KHN0cihQYXRoKHJ1bnRpbWUpIC8gImRpc3BsYXktY29udHJvbC5zb2NrIikpCiAgICAgICAgICAgIGNsaWVudC5zZW5kYWxs +KGpzb24uZHVtcHMoeyJhY3Rpb24iOiBhcmdzLmFjdGlvbiwgInJlcXVlc3QiOiBhcmdzLnJlcXVlc3RfaWR9KS5lbmNvZGUoKSArIGIiXG4iKQogICAgICAg +ICAgICB3aXRoIGNsaWVudC5tYWtlZmlsZSgicmIiKSBhcyBzdHJlYW06CiAgICAgICAgICAgICAgICByZXN1bHQgPSBqc29uLmxvYWRzKHN0cmVhbS5yZWFk +bGluZSg2NTUzNikpCiAgICBleGNlcHQgKE9TRXJyb3IsIFZhbHVlRXJyb3IpOgogICAgICAgIHJlc3VsdCA9IHsiZXJyb3IiOiAiZGlzcGxheV9ydW50aW1l +X3VuYXZhaWxhYmxlIn0KICAgIHByaW50KGpzb24uZHVtcHMocmVzdWx0KSkKICAgIHJldHVybiAxIGlmICJlcnJvciIgaW4gcmVzdWx0IGVsc2UgMAoKCmlm +IF9fbmFtZV9fID09ICJfX21haW5fXyI6CiAgICByYWlzZSBTeXN0ZW1FeGl0KG1haW4oKSkK """ private static let path = "$HOME/.cmux/cmux-display" diff --git a/Sources/DockSplitStore+BrowserActions.swift b/Sources/DockSplitStore+BrowserActions.swift index c6b36dad073a..55ddcdacd732 100644 --- a/Sources/DockSplitStore+BrowserActions.swift +++ b/Sources/DockSplitStore+BrowserActions.swift @@ -50,8 +50,9 @@ extension DockSplitStore { remoteWorkspaceID: record?.remoteWorkspaceID)], workspaceID: workspaceId) if let model = browser.cloudAccess.model, let url = browser.cloudAccess.remoteURL { duplicatedPanel.prepareCloudBrowserStore(machineID: resource.machine.rawValue) - duplicatedPanel.cloudAccess.configure(model: model, url: url, resourceID: resource) - duplicatedPanel.showCloudAddress(url) + let configuredURL = browser.cloudRestoreURL(on: url) + duplicatedPanel.cloudAccess.configure(model: model, url: configuredURL, resourceID: resource) + duplicatedPanel.showCloudAddress(configuredURL) model.connect() } else { duplicatedPanel.restoreCloudResource(resource, preferredURL: browser.currentURLForTabDuplication) diff --git a/Sources/Surfaces/Workspace+BrowserDuplication.swift b/Sources/Surfaces/Workspace+BrowserDuplication.swift index c4730f9ec6a4..5801bbb0b35a 100644 --- a/Sources/Surfaces/Workspace+BrowserDuplication.swift +++ b/Sources/Surfaces/Workspace+BrowserDuplication.swift @@ -33,8 +33,9 @@ extension Workspace { remoteWorkspaceID: record?.remoteWorkspaceID)], workspaceID: id) if let model = browser.cloudAccess.model, let url = browser.cloudAccess.remoteURL { newPanel.prepareCloudBrowserStore(machineID: resource.machine.rawValue) - newPanel.cloudAccess.configure(model: model, url: url, resourceID: resource) - newPanel.showCloudAddress(url) + let configuredURL = browser.cloudRestoreURL(on: url) + newPanel.cloudAccess.configure(model: model, url: configuredURL, resourceID: resource) + newPanel.showCloudAddress(configuredURL) model.connect() } else { newPanel.restoreCloudResource(resource, preferredURL: browser.currentURLForTabDuplication) diff --git a/tests/fixtures/cmux-display b/tests/fixtures/cmux-display index 54746769dcd5..e137544ffd18 100755 --- a/tests/fixtures/cmux-display +++ b/tests/fixtures/cmux-display @@ -214,11 +214,14 @@ class DisplayService: @staticmethod def wait_for_port(port, timeout=20): - try: - with socket.create_connection(("127.0.0.1", port), timeout=timeout): - return True - except OSError: - return False + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + try: + with socket.create_connection(("127.0.0.1", port), timeout=0.5): + return True + except OSError: + time.sleep(0.1) + return False @staticmethod def process_alive(process): From 75fdecb3bf267d48a7512ee0607e79f9c75a90b7 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 20:31:16 -0700 Subject: [PATCH 56/64] fix: defer Cloud activation for hidden restores --- Sources/Panels/BrowserPanel+CloudConnection.swift | 4 +++- Sources/Panels/BrowserPanel.swift | 6 +++++- tests/test_cloud_display_catalog.py | 4 +++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Sources/Panels/BrowserPanel+CloudConnection.swift b/Sources/Panels/BrowserPanel+CloudConnection.swift index cdb8f35014a4..d01297363b3b 100644 --- a/Sources/Panels/BrowserPanel+CloudConnection.swift +++ b/Sources/Panels/BrowserPanel+CloudConnection.swift @@ -36,7 +36,8 @@ extension BrowserPanel { /// Restore by stable resource identity before loading any saved address. /// A stale/unknown provider leaves an owned placeholder, never a local page. - func restoreCloudResource(_ resource: SurfaceResourceID, preferredURL: URL? = nil) { + func restoreCloudResource(_ resource: SurfaceResourceID, preferredURL: URL? = nil, + activate: Bool = true) { pendingCloudRestoreURL = preferredURL let catalog = SurfaceCatalog.shared let isGlobalDock = DockSplitStore.liveStore(containingPanel: id)?.scope == .global @@ -49,6 +50,7 @@ extension BrowserPanel { cloudAccess.retainResource(resource) retainTransferredSurfaceMachine(resource.machine) catalog.restore([SurfaceProjectionRecord(panelID: id, resource: resource)], workspaceID: workspaceId) + guard activate else { return } guard let provider = catalog.provider(for: resource.machine) as? CmuxTuiSurfaceProvider, let known = catalog.resources[resource] else { cloudAccess.showUnavailable(String(localized: "cloud.display.restoreUnavailable", defaultValue: "This Cloud display or browser is unavailable. Refresh its machine to reconnect.")) diff --git a/Sources/Panels/BrowserPanel.swift b/Sources/Panels/BrowserPanel.swift index 5492adf4b553..369c81ca10b8 100644 --- a/Sources/Panels/BrowserPanel.swift +++ b/Sources/Panels/BrowserPanel.swift @@ -4532,7 +4532,11 @@ final class BrowserPanel: Panel, ObservableObject { ) currentURL = restoredURL - if let resource = snapshot.cloudResource { restoreCloudResource(resource, preferredURL: restoredURL); return } + if let resource = snapshot.cloudResource { + restoreCloudResource(resource, preferredURL: restoredURL, activate: shouldRenderRestoredWebView) + if !shouldRenderRestoredWebView { shouldRenderWebView = false; refreshNavigationAvailability() } + return + } guard shouldRenderRestoredWebView, let restoredURL else { shouldRenderWebView = false refreshNavigationAvailability() diff --git a/tests/test_cloud_display_catalog.py b/tests/test_cloud_display_catalog.py index 18589a5a9d85..fc8f0cf031db 100644 --- a/tests/test_cloud_display_catalog.py +++ b/tests/test_cloud_display_catalog.py @@ -228,7 +228,9 @@ def test_additional_desktop_clients_use_display_scoped_process_names(self): def test_start_failure_retains_resource_and_replay_receipt(self): service = display.DisplayService(self.catalog(), self.root / "runtime") request = str(uuid.uuid4()) - with mock.patch.object(display.subprocess, "run", side_effect=OSError("starter unavailable")), mock.patch.object(display, "ready", return_value=False): + with mock.patch.object(display.subprocess, "run", side_effect=OSError("starter unavailable")), \ + mock.patch.object(display.shutil, "which", return_value=None), \ + mock.patch.object(display, "ready", return_value=False): try: result = service.handle({"action": "create", "request": request}) retried = service.handle({"action": "create", "request": request}) From 42f1ab4a076daefbb396ae3501fd66fa34688ab4 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 20:59:54 -0700 Subject: [PATCH 57/64] fix: preserve ownership checks across latest main merge --- Sources/Surfaces/SurfaceCatalog.swift | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/Sources/Surfaces/SurfaceCatalog.swift b/Sources/Surfaces/SurfaceCatalog.swift index 716f301fce80..134c903d4303 100644 --- a/Sources/Surfaces/SurfaceCatalog.swift +++ b/Sources/Surfaces/SurfaceCatalog.swift @@ -1,6 +1,5 @@ import Foundation import Observation - /// The single owner of surface identities and projections on this Mac. /// /// Rules that hold by construction: @@ -13,9 +12,7 @@ import Observation @Observable final class SurfaceCatalog { private typealias MaterializationKey = SurfaceProjectionMaterialization.Key - static let shared = SurfaceCatalog(sidebarOrganization: CloudSidebarOrganizationStore(defaults: .standard)) - /// A provider call with no remaining caller must not occupy a resource forever when the /// provider ignores task cancellation. The deadline starts only after the last caller /// detaches, so a slow but observed materialization is still allowed to finish normally. @@ -26,9 +23,7 @@ final class SurfaceCatalog { /// while cancellation is unresolved. This prevents one unhealthy machine from blocking /// unrelated machines while also bounding repeated provider replacements. nonisolated static let defaultMaximumTrackedMaterializations = 16 - static let didChangeNotification = Notification.Name("cmux.surfaces.didChange") - private(set) var machines: [SurfaceMachineID: SurfaceMachineInfo] = [:] private(set) var resources: [SurfaceResourceID: SurfaceResource] = [:] private struct CloudProjectionKey: Hashable { let panelID: UUID; let workspaceID: UUID } @@ -39,7 +34,6 @@ final class SurfaceCatalog { /// Resource IDs grouped by machine so providers can answer presence checks /// without sorting the full catalog snapshot on every refresh. private(set) var resourceIDsByMachine: [SurfaceMachineID: Set] = [:] - /// Accepted revisioned graphs shared by providers, socket, and agent callers. /// Whether each retained graph was observed on a live link. This is separate /// from `CloudVMState` because freshness is local observation metadata, not @@ -83,7 +77,6 @@ final class SurfaceCatalog { private let materializationClock: any Clock private var projectionEndReasons: [UUID: SurfaceProjectionEndReason] = [:] var pendingRestoredProjections = SurfaceProjectionRestoreStore() - /// Focus/select behavior the app uses to bring an existing projection forward. var focusProjection: ((SurfaceProjection) -> Void)? @@ -1322,6 +1315,10 @@ final class SurfaceCatalog { /// after the link reconnects); local resources are re-registered by the local provider /// with the same panel-derived key, so they resolve immediately. func restore(_ records: [SurfaceProjectionRecord], workspaceID: UUID) { + for record in records where DockSplitStore.liveStore(containingPanel: record.panelID)?.scope != .global { + do { try validateOwnership(of: [record.resource], at: .workspace(id: workspaceID, placement: .tab)) } + catch { return } + } for record in records { if resources[record.resource] != nil { pendingRestoredProjections.remove(panelID: record.panelID) @@ -1385,7 +1382,8 @@ final class SurfaceCatalog { var resolvedWorkspaceIDs = Set() let resolved = pendingRestoredProjections.takeResolvable( machine: machine, - availableResources: Set(resources.keys) + availableResources: Set(resources.keys), + isAllowed: canRestoreProjection ) for projection in resolved { insertSupersedingLocalPlaceholder(projection) From eb39f9ec9b885ddf5d039df4f7ee121a5830eaab Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 21:21:27 -0700 Subject: [PATCH 58/64] fix: remove duplicate projection query declarations --- .../SurfaceCatalog+ProjectionQueries.swift | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/Sources/Surfaces/SurfaceCatalog+ProjectionQueries.swift b/Sources/Surfaces/SurfaceCatalog+ProjectionQueries.swift index abbd5a16af95..eab4485ff2f8 100644 --- a/Sources/Surfaces/SurfaceCatalog+ProjectionQueries.swift +++ b/Sources/Surfaces/SurfaceCatalog+ProjectionQueries.swift @@ -20,28 +20,6 @@ extension SurfaceCatalog { func projectedTerminalIDs(on machine: SurfaceMachineID) -> [String] { Array(Set(projections.lazy.filter { $0.resource.machine == machine && $0.resource.kind == .terminal }.map { $0.resource.key })).sorted() } - func projectionRecords(forWorkspace workspaceID: UUID) -> [SurfaceProjectionRecord] { - var records = projections - .filter { $0.workspaceID == workspaceID } - .map { - SurfaceProjectionRecord( - panelID: $0.panelID, - resource: $0.resource, - remoteWorkspaceID: $0.remoteWorkspaceID, - remoteTabID: $0.remoteTabID - ) - } - pendingRestoredProjections.mergeRecords(into: &records, for: workspaceID) - return records.sorted { $0.panelID.uuidString < $1.panelID.uuidString } - } - - /// Cloud machine IDs referenced by restored panes that are waiting for a - /// provider to report their resources. The registry uses these IDs during - /// stale-machine reconciliation so a deleted ID cannot attach old panes - /// when a different machine later receives the same ID. - var pendingRestoredMachineIDs: Set { - Set(pendingRestoredProjections.machineIDs.compactMap { $0.cloudMachineID }) - } } From 23c807b50ab406e508de46c07b5e22f7df7c1ad9 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sun, 20 Sep 2026 21:25:12 -0700 Subject: [PATCH 59/64] fix: restore provider display lifecycle after main merge --- .../PortForward/CloudBrowserAccessView.swift | 2 +- .../Surfaces/CmuxTuiSurfaceProviders.swift | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Sources/Cloud/PortForward/CloudBrowserAccessView.swift b/Sources/Cloud/PortForward/CloudBrowserAccessView.swift index 1e8ec49d8198..57aaf3150d88 100644 --- a/Sources/Cloud/PortForward/CloudBrowserAccessView.swift +++ b/Sources/Cloud/PortForward/CloudBrowserAccessView.swift @@ -31,7 +31,7 @@ struct CloudBrowserAccessView: View { } else { CloudBrowserConnectionCard( address: state.remoteURL?.absoluteString ?? "", - message: state.error ?? model.failureMessage, + message: state.failureMessage ?? model.failureMessage, onRetry: { _ = panel.reload() navigateIfReady() diff --git a/Sources/Surfaces/CmuxTuiSurfaceProviders.swift b/Sources/Surfaces/CmuxTuiSurfaceProviders.swift index 1c007e3ea4c8..545a6573b853 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProviders.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProviders.swift @@ -27,6 +27,7 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { /// when the build has no hub. Owned by the registry, shared by every provider. let portForwards: CloudHubPortForwarder? let portAccessStore: CloudPortAccessStore + let displayCoordinator: CloudDisplayCoordinator let browserPolicy: @MainActor () -> BrowserURLAllowlistPolicy /// Invalidates suspended work when this provider is stopped or replaced. var isFeatureSuspended = false @@ -127,6 +128,10 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { self.catalog = catalog self.portForwards = portForwards self.portAccessStore = portAccessStore ?? CloudPortAccessStore() + self.displayCoordinator = CloudDisplayCoordinator { command, timeout in + guard let client = VMClient.shared else { throw ProviderError.notSignedIn } + return try await client.exec(id: summary.id, command: command, timeoutMs: timeout) + } self.browserPolicy = browserPolicy info = Self.info(from: summary, linkState: summary.status == "running" ? .connecting : .asleep, linkError: nil, stats: nil) installNotificationSync() @@ -145,6 +150,10 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { let previousPrivateAddress = info.privateAddress refreshGeneration &+= 1 refreshCoordinator.invalidate() + if self.summary.id != summary.id || self.summary.provider != summary.provider + || self.summary.image != summary.image || self.summary.resolvedKind != summary.resolvedKind { + displayCoordinator.invalidate() + } self.summary = summary if !supportsPortPreviews { portsCache = nil @@ -170,6 +179,7 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { } func suspendForFeatureFlag() { isFeatureSuspended = true + displayCoordinator.stop() guestURLService?.stop() guestURLService = nil lifecycleGeneration &+= 1 @@ -411,7 +421,6 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { reprojectRestoredPanes(generation: lifecycle) return snapshotEstablishedCurrentGraph } - @discardableResult func installSnapshotIfNewer(_ incoming: CloudVMState, requestVersion: UInt64? = nil) -> Bool { guard acceptsIncomingGeneration(incoming.cursor) else { @@ -470,7 +479,6 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { return false } } - /// A daemon generation is opaque, but this provider remembers every /// generation accepted by the current link lifetime. A response carrying a /// previously seen generation after another generation was accepted is an @@ -486,7 +494,6 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { case .rejectStale: return false } } - /// Checks all in-flight rename receipts before a graph becomes visible. /// Rejecting the whole graph keeps unrelated rows from being published with /// a target row known to be stale at the same cursor. @@ -512,7 +519,6 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { } return true } - /// Retires receipts only after an accepted graph proves that the daemon has /// reached them. A later same-generation cursor belongs to the canonical /// remote writer, even if it changed the requested name again. @@ -545,7 +551,6 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { pendingRemoteRenames.removeValue(forKey: key) } } - /// Publishes the authoritative graph and every derived row in one catalog /// transaction. Display and forwarded-port rows are machine capabilities, so /// they join the daemon graph here without becoming a second session state. @@ -583,7 +588,6 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { } closePanesForVanishedRemoteTerminals(observation: observation) } - func publishDelta( _ state: CloudVMState, impact: CloudVMStateDeltaImpact, @@ -595,7 +599,6 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { publish(state, ports: ports, reconcileTitles: reconcileTitles) return } - var affected = impact.resourceIDs affected.formUnion(pendingRemoteCreations.keys) var resources = CmuxTuiSnapshotParser.resources(from: state, matching: affected) @@ -632,7 +635,6 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { } closePanesForVanishedRemoteTerminals(observation: .current) } - /// Closes the panes of terminals the resolver reported as exited. The /// graph-driven sweep covers the usual case; this covers a daemon that /// still lists an exited terminal because a stale tab row survives it. @@ -642,7 +644,6 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { closeManualMirrorPane(panelID: panelID, terminalID: session.terminalID) } } - private func closeManualMirrorPane(panelID: UUID, terminalID: String) { restoredAttachTasks.removeValue(forKey: panelID)?.cancel() materializedPanels.remove(panelID) @@ -650,7 +651,6 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { guard let workspace = AppDelegate.shared?.workspace(containingSurfaceID: panelID) else { return } SurfacePaneFactory.closeExited(panelID: panelID, in: workspace.id) } - /// Closes attach panes whose remote terminal is gone from the accepted /// graph. /// From 1a127d21a427f384e2a017d8d44403bb25aabc66 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Mon, 21 Sep 2026 00:42:19 -0700 Subject: [PATCH 60/64] fix: use merged hostname route API --- Sources/Surfaces/CmuxTuiSurfaceProvider+Displays.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Surfaces/CmuxTuiSurfaceProvider+Displays.swift b/Sources/Surfaces/CmuxTuiSurfaceProvider+Displays.swift index ce03e006b674..ad5cfebfbe62 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProvider+Displays.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProvider+Displays.swift @@ -61,7 +61,7 @@ extension CmuxTuiSurfaceProvider { /// The noVNC URL retains each display's own port across VM reconnects. nonisolated static func privateDesktopURL(privateAddress: String, port: Int = CmuxTuiSnapshotParser.desktopPort) -> String { - let base = CmuxInternalHostnames.directPortURL(privateAddress: privateAddress, port: port) + let base = CmuxInternalHostnames().directPortURL(privateAddress: privateAddress, port: port) return "\(base)/vnc.html?path=websockify&autoconnect=1&resize=remote&reconnect=1&reconnect_delay=2000" } } From e18669f557703e5b9fd833ff9f53b23f12ec7b49 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Mon, 21 Sep 2026 01:46:14 -0700 Subject: [PATCH 61/64] fix: restore New Display hover button after main merge The latest origin/main merge moved CloudTreeRowHoverButtons into its own file, and the conflict resolution kept main's copy, which dropped the displays-pool New Display button and its hasButtons entry. Re-apply them in the new file, and restore the blank lines the resolution stripped from SurfaceCatalog.swift so the PR diff stays limited to behavior changes. Co-Authored-By: Claude Fable 5.1 --- Sources/Cloud/CloudTreeRowContentView.swift | 1 + Sources/Cloud/CloudTreeRowHoverButtons.swift | 10 +++++++--- Sources/Surfaces/SurfaceCatalog.swift | 7 +++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Sources/Cloud/CloudTreeRowContentView.swift b/Sources/Cloud/CloudTreeRowContentView.swift index 3c1cdd29421c..60f440031860 100644 --- a/Sources/Cloud/CloudTreeRowContentView.swift +++ b/Sources/Cloud/CloudTreeRowContentView.swift @@ -392,3 +392,4 @@ enum CloudTreeBrowserDetail { return row.workspaceTitle } } + diff --git a/Sources/Cloud/CloudTreeRowHoverButtons.swift b/Sources/Cloud/CloudTreeRowHoverButtons.swift index a6107c0e7aae..179e5e1ffab6 100644 --- a/Sources/Cloud/CloudTreeRowHoverButtons.swift +++ b/Sources/Cloud/CloudTreeRowHoverButtons.swift @@ -74,8 +74,12 @@ struct CloudTreeRowHoverButtons: View { plus(String(localized: "cloudTree.menu.newTerminal", defaultValue: "New Terminal")) { nodeActions.newTerminal(machine, nil) } - case .displaysPool: - EmptyView() + case .displaysPool(let machine, _, let canCreate): + plus(String(localized: "cloudTree.menu.newDisplay", defaultValue: "New Display")) { + nodeActions.newDisplay(machine) + } + .disabled(!canCreate) + .help(canCreate ? String(localized: "cloudTree.menu.newDisplay", defaultValue: "New Display") : CloudGuestDisplaySnapshot.unavailableMessage) case .workspacesGroup(let machine): plus(String(localized: "cloudTree.menu.newWorkspace", defaultValue: "New Workspace")) { nodeActions.newWorkspace(machine) @@ -105,7 +109,7 @@ struct CloudTreeRowHoverButtons: View { /// True when this row kind renders any hover button at all. static func hasButtons(for kind: CloudTreeNode.Kind) -> Bool { switch kind { - case .machine, .localMachine, .terminalsPool, .workspacesGroup, .workspace, .devicesSection, .cloudMachinesSection: + case .machine, .localMachine, .terminalsPool, .displaysPool, .workspacesGroup, .workspace, .devicesSection, .cloudMachinesSection: return true case .pendingMachine: return true diff --git a/Sources/Surfaces/SurfaceCatalog.swift b/Sources/Surfaces/SurfaceCatalog.swift index 7f3c9290e9f9..3f27f98c037f 100644 --- a/Sources/Surfaces/SurfaceCatalog.swift +++ b/Sources/Surfaces/SurfaceCatalog.swift @@ -1,5 +1,6 @@ import Foundation import Observation + /// The single owner of surface identities and projections on this Mac. /// /// Rules that hold by construction: @@ -12,7 +13,9 @@ import Observation @Observable final class SurfaceCatalog { private typealias MaterializationKey = SurfaceProjectionMaterialization.Key + static let shared = SurfaceCatalog(sidebarOrganization: CloudSidebarOrganizationStore(defaults: .standard)) + /// A provider call with no remaining caller must not occupy a resource forever when the /// provider ignores task cancellation. The deadline starts only after the last caller /// detaches, so a slow but observed materialization is still allowed to finish normally. @@ -23,7 +26,9 @@ final class SurfaceCatalog { /// while cancellation is unresolved. This prevents one unhealthy machine from blocking /// unrelated machines while also bounding repeated provider replacements. nonisolated static let defaultMaximumTrackedMaterializations = 16 + static let didChangeNotification = Notification.Name("cmux.surfaces.didChange") + private(set) var machines: [SurfaceMachineID: SurfaceMachineInfo] = [:] private(set) var resources: [SurfaceResourceID: SurfaceResource] = [:] private struct CloudProjectionKey: Hashable { let panelID: UUID; let workspaceID: UUID } @@ -34,6 +39,7 @@ final class SurfaceCatalog { /// Resource IDs grouped by machine so providers can answer presence checks /// without sorting the full catalog snapshot on every refresh. private(set) var resourceIDsByMachine: [SurfaceMachineID: Set] = [:] + /// Accepted revisioned graphs shared by providers, socket, and agent callers. /// Whether each retained graph was observed on a live link. This is separate /// from `CloudVMState` because freshness is local observation metadata, not @@ -77,6 +83,7 @@ final class SurfaceCatalog { private let materializationClock: any Clock private var projectionEndReasons: [UUID: SurfaceProjectionEndReason] = [:] var pendingRestoredProjections = SurfaceProjectionRestoreStore() + /// Focus/select behavior the app uses to bring an existing projection forward. var focusProjection: ((SurfaceProjection) -> Void)? From 1eda271279340895ee7cab17c1f096767526c513 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Mon, 21 Sep 2026 01:48:18 -0700 Subject: [PATCH 62/64] fix: keep SurfaceCatalog within its line budget The blank lines restored in the previous commit put the file seven lines over the Swift file-length budget, so drop them again. Co-Authored-By: Claude Fable 5.1 --- Sources/Surfaces/SurfaceCatalog.swift | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Sources/Surfaces/SurfaceCatalog.swift b/Sources/Surfaces/SurfaceCatalog.swift index 3f27f98c037f..7f3c9290e9f9 100644 --- a/Sources/Surfaces/SurfaceCatalog.swift +++ b/Sources/Surfaces/SurfaceCatalog.swift @@ -1,6 +1,5 @@ import Foundation import Observation - /// The single owner of surface identities and projections on this Mac. /// /// Rules that hold by construction: @@ -13,9 +12,7 @@ import Observation @Observable final class SurfaceCatalog { private typealias MaterializationKey = SurfaceProjectionMaterialization.Key - static let shared = SurfaceCatalog(sidebarOrganization: CloudSidebarOrganizationStore(defaults: .standard)) - /// A provider call with no remaining caller must not occupy a resource forever when the /// provider ignores task cancellation. The deadline starts only after the last caller /// detaches, so a slow but observed materialization is still allowed to finish normally. @@ -26,9 +23,7 @@ final class SurfaceCatalog { /// while cancellation is unresolved. This prevents one unhealthy machine from blocking /// unrelated machines while also bounding repeated provider replacements. nonisolated static let defaultMaximumTrackedMaterializations = 16 - static let didChangeNotification = Notification.Name("cmux.surfaces.didChange") - private(set) var machines: [SurfaceMachineID: SurfaceMachineInfo] = [:] private(set) var resources: [SurfaceResourceID: SurfaceResource] = [:] private struct CloudProjectionKey: Hashable { let panelID: UUID; let workspaceID: UUID } @@ -39,7 +34,6 @@ final class SurfaceCatalog { /// Resource IDs grouped by machine so providers can answer presence checks /// without sorting the full catalog snapshot on every refresh. private(set) var resourceIDsByMachine: [SurfaceMachineID: Set] = [:] - /// Accepted revisioned graphs shared by providers, socket, and agent callers. /// Whether each retained graph was observed on a live link. This is separate /// from `CloudVMState` because freshness is local observation metadata, not @@ -83,7 +77,6 @@ final class SurfaceCatalog { private let materializationClock: any Clock private var projectionEndReasons: [UUID: SurfaceProjectionEndReason] = [:] var pendingRestoredProjections = SurfaceProjectionRestoreStore() - /// Focus/select behavior the app uses to bring an existing projection forward. var focusProjection: ((SurfaceProjection) -> Void)? From 345e9efb2ca44af0288b4db52fe629eaa09082df Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Mon, 21 Sep 2026 18:56:18 -0700 Subject: [PATCH 63/64] fix: route cloud desktop clicks through portal --- Sources/BrowserWindowPortal.swift | 10 ++++++- .../Surfaces/CmuxTuiSurfaceProviders.swift | 26 ++++++++++++++----- .../Surfaces/SurfaceCatalog+CloudPorts.swift | 11 ++++++-- cmuxTests/CmuxTuiSurfaceProviderTests.swift | 3 +++ 4 files changed, 40 insertions(+), 10 deletions(-) diff --git a/Sources/BrowserWindowPortal.swift b/Sources/BrowserWindowPortal.swift index 89fead3a04f3..ee99de36715b 100644 --- a/Sources/BrowserWindowPortal.swift +++ b/Sources/BrowserWindowPortal.swift @@ -4027,7 +4027,15 @@ final class WindowBrowserPortal: NSObject { for subview in hostView.subviews.reversed() { guard let container = subview as? WindowBrowserSlotView else { continue } guard !container.isHidden else { continue } - guard container.frame.contains(point) else { continue } + // The portal host can be reparented during a split/workspace handoff. + // In that interval its local frame and the window-space frame may be + // expressed in different coordinate systems even though the WebView + // is visibly rendering. The first-responder guard uses this lookup to + // admit a real pointer click; relying only on `container.frame` then + // drops clicks into a visible VNC page while the terminal keeps focus. + let containsPoint = container.frame.contains(point) + || container.convert(container.bounds, to: nil).contains(windowPoint) + guard containsPoint else { continue } guard let webView = entriesByWebViewId .first(where: { _, entry in entry.containerView === container })? .value diff --git a/Sources/Surfaces/CmuxTuiSurfaceProviders.swift b/Sources/Surfaces/CmuxTuiSurfaceProviders.swift index 545a6573b853..7b53869a73ff 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProviders.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProviders.swift @@ -259,7 +259,8 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { machine: machine, scannedPorts: scannedPorts, previousResources: previousResources, - privateAddress: summary.preferredPrivateAddress + privateAddress: summary.preferredPrivateAddress, + displayPortsOwned: hasDesktop ) resources = resourcesWithPendingCreations(parsed, state: cloudState) } else { @@ -268,7 +269,8 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { machine: machine, scannedPorts: scannedPorts, previousResources: previousResources, - privateAddress: summary.preferredPrivateAddress + privateAddress: summary.preferredPrivateAddress, + displayPortsOwned: hasDesktop )) appendMissingResources(preservedNonPortResources, to: &fallback) resources = resourcesWithPendingCreations(fallback, state: nil) @@ -310,7 +312,8 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { socketPath: connected.socketPath, force: force, generation: generation, - privateAddress: privateAddress + privateAddress: privateAddress, + displayPortsOwned: hasDesktop ) async let snapshotData = link.run(arguments: CloudTuiRequests.snapshotArguments(socketPath: connected.socketPath)) if let refreshedPorts = await refreshedPorts { @@ -727,7 +730,8 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { machine: machine, scannedPorts: ports, previousResources: catalog.authoritativeSnapshot.resources(on: machine), - privateAddress: summary.preferredPrivateAddress + privateAddress: summary.preferredPrivateAddress, + displayPortsOwned: summary.resolvedKind.hasDesktop ) } @@ -1337,10 +1341,14 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { socketPath: String, force: Bool, generation: UInt64, - privateAddress: String? + privateAddress: String?, + displayPortsOwned: Bool ) async -> [Int]? { if !force, let cached = portsCache, Date.now.timeIntervalSince(cached.at) < portsTTL { - return cached.ports + return cached.ports.filter { + !CmuxTuiSnapshotParser.internalPorts.contains($0) + && (!displayPortsOwned || !CmuxTuiSnapshotParser.displayPorts.contains($0)) + } } guard let arguments = CloudTuiRequests.listeningPortsArguments(socketPath: socketPath), let data = try? await link.run(arguments: arguments), @@ -1349,7 +1357,11 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { return nil } let result = VMExecResult(exitCode: 0, stdout: stdout, stderr: "") - guard let ports = Self.ports(from: result, privateAddress: privateAddress) else { return nil } + guard let ports = Self.ports( + from: result, + privateAddress: privateAddress, + displayPortsOwned: displayPortsOwned + ) else { return nil } guard generation == refreshGeneration else { return nil } portsCache = (ports, Date.now) return ports diff --git a/Sources/Surfaces/SurfaceCatalog+CloudPorts.swift b/Sources/Surfaces/SurfaceCatalog+CloudPorts.swift index cd73842e3362..a1b016c9b438 100644 --- a/Sources/Surfaces/SurfaceCatalog+CloudPorts.swift +++ b/Sources/Surfaces/SurfaceCatalog+CloudPorts.swift @@ -267,13 +267,20 @@ extension CmuxTuiSurfaceProvider { /// Converts one port-probe result into a complete scan. A non-zero exit is /// incomplete (the command or transport was unavailable); a successful /// header-only listing is authoritative and intentionally returns `[]`. - nonisolated static func ports(from result: VMExecResult, privateAddress: String? = nil) -> [Int]? { + nonisolated static func ports( + from result: VMExecResult, + privateAddress: String? = nil, + displayPortsOwned: Bool = false + ) -> [Int]? { guard result.exitCode == 0 else { return nil } return CmuxTuiSnapshotParser.reachableListeningPorts( fromSocketListing: result.stdout, privateAddress: privateAddress ) - .filter { !CmuxTuiSnapshotParser.internalPorts.contains($0) } + .filter { + !CmuxTuiSnapshotParser.internalPorts.contains($0) + && (!displayPortsOwned || !CmuxTuiSnapshotParser.displayPorts.contains($0)) + } } /// Reconciles one machine's port scan with its prior catalog values. diff --git a/cmuxTests/CmuxTuiSurfaceProviderTests.swift b/cmuxTests/CmuxTuiSurfaceProviderTests.swift index fb0f06a67aa5..1be5d713da8d 100644 --- a/cmuxTests/CmuxTuiSurfaceProviderTests.swift +++ b/cmuxTests/CmuxTuiSurfaceProviderTests.swift @@ -1010,6 +1010,9 @@ import Testing LISTEN 0 128 0.0.0.0:3000 0.0.0.0:* """ #expect(CmuxTuiSnapshotParser.listeningPorts(fromSocketListing: ss) == [1337, 3000, 5901]) + let probe = VMExecResult(exitCode: 0, stdout: ss, stderr: "") + #expect(CmuxTuiSurfaceProvider.ports(from: probe, displayPortsOwned: true) == [3000]) + #expect(CmuxTuiSurfaceProvider.ports(from: probe, displayPortsOwned: false) == [3000, 5901]) #expect(CmuxTuiSnapshotParser.displayPorts.isSuperset(of: [5901, 5902, 5916, 6901, 6902, 6916])) #expect(CmuxTuiSnapshotParser.machineHasDesktop(image: "cmux-xfce-vnc:latest")) #expect(!CmuxTuiSnapshotParser.machineHasDesktop(image: "cmuxd-ws:tooling-20260509f")) From 019fd05c84aafd8fdaed0db7b7f674d959f169f2 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Mon, 21 Sep 2026 19:03:04 -0700 Subject: [PATCH 64/64] chore: keep cloud fix within file budgets --- Sources/BrowserWindowPortal.swift | 10 +--- .../Surfaces/CmuxTuiSurfaceProviders.swift | 48 +++---------------- cmuxTests/CmuxTuiSurfaceProviderTests.swift | 5 +- 3 files changed, 8 insertions(+), 55 deletions(-) diff --git a/Sources/BrowserWindowPortal.swift b/Sources/BrowserWindowPortal.swift index ee99de36715b..66e1626ac816 100644 --- a/Sources/BrowserWindowPortal.swift +++ b/Sources/BrowserWindowPortal.swift @@ -4027,15 +4027,7 @@ final class WindowBrowserPortal: NSObject { for subview in hostView.subviews.reversed() { guard let container = subview as? WindowBrowserSlotView else { continue } guard !container.isHidden else { continue } - // The portal host can be reparented during a split/workspace handoff. - // In that interval its local frame and the window-space frame may be - // expressed in different coordinate systems even though the WebView - // is visibly rendering. The first-responder guard uses this lookup to - // admit a real pointer click; relying only on `container.frame` then - // drops clicks into a visible VNC page while the terminal keeps focus. - let containsPoint = container.frame.contains(point) - || container.convert(container.bounds, to: nil).contains(windowPoint) - guard containsPoint else { continue } + let containsPoint = container.frame.contains(point) || container.convert(container.bounds, to: nil).contains(windowPoint); guard containsPoint else { continue } guard let webView = entriesByWebViewId .first(where: { _, entry in entry.containerView === container })? .value diff --git a/Sources/Surfaces/CmuxTuiSurfaceProviders.swift b/Sources/Surfaces/CmuxTuiSurfaceProviders.swift index 7b53869a73ff..2d18cae448a5 100644 --- a/Sources/Surfaces/CmuxTuiSurfaceProviders.swift +++ b/Sources/Surfaces/CmuxTuiSurfaceProviders.swift @@ -255,23 +255,11 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { let parsed = CmuxTuiSnapshotParser.mergingDisplays( pool: hasDesktop ? [desktopDisplayResource()] : [], parsed: CmuxTuiSnapshotParser.resources(from: cloudState) - ) + Self.portResources( - machine: machine, - scannedPorts: scannedPorts, - previousResources: previousResources, - privateAddress: summary.preferredPrivateAddress, - displayPortsOwned: hasDesktop - ) + ) + Self.portResources(machine: machine, scannedPorts: scannedPorts, previousResources: previousResources, privateAddress: summary.preferredPrivateAddress, displayPortsOwned: hasDesktop) resources = resourcesWithPendingCreations(parsed, state: cloudState) } else { var fallback = hasDesktop ? [desktopDisplayResource()] : [] - fallback.append(contentsOf: Self.portResources( - machine: machine, - scannedPorts: scannedPorts, - previousResources: previousResources, - privateAddress: summary.preferredPrivateAddress, - displayPortsOwned: hasDesktop - )) + fallback.append(contentsOf: Self.portResources(machine: machine, scannedPorts: scannedPorts, previousResources: previousResources, privateAddress: summary.preferredPrivateAddress, displayPortsOwned: hasDesktop)) appendMissingResources(preservedNonPortResources, to: &fallback) resources = resourcesWithPendingCreations(fallback, state: nil) } @@ -307,14 +295,7 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { // Start both after the link is ready, so refresh latency is the slower // request rather than their sum. Each result remains guarded by the // same generation fence before it publishes. - async let refreshedPorts = ports( - link: link, - socketPath: connected.socketPath, - force: force, - generation: generation, - privateAddress: privateAddress, - displayPortsOwned: hasDesktop - ) + async let refreshedPorts = ports(link: link, socketPath: connected.socketPath, force: force, generation: generation, privateAddress: privateAddress, displayPortsOwned: hasDesktop) async let snapshotData = link.run(arguments: CloudTuiRequests.snapshotArguments(socketPath: connected.socketPath)) if let refreshedPorts = await refreshedPorts { guard isCurrentRefresh(lifecycle: lifecycle, refresh: generation) else { return false } @@ -726,13 +707,7 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { /// the Freestyle attach path. Keeping this derivation in one place avoids /// losing the route when a cached or unavailable snapshot is published. private func portResources(_ ports: [Int]) -> [SurfaceResource] { - Self.portResources( - machine: machine, - scannedPorts: ports, - previousResources: catalog.authoritativeSnapshot.resources(on: machine), - privateAddress: summary.preferredPrivateAddress, - displayPortsOwned: summary.resolvedKind.hasDesktop - ) + Self.portResources(machine: machine, scannedPorts: ports, previousResources: catalog.authoritativeSnapshot.resources(on: machine), privateAddress: summary.preferredPrivateAddress, displayPortsOwned: summary.resolvedKind.hasDesktop) } /// cmux-tui's `selector.not_found` error body, surfaced by `link.run` as the @@ -1336,14 +1311,7 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { return updated } - private func ports( - link: CloudMachineLink, - socketPath: String, - force: Bool, - generation: UInt64, - privateAddress: String?, - displayPortsOwned: Bool - ) async -> [Int]? { + private func ports(link: CloudMachineLink, socketPath: String, force: Bool, generation: UInt64, privateAddress: String?, displayPortsOwned: Bool) async -> [Int]? { if !force, let cached = portsCache, Date.now.timeIntervalSince(cached.at) < portsTTL { return cached.ports.filter { !CmuxTuiSnapshotParser.internalPorts.contains($0) @@ -1357,11 +1325,7 @@ final class CmuxTuiSurfaceProvider: SurfaceProvider { return nil } let result = VMExecResult(exitCode: 0, stdout: stdout, stderr: "") - guard let ports = Self.ports( - from: result, - privateAddress: privateAddress, - displayPortsOwned: displayPortsOwned - ) else { return nil } + guard let ports = Self.ports(from: result, privateAddress: privateAddress, displayPortsOwned: displayPortsOwned) else { return nil } guard generation == refreshGeneration else { return nil } portsCache = (ports, Date.now) return ports diff --git a/cmuxTests/CmuxTuiSurfaceProviderTests.swift b/cmuxTests/CmuxTuiSurfaceProviderTests.swift index 1be5d713da8d..fbe089598f1e 100644 --- a/cmuxTests/CmuxTuiSurfaceProviderTests.swift +++ b/cmuxTests/CmuxTuiSurfaceProviderTests.swift @@ -1009,10 +1009,7 @@ import Testing LISTEN 0 128 127.0.0.1:5901 0.0.0.0:* LISTEN 0 128 0.0.0.0:3000 0.0.0.0:* """ - #expect(CmuxTuiSnapshotParser.listeningPorts(fromSocketListing: ss) == [1337, 3000, 5901]) - let probe = VMExecResult(exitCode: 0, stdout: ss, stderr: "") - #expect(CmuxTuiSurfaceProvider.ports(from: probe, displayPortsOwned: true) == [3000]) - #expect(CmuxTuiSurfaceProvider.ports(from: probe, displayPortsOwned: false) == [3000, 5901]) + #expect(CmuxTuiSnapshotParser.listeningPorts(fromSocketListing: ss) == [1337, 3000, 5901]); let probe = VMExecResult(exitCode: 0, stdout: ss, stderr: ""); #expect(CmuxTuiSurfaceProvider.ports(from: probe, displayPortsOwned: true) == [3000]); #expect(CmuxTuiSurfaceProvider.ports(from: probe, displayPortsOwned: false) == [3000, 5901]) #expect(CmuxTuiSnapshotParser.displayPorts.isSuperset(of: [5901, 5902, 5916, 6901, 6902, 6916])) #expect(CmuxTuiSnapshotParser.machineHasDesktop(image: "cmux-xfce-vnc:latest")) #expect(!CmuxTuiSnapshotParser.machineHasDesktop(image: "cmuxd-ws:tooling-20260509f"))