[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
199
electron/bridges/sftpTransferSessionLease.cjs
Normal file
199
electron/bridges/sftpTransferSessionLease.cjs
Normal file
@@ -0,0 +1,199 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Transfer-owned SFTP session leases (reference counting).
|
||||
*
|
||||
* File transfers borrow panel (or agent) sftpIds. Without leases, disconnecting
|
||||
* the browser panel calls closeSftp and kills in-flight / paused transfers.
|
||||
*
|
||||
* Model:
|
||||
* - acquire(sftpId, transferId) while a transfer uses that session
|
||||
* - release when the transfer finishes, fails, or is cancelled
|
||||
* - closeSftp with outstanding leases becomes a soft-close: the client stays
|
||||
* in sftpClients until the last transfer releases
|
||||
* - when the last lease is released on a soft-closed session, the real close
|
||||
* runs (pendingHardClose callback / flag)
|
||||
*
|
||||
* This is intentionally independent of UI panels. Browse can go away; transfers
|
||||
* keep their leased sessions until they complete.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} SftpTransferSessionLeaseStore
|
||||
* @property {(sftpId: string, transferId: string) => boolean} acquire
|
||||
* @property {(sftpId: string, transferId: string) => { released: boolean, shouldHardClose: boolean, remaining: number }} release
|
||||
* @property {(sftpId: string) => boolean} isHeld
|
||||
* @property {(sftpId: string) => number} getLeaseCount
|
||||
* @property {(sftpId: string) => string[]} listTransferIds
|
||||
* @property {(sftpId: string) => boolean} markSoftClosed
|
||||
* @property {(sftpId: string) => boolean} isSoftClosed
|
||||
* @property {(sftpId: string) => number|undefined} getPendingHardCloseToken
|
||||
* @property {(sftpId: string) => number|undefined} beginHardClose
|
||||
* @property {(sftpId: string, closeToken: number) => boolean} commitHardClose
|
||||
* @property {(sftpId: string) => boolean} isHardCloseCommitted
|
||||
* @property {(sftpId: string) => void} clear
|
||||
* @property {() => void} resetForTests
|
||||
*/
|
||||
|
||||
/**
|
||||
* @returns {SftpTransferSessionLeaseStore}
|
||||
*/
|
||||
function createSftpTransferSessionLeaseStore() {
|
||||
/** @type {Map<string, Set<string>>} */
|
||||
const leases = new Map();
|
||||
/** @type {Set<string>} */
|
||||
const softClosed = new Set();
|
||||
/** @type {Map<string, { token: number, committed: boolean }>} */
|
||||
const hardCloseClaims = new Map();
|
||||
let nextHardCloseToken = 0;
|
||||
|
||||
function beginHardClose(sftpId) {
|
||||
const existing = hardCloseClaims.get(sftpId);
|
||||
if (existing) return existing.token;
|
||||
nextHardCloseToken += 1;
|
||||
hardCloseClaims.set(sftpId, {
|
||||
token: nextHardCloseToken,
|
||||
committed: false,
|
||||
});
|
||||
return nextHardCloseToken;
|
||||
}
|
||||
|
||||
function getSet(sftpId) {
|
||||
let set = leases.get(sftpId);
|
||||
if (!set) {
|
||||
set = new Set();
|
||||
leases.set(sftpId, set);
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
return {
|
||||
acquire(sftpId, transferId) {
|
||||
if (!sftpId || !transferId) return false;
|
||||
const closeClaim = hardCloseClaims.get(sftpId);
|
||||
// Before commit, a directory walk's next child may cancel the deferred
|
||||
// close and keep using the live session. After commit, the SSH/SFTP close
|
||||
// has started and this client must never be lent again.
|
||||
if (closeClaim?.committed) return false;
|
||||
if (closeClaim) {
|
||||
hardCloseClaims.delete(sftpId);
|
||||
// The panel's close intent remains sticky across the cancelled close;
|
||||
// the next last release must claim hard-close again.
|
||||
softClosed.add(sftpId);
|
||||
}
|
||||
const set = getSet(sftpId);
|
||||
const before = set.size;
|
||||
set.add(String(transferId));
|
||||
// Keep soft-close sticky. Panel already asked to tear down this session;
|
||||
// new transfers may still borrow it, but the last release must hard-close.
|
||||
// Clearing softClosed here previously leaked sockets forever after
|
||||
// panel close + re-acquire + all transfers finishing.
|
||||
return set.size > before;
|
||||
},
|
||||
|
||||
release(sftpId, transferId) {
|
||||
if (!sftpId || !transferId) {
|
||||
return { released: false, shouldHardClose: false, remaining: 0 };
|
||||
}
|
||||
const set = leases.get(sftpId);
|
||||
if (!set || !set.has(String(transferId))) {
|
||||
return {
|
||||
released: false,
|
||||
shouldHardClose: softClosed.has(sftpId) && (!set || set.size === 0),
|
||||
remaining: set ? set.size : 0,
|
||||
};
|
||||
}
|
||||
set.delete(String(transferId));
|
||||
const remaining = set.size;
|
||||
if (remaining === 0) {
|
||||
leases.delete(sftpId);
|
||||
const shouldHardClose = softClosed.has(sftpId);
|
||||
if (shouldHardClose) {
|
||||
beginHardClose(sftpId);
|
||||
softClosed.delete(sftpId);
|
||||
}
|
||||
return { released: true, shouldHardClose, remaining: 0 };
|
||||
}
|
||||
return { released: true, shouldHardClose: false, remaining };
|
||||
},
|
||||
|
||||
isHeld(sftpId) {
|
||||
if (!sftpId) return false;
|
||||
const set = leases.get(sftpId);
|
||||
return !!(set && set.size > 0);
|
||||
},
|
||||
|
||||
getLeaseCount(sftpId) {
|
||||
if (!sftpId) return 0;
|
||||
return leases.get(sftpId)?.size ?? 0;
|
||||
},
|
||||
|
||||
listTransferIds(sftpId) {
|
||||
if (!sftpId) return [];
|
||||
return [...(leases.get(sftpId) ?? [])];
|
||||
},
|
||||
|
||||
/**
|
||||
* Panel wants to close this session. If transfers still hold it, defer the
|
||||
* real teardown until the last transfer releases.
|
||||
* @returns {boolean} true when close was deferred (session stays open)
|
||||
*/
|
||||
markSoftClosed(sftpId) {
|
||||
if (!sftpId) return false;
|
||||
if (!this.isHeld(sftpId)) {
|
||||
softClosed.delete(sftpId);
|
||||
return false;
|
||||
}
|
||||
softClosed.add(sftpId);
|
||||
return true;
|
||||
},
|
||||
|
||||
isSoftClosed(sftpId) {
|
||||
return !!sftpId && softClosed.has(sftpId);
|
||||
},
|
||||
|
||||
getPendingHardCloseToken(sftpId) {
|
||||
return hardCloseClaims.get(sftpId)?.token;
|
||||
},
|
||||
|
||||
beginHardClose(sftpId) {
|
||||
if (!sftpId) return undefined;
|
||||
return beginHardClose(sftpId);
|
||||
},
|
||||
|
||||
commitHardClose(sftpId, closeToken) {
|
||||
const claim = hardCloseClaims.get(sftpId);
|
||||
if (!claim || claim.token !== closeToken || claim.committed || this.isHeld(sftpId)) {
|
||||
return false;
|
||||
}
|
||||
claim.committed = true;
|
||||
return true;
|
||||
},
|
||||
|
||||
isHardCloseCommitted(sftpId) {
|
||||
return hardCloseClaims.get(sftpId)?.committed === true;
|
||||
},
|
||||
|
||||
clear(sftpId) {
|
||||
if (!sftpId) return;
|
||||
leases.delete(sftpId);
|
||||
softClosed.delete(sftpId);
|
||||
hardCloseClaims.delete(sftpId);
|
||||
},
|
||||
|
||||
resetForTests() {
|
||||
leases.clear();
|
||||
softClosed.clear();
|
||||
hardCloseClaims.clear();
|
||||
nextHardCloseToken = 0;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Process-wide store shared by transferBridge and closeSftp. */
|
||||
const sftpTransferSessionLeaseStore = createSftpTransferSessionLeaseStore();
|
||||
|
||||
module.exports = {
|
||||
createSftpTransferSessionLeaseStore,
|
||||
sftpTransferSessionLeaseStore,
|
||||
};
|
||||
Reference in New Issue
Block a user