fix: release closed session resources
This commit is contained in:
+88
-10
@@ -47,6 +47,8 @@
|
||||
this.sshKeys = new SSHKeyManager(this);
|
||||
this.triggers = new TriggerEngine(this);
|
||||
this.leviathan = new LeviathanWorkbench(this);
|
||||
/** @type {Map<string, Promise<void>>} Idempotent in-flight session closes. */
|
||||
this._closingSessions = new Map();
|
||||
this.updateInfo = null;
|
||||
this.mode = "nexus";
|
||||
}
|
||||
@@ -399,20 +401,94 @@
|
||||
* Disconnects and removes a session without confirmation prompts.
|
||||
* @param {string} sessionId
|
||||
*/
|
||||
async closeSessionImmediate(sessionId) {
|
||||
closeSessionImmediate(sessionId, options = {}) {
|
||||
const existing = this._closingSessions.get(sessionId);
|
||||
if (existing) return existing;
|
||||
|
||||
let resolveOperation;
|
||||
let rejectOperation;
|
||||
const operation = new Promise((resolve, reject) => {
|
||||
resolveOperation = resolve;
|
||||
rejectOperation = reject;
|
||||
});
|
||||
this._closingSessions.set(sessionId, operation);
|
||||
|
||||
try {
|
||||
await window.NexusTerminal.wailsService.DisconnectSession(sessionId);
|
||||
} catch (_) {
|
||||
// Ignore failures; we still want to clean up locally.
|
||||
this._closeSessionResources(sessionId, options).then(
|
||||
resolveOperation,
|
||||
rejectOperation,
|
||||
);
|
||||
} catch (err) {
|
||||
rejectOperation(err);
|
||||
}
|
||||
|
||||
this.terminals.destroyTerminal(sessionId);
|
||||
this.rawViewer.destroyViewer(sessionId);
|
||||
window.NexusTerminal.GitHubViewer.destroyViewer(sessionId);
|
||||
window.NexusTerminal.VNCViewer.destroyViewer(sessionId);
|
||||
this.tabs.removeTab(sessionId);
|
||||
this.state.removeSession(sessionId);
|
||||
const clearClosing = () => {
|
||||
if (this._closingSessions.get(sessionId) === operation) {
|
||||
this._closingSessions.delete(sessionId);
|
||||
}
|
||||
};
|
||||
operation.then(clearClosing, clearClosing);
|
||||
return operation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases every frontend and backend resource owned by a session.
|
||||
* Local objects are dropped before awaiting the backend so a slow close
|
||||
* cannot leave renderers, observers or viewers running invisibly.
|
||||
* @param {string} sessionId
|
||||
* @param {{removeTab?: boolean, cleanupSplits?: boolean}} options
|
||||
* @private
|
||||
*/
|
||||
async _closeSessionResources(sessionId, options = {}) {
|
||||
const removeTab = options.removeTab !== false;
|
||||
const cleanupSplits = options.cleanupSplits !== false;
|
||||
|
||||
let splitCleanup = Promise.resolve();
|
||||
if (cleanupSplits) {
|
||||
try {
|
||||
splitCleanup = Promise.resolve(this.splitPanes.cleanup(sessionId));
|
||||
} catch (_) {
|
||||
// Continue with the rest of the idempotent local cleanup.
|
||||
}
|
||||
}
|
||||
|
||||
const localCleanups = [
|
||||
() => this.sftp.close(sessionId),
|
||||
() => this.triggers.cleanup(sessionId),
|
||||
() => this.recordings.cleanup(sessionId),
|
||||
() => this.terminals.destroyTerminal(sessionId),
|
||||
() => this.rawViewer.destroyViewer(sessionId),
|
||||
() => window.NexusTerminal.GitHubViewer?.destroyViewer(sessionId),
|
||||
() => window.NexusTerminal.VNCViewer?.destroyViewer(sessionId),
|
||||
() => window.NexusTerminal.IRCViewer?.destroyViewer(sessionId),
|
||||
() => this.leviathan?.destroyPanel(sessionId),
|
||||
];
|
||||
for (const cleanup of localCleanups) {
|
||||
try {
|
||||
cleanup();
|
||||
} catch (_) {
|
||||
// One faulty viewer must not prevent the remaining resources closing.
|
||||
}
|
||||
}
|
||||
|
||||
if (removeTab && this.tabs.order.includes(sessionId)) {
|
||||
this.tabs.removeTab(sessionId);
|
||||
}
|
||||
if (this.state.getSession(sessionId)) {
|
||||
this.state.removeSession(sessionId);
|
||||
}
|
||||
this.updateStatusBar();
|
||||
|
||||
let backendCleanup = Promise.resolve();
|
||||
try {
|
||||
backendCleanup = Promise.resolve(
|
||||
window.NexusTerminal.wailsService.DisconnectSession(sessionId),
|
||||
);
|
||||
} catch (_) {
|
||||
// Ignore failures; all local resources have already been released.
|
||||
}
|
||||
|
||||
await Promise.allSettled([splitCleanup, backendCleanup]);
|
||||
}
|
||||
|
||||
// ── Keyboard shortcuts ───────────────────────────────────────────
|
||||
@@ -1829,6 +1905,8 @@
|
||||
|
||||
// ── Bootstrap ────────────────────────────────────────────────────────
|
||||
|
||||
window.NexusTerminal.App = App;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
try {
|
||||
const app = new App();
|
||||
|
||||
@@ -195,6 +195,13 @@
|
||||
this.term.dispose();
|
||||
this.term = null;
|
||||
}
|
||||
this.fitAddon = null;
|
||||
this.searchAddon = null;
|
||||
this.targets.clear();
|
||||
this.users.clear();
|
||||
this.unreadTargets.clear();
|
||||
this.joinedRooms = [];
|
||||
this.roomList = [];
|
||||
this.container.classList.remove("irc-viewer-panel");
|
||||
this.container.innerHTML = "";
|
||||
}
|
||||
|
||||
@@ -542,6 +542,7 @@
|
||||
destroyPanel(sessionId) {
|
||||
this.lastResults.delete(sessionId);
|
||||
this.autoRunners.delete(sessionId);
|
||||
this.hexSearches.delete(sessionId);
|
||||
}
|
||||
|
||||
handleTabFocus(sessionId) {
|
||||
|
||||
@@ -65,6 +65,16 @@ class RecordingManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops frontend recording state when a session is closed. The backend
|
||||
* finalises the recorder as part of DisconnectSession.
|
||||
* @param {string} sessionId
|
||||
*/
|
||||
cleanup(sessionId) {
|
||||
this.activeSessions.delete(sessionId);
|
||||
this._updateRecordingIndicator(sessionId, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the recordings browser modal.
|
||||
*/
|
||||
|
||||
@@ -192,6 +192,7 @@
|
||||
var state = this.panels.get(sessionId);
|
||||
if (!state) return;
|
||||
|
||||
if (state.cleanupResize) state.cleanupResize();
|
||||
state.el.remove();
|
||||
|
||||
this.panels.delete(sessionId);
|
||||
@@ -1196,14 +1197,20 @@
|
||||
}
|
||||
|
||||
function onMouseUp() {
|
||||
cleanupResize();
|
||||
self._refitTerminal(sessionId);
|
||||
}
|
||||
|
||||
function cleanupResize() {
|
||||
handle.classList.remove("is-dragging");
|
||||
document.body.style.userSelect = "";
|
||||
document.body.style.cursor = "";
|
||||
document.removeEventListener("mousemove", onMouseMove);
|
||||
document.removeEventListener("mouseup", onMouseUp);
|
||||
self._refitTerminal(sessionId);
|
||||
}
|
||||
|
||||
state.cleanupResize = cleanupResize;
|
||||
|
||||
handle.addEventListener("mousedown", function (e) {
|
||||
e.preventDefault();
|
||||
startX = e.clientX;
|
||||
|
||||
+163
-17
@@ -33,6 +33,14 @@ class SplitPaneManager {
|
||||
* @type {Map<string, Object|null>}
|
||||
*/
|
||||
this.trees = new Map();
|
||||
|
||||
/**
|
||||
* Connections started from placeholders but not yet attached to a pane.
|
||||
* Closing a pane/tab marks its token cancelled so a late backend result
|
||||
* is disconnected instead of becoming an invisible session.
|
||||
* @type {Map<string, {ownerSessionId: string, cancelled: boolean, completed: boolean}>}
|
||||
*/
|
||||
this.pendingConnections = new Map();
|
||||
}
|
||||
|
||||
// ── Public API ───────────────────────────────────────────────────
|
||||
@@ -55,8 +63,47 @@ class SplitPaneManager {
|
||||
* Removes all split state for a session (used on tab close).
|
||||
* @param {string} sessionId
|
||||
*/
|
||||
cleanup(sessionId) {
|
||||
async cleanup(sessionId) {
|
||||
var root = this.trees.get(sessionId);
|
||||
this.trees.delete(sessionId);
|
||||
|
||||
var childSessionIds = new Set();
|
||||
var self = this;
|
||||
function releaseNode(node) {
|
||||
if (!node) return;
|
||||
if (node.type === 'leaf') {
|
||||
self._cancelPendingConnection(node.paneId);
|
||||
if (node.sessionId && node.sessionId !== sessionId) {
|
||||
childSessionIds.add(node.sessionId);
|
||||
}
|
||||
if (node.el) node.el._splitNode = null;
|
||||
node.parent = null;
|
||||
return;
|
||||
}
|
||||
if (node.cancelDrag) node.cancelDrag();
|
||||
releaseNode(node.first);
|
||||
releaseNode(node.second);
|
||||
if (node.el) node.el._splitNode = null;
|
||||
node.parent = null;
|
||||
}
|
||||
releaseNode(root);
|
||||
|
||||
// A connection may still be resolving before its leaf has a session ID.
|
||||
// Its token remains captured by the promise and will disconnect any late
|
||||
// result after _cancelPendingConnection marks it cancelled.
|
||||
for (var [paneId, token] of this.pendingConnections) {
|
||||
if (token.ownerSessionId === sessionId) {
|
||||
token.cancelled = true;
|
||||
this.pendingConnections.delete(paneId);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.allSettled(Array.from(childSessionIds, function(childId) {
|
||||
return self.app.closeSessionImmediate(childId, {
|
||||
removeTab: false,
|
||||
cleanupSplits: false
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -265,15 +312,27 @@ class SplitPaneManager {
|
||||
var branch = leaf.parent;
|
||||
var sibling = (branch.first === leaf) ? branch.second : branch.first;
|
||||
|
||||
// If the closing pane has a terminal session, destroy it (but not the owner session)
|
||||
this._cancelPendingConnection(leaf.paneId);
|
||||
|
||||
// If the closing pane has a child session, release both its frontend
|
||||
// renderer and backend connection. The owner pane cannot be closed
|
||||
// independently because it provides the tab's identity.
|
||||
if (leaf.sessionId && leaf.sessionId !== ownerSessionId) {
|
||||
this.app.terminals.destroyTerminal(leaf.sessionId);
|
||||
this.app.closeSessionImmediate(leaf.sessionId, {
|
||||
removeTab: false,
|
||||
cleanupSplits: false
|
||||
});
|
||||
leaf.sessionId = null;
|
||||
}
|
||||
|
||||
if (branch.cancelDrag) branch.cancelDrag();
|
||||
|
||||
// Replace the branch element with the sibling element in the DOM
|
||||
sibling.el.style.flex = branch.el.style.flex || '1';
|
||||
branch.el.parentNode.insertBefore(sibling.el, branch.el);
|
||||
branch.el.remove();
|
||||
leaf.el._splitNode = null;
|
||||
branch.el._splitNode = null;
|
||||
|
||||
// Remove close button from sibling if it's now the only pane
|
||||
sibling.parent = branch.parent;
|
||||
@@ -312,18 +371,37 @@ class SplitPaneManager {
|
||||
*/
|
||||
_setupDividerDrag(divider, branch) {
|
||||
var self = this;
|
||||
var onMouseMove = null;
|
||||
var onMouseUp = null;
|
||||
var bodyClass = null;
|
||||
|
||||
function stopDrag(refit) {
|
||||
if (bodyClass) document.body.classList.remove(bodyClass);
|
||||
divider.classList.remove('is-dragging');
|
||||
if (onMouseMove) document.removeEventListener('mousemove', onMouseMove);
|
||||
if (onMouseUp) document.removeEventListener('mouseup', onMouseUp);
|
||||
onMouseMove = null;
|
||||
onMouseUp = null;
|
||||
bodyClass = null;
|
||||
if (refit) self._refitTree(branch);
|
||||
}
|
||||
|
||||
branch.cancelDrag = function() {
|
||||
stopDrag(false);
|
||||
};
|
||||
|
||||
divider.addEventListener('mousedown', function(e) {
|
||||
e.preventDefault();
|
||||
stopDrag(false);
|
||||
var isHorizontal = branch.direction === 'horizontal';
|
||||
var bodyClass = isHorizontal ? 'split-resizing-row' : 'split-resizing';
|
||||
bodyClass = isHorizontal ? 'split-resizing-row' : 'split-resizing';
|
||||
document.body.classList.add(bodyClass);
|
||||
divider.classList.add('is-dragging');
|
||||
|
||||
var containerRect = branch.el.getBoundingClientRect();
|
||||
var dividerSize = isHorizontal ? 4 : 4; // divider thickness
|
||||
|
||||
function onMouseMove(e) {
|
||||
onMouseMove = function(e) {
|
||||
var totalSize, mousePos;
|
||||
if (isHorizontal) {
|
||||
totalSize = containerRect.height - dividerSize;
|
||||
@@ -343,17 +421,11 @@ class SplitPaneManager {
|
||||
|
||||
// Refit terminals in this branch
|
||||
self._refitTree(branch);
|
||||
}
|
||||
};
|
||||
|
||||
function onMouseUp() {
|
||||
document.body.classList.remove(bodyClass);
|
||||
divider.classList.remove('is-dragging');
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
document.removeEventListener('mouseup', onMouseUp);
|
||||
|
||||
// Final refit
|
||||
self._refitTree(branch);
|
||||
}
|
||||
onMouseUp = function() {
|
||||
stopDrag(true);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
@@ -443,17 +515,29 @@ class SplitPaneManager {
|
||||
*/
|
||||
_connectInPane(paneEl, profile, ownerSessionId) {
|
||||
var self = this;
|
||||
var token = this._beginPendingConnection(paneEl, ownerSessionId);
|
||||
|
||||
// Remove placeholder
|
||||
var placeholder = paneEl.querySelector('.split-pane-placeholder');
|
||||
if (placeholder) placeholder.remove();
|
||||
|
||||
// Connect via backend
|
||||
window.NexusTerminal.wailsService.ConnectSession(profile.id).then(function(sessionInfo) {
|
||||
window.NexusTerminal.wailsService.ConnectSession(profile.id).then(async function(sessionInfo) {
|
||||
if (!self._completePendingConnection(paneEl, ownerSessionId, token)) {
|
||||
try {
|
||||
await window.NexusTerminal.wailsService.DisconnectSession(sessionInfo.id);
|
||||
} catch (_) {
|
||||
// The backend may already have completed the close.
|
||||
}
|
||||
return;
|
||||
}
|
||||
self._attachTerminalToPane(paneEl, sessionInfo, profile);
|
||||
|
||||
self.app.notifications.info('Connecting to ' + (profile.displayName || profile.host) + '…');
|
||||
}).catch(function(err) {
|
||||
if (!self._completePendingConnection(paneEl, ownerSessionId, token)) {
|
||||
return;
|
||||
}
|
||||
self.app.notifications.error('Connection failed: ' + err);
|
||||
// Re-render placeholder on failure
|
||||
self._renderPlaceholder(paneEl, ownerSessionId);
|
||||
@@ -468,10 +552,19 @@ class SplitPaneManager {
|
||||
*/
|
||||
_connectLocalInPane(paneEl, ownerSessionId) {
|
||||
var self = this;
|
||||
var token = this._beginPendingConnection(paneEl, ownerSessionId);
|
||||
var placeholder = paneEl.querySelector('.split-pane-placeholder');
|
||||
if (placeholder) placeholder.remove();
|
||||
|
||||
window.NexusTerminal.wailsService.ConnectLocal().then(function(sessionInfo) {
|
||||
window.NexusTerminal.wailsService.ConnectLocal().then(async function(sessionInfo) {
|
||||
if (!self._completePendingConnection(paneEl, ownerSessionId, token)) {
|
||||
try {
|
||||
await window.NexusTerminal.wailsService.DisconnectSession(sessionInfo.id);
|
||||
} catch (_) {
|
||||
// The backend may already have completed the close.
|
||||
}
|
||||
return;
|
||||
}
|
||||
var prefs = self.app.state.preferences || {};
|
||||
self._attachTerminalToPane(paneEl, sessionInfo, {
|
||||
terminalFontFamily: prefs.defaultFontFamily,
|
||||
@@ -481,6 +574,9 @@ class SplitPaneManager {
|
||||
});
|
||||
self.app.notifications.info('Opening local terminal…');
|
||||
}).catch(function(err) {
|
||||
if (!self._completePendingConnection(paneEl, ownerSessionId, token)) {
|
||||
return;
|
||||
}
|
||||
self.app.notifications.error('Local terminal failed: ' + err);
|
||||
self._renderPlaceholder(paneEl, ownerSessionId);
|
||||
});
|
||||
@@ -513,6 +609,53 @@ class SplitPaneManager {
|
||||
this.app.terminals.createTerminal(sessionInfo.id, termContainer.id, terminalProfile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks an asynchronous connection attempt owned by a pane.
|
||||
* @private
|
||||
*/
|
||||
_beginPendingConnection(paneEl, ownerSessionId) {
|
||||
var paneId = paneEl.dataset.paneId;
|
||||
this._cancelPendingConnection(paneId);
|
||||
var token = {
|
||||
ownerSessionId: ownerSessionId,
|
||||
cancelled: false,
|
||||
completed: false
|
||||
};
|
||||
this.pendingConnections.set(paneId, token);
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true only if a connection result still belongs to a live pane.
|
||||
* @private
|
||||
*/
|
||||
_completePendingConnection(paneEl, ownerSessionId, token) {
|
||||
if (token.completed) return false;
|
||||
token.completed = true;
|
||||
var paneId = paneEl.dataset.paneId;
|
||||
if (this.pendingConnections.get(paneId) === token) {
|
||||
this.pendingConnections.delete(paneId);
|
||||
}
|
||||
var root = this.trees.get(ownerSessionId);
|
||||
var leaf = this._findLeafById(root, paneId);
|
||||
return !token.cancelled &&
|
||||
paneEl.isConnected !== false &&
|
||||
leaf &&
|
||||
leaf.el === paneEl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels a pending pane connection without waiting for the backend call.
|
||||
* A late successful result will disconnect itself.
|
||||
* @private
|
||||
*/
|
||||
_cancelPendingConnection(paneId) {
|
||||
var token = this.pendingConnections.get(paneId);
|
||||
if (!token) return;
|
||||
token.cancelled = true;
|
||||
this.pendingConnections.delete(paneId);
|
||||
}
|
||||
|
||||
// ── Tree traversal helpers ───────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -594,6 +737,9 @@ class SplitPaneManager {
|
||||
_addCloseButton(paneEl, ownerSessionId) {
|
||||
// Don't add if already present
|
||||
if (paneEl.querySelector(':scope > .split-pane-close')) return;
|
||||
// The owner session supplies the tab/panel identity. Allowing that leaf
|
||||
// to close would leave an invisible backend session behind.
|
||||
if (paneEl._splitNode && paneEl._splitNode.sessionId === ownerSessionId) return;
|
||||
|
||||
var btn = document.createElement('button');
|
||||
btn.className = 'split-pane-close';
|
||||
|
||||
@@ -164,6 +164,9 @@
|
||||
*/
|
||||
removeSession(id) {
|
||||
this.sessions.delete(id);
|
||||
if (this.broadcastTargets) {
|
||||
this.broadcastTargets.delete(id);
|
||||
}
|
||||
if (this.activeSessionId === id) {
|
||||
// Activate the most recent remaining session, or null
|
||||
const remaining = [...this.sessions.keys()];
|
||||
|
||||
+3
-20
@@ -182,19 +182,7 @@
|
||||
if (!ok) return;
|
||||
}
|
||||
|
||||
try {
|
||||
await window.NexusTerminal.wailsService.DisconnectSession(sessionId);
|
||||
} catch (_) {
|
||||
// Session may already be closed — carry on
|
||||
}
|
||||
|
||||
this.app.terminals.destroyTerminal(sessionId);
|
||||
this.app.rawViewer.destroyViewer(sessionId);
|
||||
window.NexusTerminal.GitHubViewer.destroyViewer(sessionId);
|
||||
window.NexusTerminal.IRCViewer.destroyViewer(sessionId);
|
||||
this.app.leviathan?.destroyPanel(sessionId);
|
||||
this.removeTab(sessionId);
|
||||
this.app.state.removeSession(sessionId);
|
||||
await this.app.closeSessionImmediate(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -852,13 +840,8 @@
|
||||
const session = this.app.state.getSession(sessionId);
|
||||
if (!session || !session.profileId) return;
|
||||
|
||||
// Close the current tab, then open a fresh connection
|
||||
this.app.terminals.destroyTerminal(sessionId);
|
||||
this.app.rawViewer.destroyViewer(sessionId);
|
||||
window.NexusTerminal.GitHubViewer.destroyViewer(sessionId);
|
||||
this.app.state.removeSession(sessionId);
|
||||
this.removeTab(sessionId);
|
||||
|
||||
// Fully close the old session before opening a fresh connection.
|
||||
await this.app.closeSessionImmediate(sessionId);
|
||||
await this.app.connectProfile(session.profileId);
|
||||
}
|
||||
|
||||
|
||||
+44
-8
@@ -74,6 +74,9 @@
|
||||
this.container = container;
|
||||
this.profile = profile;
|
||||
this.rfb = null;
|
||||
this._rfbListeners = [];
|
||||
this._credentialDialog = null;
|
||||
this._destroyed = false;
|
||||
|
||||
this._buildUI();
|
||||
}
|
||||
@@ -132,6 +135,7 @@
|
||||
* @param {string} wsURL - WebSocket URL (ws://127.0.0.1:<port>)
|
||||
*/
|
||||
connect(wsURL) {
|
||||
if (this._destroyed) return;
|
||||
if (!window.noVNC || !window.noVNC.RFB) {
|
||||
this.statusEl.textContent = "Error: noVNC not loaded";
|
||||
this.statusEl.classList.add("vnc-status-error");
|
||||
@@ -154,13 +158,13 @@
|
||||
this.rfb.compressionLevel = 2;
|
||||
|
||||
// Event handlers
|
||||
this.rfb.addEventListener("connect", () => {
|
||||
this._addRFBListener("connect", () => {
|
||||
this.statusEl.textContent = "Connected";
|
||||
this.statusEl.classList.remove("vnc-status-error");
|
||||
this.statusEl.classList.add("vnc-status-connected");
|
||||
});
|
||||
|
||||
this.rfb.addEventListener("disconnect", (ev) => {
|
||||
this._addRFBListener("disconnect", (ev) => {
|
||||
const clean = ev.detail.clean;
|
||||
this.statusEl.textContent = clean
|
||||
? "Disconnected"
|
||||
@@ -171,13 +175,13 @@
|
||||
}
|
||||
});
|
||||
|
||||
this.rfb.addEventListener("credentialsrequired", () => {
|
||||
this._addRFBListener("credentialsrequired", () => {
|
||||
this.statusEl.textContent = "Credentials required";
|
||||
// TODO: prompt for VNC password via modal
|
||||
this._promptCredentials();
|
||||
});
|
||||
|
||||
this.rfb.addEventListener("desktopname", (ev) => {
|
||||
this._addRFBListener("desktopname", (ev) => {
|
||||
this.statusEl.textContent = "Connected — " + ev.detail.name;
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -192,6 +196,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
_addRFBListener(name, handler) {
|
||||
this.rfb.addEventListener(name, handler);
|
||||
this._rfbListeners.push({ name, handler });
|
||||
}
|
||||
|
||||
async _syncClipboard() {
|
||||
if (!this.rfb) return;
|
||||
try {
|
||||
@@ -213,6 +222,9 @@
|
||||
}
|
||||
|
||||
_promptCredentials() {
|
||||
if (this._destroyed) return;
|
||||
this._closeCredentialDialog();
|
||||
|
||||
// Build a simple modal for VNC password
|
||||
const backdrop = createElement("div", "modal-backdrop");
|
||||
const modal = createElement("div", "modal modal-sm");
|
||||
@@ -242,13 +254,21 @@
|
||||
const input = modal.querySelector("#vnc-password-input");
|
||||
const submit = modal.querySelector("#vnc-password-submit");
|
||||
const cancel = modal.querySelector("#vnc-password-cancel");
|
||||
const dialog = {
|
||||
backdrop,
|
||||
modal,
|
||||
removeTimer: null,
|
||||
focusTimer: null,
|
||||
};
|
||||
this._credentialDialog = dialog;
|
||||
|
||||
const close = () => {
|
||||
backdrop.classList.remove("is-visible");
|
||||
modal.classList.remove("is-visible");
|
||||
setTimeout(() => {
|
||||
backdrop.remove();
|
||||
modal.remove();
|
||||
dialog.removeTimer = setTimeout(() => {
|
||||
if (this._credentialDialog === dialog) {
|
||||
this._closeCredentialDialog();
|
||||
}
|
||||
}, 200);
|
||||
};
|
||||
|
||||
@@ -269,7 +289,17 @@
|
||||
if (e.key === "Escape") cancel.click();
|
||||
});
|
||||
|
||||
setTimeout(() => input.focus(), 50);
|
||||
dialog.focusTimer = setTimeout(() => input.focus(), 50);
|
||||
}
|
||||
|
||||
_closeCredentialDialog() {
|
||||
const dialog = this._credentialDialog;
|
||||
if (!dialog) return;
|
||||
if (dialog.removeTimer) clearTimeout(dialog.removeTimer);
|
||||
if (dialog.focusTimer) clearTimeout(dialog.focusTimer);
|
||||
dialog.backdrop.remove();
|
||||
dialog.modal.remove();
|
||||
this._credentialDialog = null;
|
||||
}
|
||||
|
||||
resize() {
|
||||
@@ -281,7 +311,13 @@
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._destroyed = true;
|
||||
this._closeCredentialDialog();
|
||||
if (this.rfb) {
|
||||
for (const { name, handler } of this._rfbListeners) {
|
||||
this.rfb.removeEventListener(name, handler);
|
||||
}
|
||||
this._rfbListeners = [];
|
||||
this.rfb.disconnect();
|
||||
this.rfb = null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user