@zhangfengshun/dsh-remote-ssh 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +2 -1
- package/lib/index.js +178 -26
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
本文件的版本号与 `package.json` 的 `version` 保持一致。每个版本对应一个 Cordis Package 快照(`pkg-N`)。
|
|
4
4
|
|
|
5
|
+
## [1.7.0] — SSH 连接复用(大幅加速)
|
|
6
|
+
### 重大优化
|
|
7
|
+
- **持久 SSH 会话池**:`ls`/`cat`/`write`/`grep`/`glob`/`mkdir`/`delete`/`move` 等文件操作不再每次新建 ssh 子进程(每次都要完整 TCP 握手 + 密钥交换 + 认证,超算/跳板机单次 2-10 秒)。改为维护一条常驻 `ssh <host> bash` 进程,所有命令复用它,用哨兵标记(sentinel)分隔输出、解析退出码。首次连接后,后续操作近乎瞬时。
|
|
8
|
+
- **自动回退**:持久会话断开(网络中断、远端重启等)时自动清理并回退到一次性 `runRemote`,保证可靠性。
|
|
9
|
+
- **空闲清理**:会话空闲超过 10 分钟自动断开,避免占着连接。插件卸载时全部清理。
|
|
10
|
+
- `remote_ssh_exec` 与测试连接仍用一次性连接(保留 stdout/stderr 分离)。
|
|
11
|
+
- Windows OpenSSH 不支持 ControlMaster(已验证),故采用此进程内会话池方案,不依赖 ControlMaster。
|
|
12
|
+
|
|
5
13
|
## [1.6.0] — 远程搜索与文件操作工具
|
|
6
14
|
### 新增
|
|
7
15
|
- **内容搜索 `remote_ssh_grep`**:在远端递归搜索文件内容(`grep -rnIE`,扩展正则),支持 `include` 文件名过滤(如 `*.py`)、`ignoreCase` 忽略大小写、`maxResults` 限流(默认 200)。借鉴 dsh-remote / dsh-remote-ssh 的远程搜索,但用通用 GNU grep(超算/Linux 通用,不依赖 ripgrep)。
|
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
| 能力 | 说明 |
|
|
10
10
|
| --- | --- |
|
|
11
11
|
| 🔌 远程连接 | SSH 连接超算 / 服务器,密钥认证(推荐)或密码认证(需本机 `sshpass`),内置「测试连接」;支持 `ProxyJump` 跳板机 |
|
|
12
|
+
| ⚡ 连接复用 | 持久 SSH 会话池,文件操作复用同一条已认证连接,不再每次握手(首次后近乎瞬时) |
|
|
12
13
|
| 📥 配置导入 | 一键从 `~/.ssh/config`(递归 `Include`)发现主机并批量导入连接配置 |
|
|
13
14
|
| 🗂️ 远程文件 | 在 better-sidebar「远程文件」页签浏览 / 打开 / 编辑 / 保存远程文件;选中工作区后可双向同步 |
|
|
14
15
|
| 💻 远程终端 | 在 better-sidebar「远程终端」页签打开 `ssh -tt` 集成终端,支持多终端并发 |
|
|
@@ -29,7 +30,7 @@ dsh plugin --profile <name> add /absolute/path/to/dsh-remote-ssh
|
|
|
29
30
|
### 发布后安装
|
|
30
31
|
|
|
31
32
|
```bash
|
|
32
|
-
dsh plugin --profile <name> add @zhangfengshun/dsh-remote-ssh@1.
|
|
33
|
+
dsh plugin --profile <name> add @zhangfengshun/dsh-remote-ssh@1.7.0
|
|
33
34
|
```
|
|
34
35
|
|
|
35
36
|
> ⚠️ 安装后需**重启 DSH** 才生效;后续仅修改 Client 半边时刷新浏览器即可。
|
package/lib/index.js
CHANGED
|
@@ -155,6 +155,121 @@ async function listSshConfigHosts() {
|
|
|
155
155
|
return { ok: true, hosts: out };
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
/**
|
|
159
|
+
* 持久 SSH 会话:维护一条常驻 ssh <host> bash 进程,所有命令复用它,
|
|
160
|
+
* 避免每次操作都做完整 TCP 握手 + SSH 密钥交换 + 认证(超算/跳板机单次 2-10 秒)。
|
|
161
|
+
*
|
|
162
|
+
* 工作原理:向 bash 的 stdin 写入命令 + 哨兵标记 printf,从 stdout 读取直到哨兵出现,
|
|
163
|
+
* 哨兵后的数字即退出码。每条命令用 2>&1 合并 stderr(成功时 stderr 为空,不影响解析;
|
|
164
|
+
* 失败时错误信息在 stdout 中,退出码非 0)。带 stdin 的写操作用 heredoc。
|
|
165
|
+
*/
|
|
166
|
+
class CommandSession {
|
|
167
|
+
constructor(subprocess, profile) {
|
|
168
|
+
this.subprocess = subprocess;
|
|
169
|
+
this.profile = profile;
|
|
170
|
+
this.handle = null;
|
|
171
|
+
this.alive = false;
|
|
172
|
+
this.buf = "";
|
|
173
|
+
this.queue = [];
|
|
174
|
+
this.current = null;
|
|
175
|
+
this.sentinel = "DSHEOF" + Math.random().toString(36).slice(2, 14) + Math.random().toString(36).slice(2, 10) + "DSHEOF";
|
|
176
|
+
this.lastUsed = Date.now();
|
|
177
|
+
this.connectPromise = null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async connect() {
|
|
181
|
+
if (this.alive && this.handle) return;
|
|
182
|
+
if (this.connectPromise) return this.connectPromise;
|
|
183
|
+
this.connectPromise = this._doConnect();
|
|
184
|
+
try { await this.connectPromise; } finally { this.connectPromise = null; }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async _doConnect() {
|
|
188
|
+
if (this.handle) { try { this.handle.terminate(); } catch (e) {} this.handle = null; }
|
|
189
|
+
this.alive = false;
|
|
190
|
+
this.buf = "";
|
|
191
|
+
this.current = null;
|
|
192
|
+
this.queue = [];
|
|
193
|
+
const argv = sshArgv(this.profile, "bash", false);
|
|
194
|
+
const h = this.subprocess.spawn({
|
|
195
|
+
argv: argv,
|
|
196
|
+
cwd: process.cwd(),
|
|
197
|
+
stdio: { stdin: "pipe", stdout: "pipe", stderr: "pipe" },
|
|
198
|
+
graceMs: 60000
|
|
199
|
+
});
|
|
200
|
+
this.handle = h;
|
|
201
|
+
h.stdout.on("data", (chunk) => {
|
|
202
|
+
this.buf += chunk.toString("utf8");
|
|
203
|
+
this._check();
|
|
204
|
+
});
|
|
205
|
+
const onEnd = () => {
|
|
206
|
+
this.alive = false;
|
|
207
|
+
if (this.current) { this.current.reject(new Error("ssh 会话已关闭")); this.current = null; }
|
|
208
|
+
while (this.queue.length) this.queue.shift().reject(new Error("ssh 会话已关闭"));
|
|
209
|
+
};
|
|
210
|
+
h.stdout.on("end", onEnd);
|
|
211
|
+
h.stdout.on("error", onEnd);
|
|
212
|
+
h.done.then(() => { this.alive = false; }, () => { this.alive = false; });
|
|
213
|
+
this.alive = true;
|
|
214
|
+
this.lastUsed = Date.now();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
_check() {
|
|
218
|
+
if (!this.current) return;
|
|
219
|
+
const idx = this.buf.indexOf(this.sentinel);
|
|
220
|
+
if (idx < 0) return;
|
|
221
|
+
const before = this.buf.slice(0, idx);
|
|
222
|
+
const after = this.buf.slice(idx + this.sentinel.length);
|
|
223
|
+
const nl = after.indexOf("\n");
|
|
224
|
+
const codeStr = nl >= 0 ? after.slice(0, nl) : after;
|
|
225
|
+
const rest = nl >= 0 ? after.slice(nl + 1) : "";
|
|
226
|
+
this.buf = rest;
|
|
227
|
+
const exitCode = parseInt(codeStr, 10);
|
|
228
|
+
const cur = this.current;
|
|
229
|
+
this.current = null;
|
|
230
|
+
cur.resolve({ stdout: before, exitCode: isNaN(exitCode) ? -1 : exitCode });
|
|
231
|
+
this._next();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
_next() {
|
|
235
|
+
if (this.current || !this.alive || this.queue.length === 0) return;
|
|
236
|
+
const item = this.queue.shift();
|
|
237
|
+
this.current = { resolve: item.resolve, reject: item.reject };
|
|
238
|
+
try {
|
|
239
|
+
let toWrite;
|
|
240
|
+
if (item.stdinData !== undefined && item.stdinData !== null) {
|
|
241
|
+
// 写操作:heredoc 传内容,不用 2>&1(cat > file 不产生 stdout)
|
|
242
|
+
const delim = "DSHW" + Math.random().toString(36).slice(2, 14);
|
|
243
|
+
toWrite = item.cmd + " <<'" + delim + "'\n" + String(item.stdinData) + "\n" + delim + "\nprintf '\\n" + this.sentinel + "%s\\n' $?\n";
|
|
244
|
+
} else {
|
|
245
|
+
// 读/列举/搜索:2>&1 合并 stderr(成功时 stderr 为空)
|
|
246
|
+
toWrite = "{ " + item.cmd + " ; } 2>&1\nprintf '\\n" + this.sentinel + "%s\\n' $?\n";
|
|
247
|
+
}
|
|
248
|
+
this.handle.stdin.write(toWrite);
|
|
249
|
+
} catch (e) {
|
|
250
|
+
this.current = null;
|
|
251
|
+
item.reject(e);
|
|
252
|
+
this._next();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async exec(cmd, stdinData) {
|
|
257
|
+
await this.connect();
|
|
258
|
+
this.lastUsed = Date.now();
|
|
259
|
+
return new Promise((resolve, reject) => {
|
|
260
|
+
this.queue.push({ cmd, stdinData, resolve, reject });
|
|
261
|
+
this._next();
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
close() {
|
|
266
|
+
this.alive = false;
|
|
267
|
+
if (this.current) { this.current.reject(new Error("session closed")); this.current = null; }
|
|
268
|
+
while (this.queue.length) this.queue.shift().reject(new Error("session closed"));
|
|
269
|
+
if (this.handle) { try { this.handle.terminate(); } catch (e) {} this.handle = null; }
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
158
273
|
function sshArgv(p, remoteCmd, tty) {
|
|
159
274
|
const opts = ["ssh"];
|
|
160
275
|
if (tty) opts.push("-tt");
|
|
@@ -214,10 +329,10 @@ async function runRemote(subprocess, p, remoteCmd, stdinData, maxBytes) {
|
|
|
214
329
|
};
|
|
215
330
|
}
|
|
216
331
|
|
|
217
|
-
async function remoteListDir(
|
|
332
|
+
async function remoteListDir(runner, p, path) {
|
|
218
333
|
const target = path || p.remoteRoot || "~";
|
|
219
334
|
const script = "cd " + shellQuotePath(target) + " 2>/dev/null || { echo '__DSH_ERR__ cannot cd'; exit 1; }; find . -maxdepth 1 -mindepth 1 -printf '%Y\\t%f\\t%s\\n' 2>/dev/null | sort";
|
|
220
|
-
const r = await
|
|
335
|
+
const r = await runner(p, script, undefined, MAX_BYTES);
|
|
221
336
|
if (!r.ok) return { ok: false, error: (r.error || r.stderr || "").trim() || "读取目录失败" };
|
|
222
337
|
const entries = [];
|
|
223
338
|
const lines = String(r.stdout || "").split("\n");
|
|
@@ -236,9 +351,9 @@ async function remoteListDir(subprocess, p, path) {
|
|
|
236
351
|
return { ok: true, path: target, entries: entries };
|
|
237
352
|
}
|
|
238
353
|
|
|
239
|
-
async function remoteReadFile(
|
|
354
|
+
async function remoteReadFile(runner, p, path) {
|
|
240
355
|
if (!path) return { ok: false, error: "path 为必填项" };
|
|
241
|
-
const r = await
|
|
356
|
+
const r = await runner(p, "base64 -w0 " + shellQuotePath(path), undefined, MAX_BYTES);
|
|
242
357
|
if (!r.ok) return r;
|
|
243
358
|
let text = "";
|
|
244
359
|
let binary = false;
|
|
@@ -253,10 +368,10 @@ async function remoteReadFile(subprocess, p, path) {
|
|
|
253
368
|
return { ok: true, path: path, content: text, binary: binary, truncated: r.truncated };
|
|
254
369
|
}
|
|
255
370
|
|
|
256
|
-
async function remoteWriteFile(
|
|
371
|
+
async function remoteWriteFile(runner, p, path, content) {
|
|
257
372
|
if (!path) return { ok: false, error: "path 为必填项" };
|
|
258
373
|
const c = content !== undefined ? String(content) : "";
|
|
259
|
-
return await
|
|
374
|
+
return await runner(p, "cat > " + shellQuotePath(path), c, MAX_BYTES);
|
|
260
375
|
}
|
|
261
376
|
|
|
262
377
|
/**
|
|
@@ -264,7 +379,7 @@ async function remoteWriteFile(subprocess, p, path, content) {
|
|
|
264
379
|
* 但用通用的 GNU grep(超算/Linux 通用),不依赖 ripgrep。
|
|
265
380
|
* exit 1 = 无匹配(ok,空结果);exit 0 = 有匹配;exit 2 = 出错。
|
|
266
381
|
*/
|
|
267
|
-
async function remoteGrep(
|
|
382
|
+
async function remoteGrep(runner, p, pattern, path, opts) {
|
|
268
383
|
opts = opts || {};
|
|
269
384
|
const target = path || p.remoteRoot || "~";
|
|
270
385
|
const parts = ["grep", "-rnIE"];
|
|
@@ -272,7 +387,7 @@ async function remoteGrep(subprocess, p, pattern, path, opts) {
|
|
|
272
387
|
if (opts.include) parts.push("--include=" + String(opts.include));
|
|
273
388
|
parts.push("--", shellQuote(pattern), shellQuotePath(target));
|
|
274
389
|
const cmd = parts.join(" ");
|
|
275
|
-
const r = await
|
|
390
|
+
const r = await runner(p, cmd, undefined, MAX_BYTES);
|
|
276
391
|
if (r.exitCode === 1 || (r.ok && !r.stdout)) {
|
|
277
392
|
return { ok: true, pattern: pattern, path: target, matches: [], truncated: false, error: "" };
|
|
278
393
|
}
|
|
@@ -304,14 +419,14 @@ async function remoteGrep(subprocess, p, pattern, path, opts) {
|
|
|
304
419
|
* 在远端按 glob 模式查找文件(find -name)。find 已递归;pattern 为 basename 通配
|
|
305
420
|
* (如 .py 后缀),自动剥离前导双星号-斜杠与星号-斜杠以适配 POSIX find -name。
|
|
306
421
|
*/
|
|
307
|
-
async function remoteGlob(
|
|
422
|
+
async function remoteGlob(runner, p, pattern, path, opts) {
|
|
308
423
|
opts = opts || {};
|
|
309
424
|
const target = path || p.remoteRoot || "~";
|
|
310
425
|
let pat = String(pattern || "").trim();
|
|
311
426
|
pat = pat.replace(/^(\*\*\/|\*\/)+/, "");
|
|
312
427
|
const max = parseInt(String(opts.maxResults || 500), 10) || 500;
|
|
313
428
|
const cmd = "find " + shellQuotePath(target) + " -name " + shellQuote(pat) + " -printf '%p\\n' 2>/dev/null | sort | head -n " + max;
|
|
314
|
-
const r = await
|
|
429
|
+
const r = await runner(p, cmd, undefined, MAX_BYTES);
|
|
315
430
|
const files = String(r.stdout || "").split("\n").filter(function (s) { return !!s; });
|
|
316
431
|
return { ok: true, pattern: pattern, path: target, files: files, truncated: !!r.truncated, error: "" };
|
|
317
432
|
}
|
|
@@ -530,6 +645,40 @@ function apply(ctx, config) {
|
|
|
530
645
|
const terminals = new Map();
|
|
531
646
|
let nextTerminalId = 1;
|
|
532
647
|
|
|
648
|
+
// ---- 持久 SSH 会话池(连接复用,避免每次操作都做完整 SSH 握手)----
|
|
649
|
+
const sessions = new Map(); // profileKey -> CommandSession
|
|
650
|
+
const SESSION_IDLE_MS = 10 * 60 * 1000;
|
|
651
|
+
function profileKey(p) {
|
|
652
|
+
return String(p.id) + "|" + String(p.host) + ":" + String(p.port || 22) + "|" + String(p.user || "");
|
|
653
|
+
}
|
|
654
|
+
function getSession(p) {
|
|
655
|
+
const key = profileKey(p);
|
|
656
|
+
let s = sessions.get(key);
|
|
657
|
+
if (!s) { s = new CommandSession(subprocess, p); sessions.set(key, s); }
|
|
658
|
+
return s;
|
|
659
|
+
}
|
|
660
|
+
/** 池化执行:复用持久会话,失败时回退到一次性 runRemote。 */
|
|
661
|
+
async function runPooled(p, cmd, stdinData, maxBytes) {
|
|
662
|
+
try {
|
|
663
|
+
const s = getSession(p);
|
|
664
|
+
const r = await s.exec(cmd, stdinData);
|
|
665
|
+
return { ok: r.exitCode === 0, exitCode: r.exitCode, stdout: r.stdout, stderr: "", error: r.exitCode !== 0 ? String(r.stdout).trim().slice(0, 500) : "", truncated: false };
|
|
666
|
+
} catch (e) {
|
|
667
|
+
// 会话挂了 —— 清理并回退到一次性连接
|
|
668
|
+
const key = profileKey(p);
|
|
669
|
+
const old = sessions.get(key);
|
|
670
|
+
if (old) { old.close(); sessions.delete(key); }
|
|
671
|
+
return runRemote(subprocess, p, cmd, stdinData, maxBytes);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
// 定期清理空闲会话
|
|
675
|
+
const idleTimer = setInterval(function () {
|
|
676
|
+
const now = Date.now();
|
|
677
|
+
for (const [key, s] of sessions) {
|
|
678
|
+
if (now - s.lastUsed > SESSION_IDLE_MS) { s.close(); sessions.delete(key); }
|
|
679
|
+
}
|
|
680
|
+
}, 60 * 1000);
|
|
681
|
+
|
|
533
682
|
// ---- settings 持久化 ----
|
|
534
683
|
let settingsFace = {
|
|
535
684
|
read: () => ({ profiles: [], workspaces: [] }),
|
|
@@ -728,45 +877,45 @@ function apply(ctx, config) {
|
|
|
728
877
|
listDir: async (args) => {
|
|
729
878
|
const p = resolveProfile(args);
|
|
730
879
|
if (!p) return { ok: false, error: "需要 profileId 或 host+user" };
|
|
731
|
-
return await remoteListDir(
|
|
880
|
+
return await remoteListDir(runPooled, p, args && args.path);
|
|
732
881
|
},
|
|
733
882
|
readFile: async (args) => {
|
|
734
883
|
const p = resolveProfile(args);
|
|
735
884
|
if (!p) return { ok: false, error: "需要 profileId 或 host+user" };
|
|
736
|
-
return await remoteReadFile(
|
|
885
|
+
return await remoteReadFile(runPooled, p, args && args.path);
|
|
737
886
|
},
|
|
738
887
|
writeFile: async (args) => {
|
|
739
888
|
const p = resolveProfile(args);
|
|
740
889
|
if (!p) return { ok: false, error: "需要 profileId 或 host+user" };
|
|
741
|
-
return await remoteWriteFile(
|
|
890
|
+
return await remoteWriteFile(runPooled, p, args && args.path, args && args.content);
|
|
742
891
|
},
|
|
743
892
|
grep: async (args) => {
|
|
744
893
|
const p = resolveProfile(args);
|
|
745
894
|
if (!p) return { ok: false, error: "需要 profileId 或 host+user" };
|
|
746
|
-
return await remoteGrep(
|
|
895
|
+
return await remoteGrep(runPooled, p, args && args.pattern, args && args.path, args || {});
|
|
747
896
|
},
|
|
748
897
|
glob: async (args) => {
|
|
749
898
|
const p = resolveProfile(args);
|
|
750
899
|
if (!p) return { ok: false, error: "需要 profileId 或 host+user" };
|
|
751
|
-
return await remoteGlob(
|
|
900
|
+
return await remoteGlob(runPooled, p, args && args.pattern, args && args.path, args || {});
|
|
752
901
|
},
|
|
753
902
|
mkdir: async (args) => {
|
|
754
903
|
const p = resolveProfile(args);
|
|
755
904
|
if (!p) return { ok: false, error: "需要 profileId 或 host+user" };
|
|
756
905
|
if (!args || !args.path) return { ok: false, error: "path 为必填项" };
|
|
757
|
-
return await
|
|
906
|
+
return await runPooled(p, "mkdir -p " + shellQuotePath(args.path), undefined, undefined);
|
|
758
907
|
},
|
|
759
908
|
deleteFile: async (args) => {
|
|
760
909
|
const p = resolveProfile(args);
|
|
761
910
|
if (!p) return { ok: false, error: "需要 profileId 或 host+user" };
|
|
762
911
|
if (!args || !args.path) return { ok: false, error: "path 为必填项" };
|
|
763
|
-
return await
|
|
912
|
+
return await runPooled(p, "rm -rf " + shellQuotePath(args.path), undefined, undefined);
|
|
764
913
|
},
|
|
765
914
|
move: async (args) => {
|
|
766
915
|
const p = resolveProfile(args);
|
|
767
916
|
if (!p) return { ok: false, error: "需要 profileId 或 host+user" };
|
|
768
917
|
if (!args || !args.src || !args.dst) return { ok: false, error: "src 和 dst 为必填项" };
|
|
769
|
-
return await
|
|
918
|
+
return await runPooled(p, "mv " + shellQuotePath(args.src) + " " + shellQuotePath(args.dst), undefined, undefined);
|
|
770
919
|
},
|
|
771
920
|
/** 远端 → 本地镜像(按工作区)。 */
|
|
772
921
|
syncDown: async (args) => {
|
|
@@ -934,7 +1083,7 @@ function apply(ctx, config) {
|
|
|
934
1083
|
execute: async function (args, exec) {
|
|
935
1084
|
const tc = toolContext(args, exec);
|
|
936
1085
|
if (!tc.profile) return { ok: false, path: "", entries: [], error: "需要 profileId 或 host+user(当前会话非远程工作区时必填)" };
|
|
937
|
-
const r = await remoteListDir(
|
|
1086
|
+
const r = await remoteListDir(runPooled, tc.profile, resolveRemotePath(args.path, tc.remotePath));
|
|
938
1087
|
return { ok: !!r.ok, path: r.path || "", entries: r.entries || [], error: r.error || "" };
|
|
939
1088
|
}
|
|
940
1089
|
});
|
|
@@ -959,7 +1108,7 @@ function apply(ctx, config) {
|
|
|
959
1108
|
execute: async function (args, exec) {
|
|
960
1109
|
const tc = toolContext(args, exec);
|
|
961
1110
|
if (!tc.profile) return { ok: false, path: "", content: "", binary: false, truncated: false, error: "需要 profileId 或 host+user(当前会话非远程工作区时必填)" };
|
|
962
|
-
const r = await remoteReadFile(
|
|
1111
|
+
const r = await remoteReadFile(runPooled, tc.profile, resolveRemotePath(args.path, tc.remotePath));
|
|
963
1112
|
return { ok: !!r.ok, path: r.path || "", content: r.content || "", binary: !!r.binary, truncated: !!r.truncated, error: r.error || "" };
|
|
964
1113
|
}
|
|
965
1114
|
});
|
|
@@ -978,7 +1127,7 @@ function apply(ctx, config) {
|
|
|
978
1127
|
execute: async function (args, exec) {
|
|
979
1128
|
const tc = toolContext(args, exec);
|
|
980
1129
|
if (!tc.profile) return normExec({ ok: false, error: "需要 profileId 或 host+user(当前会话非远程工作区时必填)" });
|
|
981
|
-
return normExec(await remoteWriteFile(
|
|
1130
|
+
return normExec(await remoteWriteFile(runPooled, tc.profile, resolveRemotePath(args.path, tc.remotePath), args.content));
|
|
982
1131
|
}
|
|
983
1132
|
});
|
|
984
1133
|
|
|
@@ -1059,7 +1208,7 @@ function apply(ctx, config) {
|
|
|
1059
1208
|
const tc = toolContext(args, exec);
|
|
1060
1209
|
if (!tc.profile) return { ok: false, pattern: "", path: "", matches: [], truncated: false, error: "需要 profileId 或 host+user(当前会话非远程工作区时必填)" };
|
|
1061
1210
|
if (!args.pattern) return { ok: false, pattern: "", path: "", matches: [], truncated: false, error: "pattern 为必填项" };
|
|
1062
|
-
const r = await remoteGrep(
|
|
1211
|
+
const r = await remoteGrep(runPooled, tc.profile, args.pattern, resolveRemotePath(args.path, tc.remotePath), args);
|
|
1063
1212
|
return { ok: !!r.ok, pattern: r.pattern || args.pattern, path: r.path || "", matches: r.matches || [], truncated: !!r.truncated, error: r.error || "" };
|
|
1064
1213
|
}
|
|
1065
1214
|
});
|
|
@@ -1095,7 +1244,7 @@ function apply(ctx, config) {
|
|
|
1095
1244
|
const tc = toolContext(args, exec);
|
|
1096
1245
|
if (!tc.profile) return { ok: false, pattern: "", path: "", files: [], truncated: false, error: "需要 profileId 或 host+user(当前会话非远程工作区时必填)" };
|
|
1097
1246
|
if (!args.pattern) return { ok: false, pattern: "", path: "", files: [], truncated: false, error: "pattern 为必填项" };
|
|
1098
|
-
const r = await remoteGlob(
|
|
1247
|
+
const r = await remoteGlob(runPooled, tc.profile, args.pattern, resolveRemotePath(args.path, tc.remotePath), args);
|
|
1099
1248
|
return { ok: !!r.ok, pattern: r.pattern || args.pattern, path: r.path || "", files: r.files || [], truncated: !!r.truncated, error: r.error || "" };
|
|
1100
1249
|
}
|
|
1101
1250
|
});
|
|
@@ -1111,7 +1260,7 @@ function apply(ctx, config) {
|
|
|
1111
1260
|
if (!tc.profile) return normExec({ ok: false, error: "需要 profileId 或 host+user(当前会话非远程工作区时必填)" });
|
|
1112
1261
|
if (!args.path) return normExec({ ok: false, error: "path 为必填项" });
|
|
1113
1262
|
let cmd = "mkdir -p " + shellQuotePath(resolveRemotePath(args.path, tc.remotePath));
|
|
1114
|
-
return normExec(await
|
|
1263
|
+
return normExec(await runPooled(tc.profile, cmd, undefined, undefined));
|
|
1115
1264
|
}
|
|
1116
1265
|
});
|
|
1117
1266
|
|
|
@@ -1126,7 +1275,7 @@ function apply(ctx, config) {
|
|
|
1126
1275
|
if (!tc.profile) return normExec({ ok: false, error: "需要 profileId 或 host+user(当前会话非远程工作区时必填)" });
|
|
1127
1276
|
if (!args.path) return normExec({ ok: false, error: "path 为必填项" });
|
|
1128
1277
|
let cmd = "rm -rf " + shellQuotePath(resolveRemotePath(args.path, tc.remotePath));
|
|
1129
|
-
return normExec(await
|
|
1278
|
+
return normExec(await runPooled(tc.profile, cmd, undefined, undefined));
|
|
1130
1279
|
}
|
|
1131
1280
|
});
|
|
1132
1281
|
|
|
@@ -1144,12 +1293,15 @@ function apply(ctx, config) {
|
|
|
1144
1293
|
if (!tc.profile) return normExec({ ok: false, error: "需要 profileId 或 host+user(当前会话非远程工作区时必填)" });
|
|
1145
1294
|
if (!args.src || !args.dst) return normExec({ ok: false, error: "src 和 dst 为必填项" });
|
|
1146
1295
|
let cmd = "mv " + shellQuotePath(resolveRemotePath(args.src, tc.remotePath)) + " " + shellQuotePath(resolveRemotePath(args.dst, tc.remotePath));
|
|
1147
|
-
return normExec(await
|
|
1296
|
+
return normExec(await runPooled(tc.profile, cmd, undefined, undefined));
|
|
1148
1297
|
}
|
|
1149
1298
|
});
|
|
1150
1299
|
|
|
1151
1300
|
// ---- 清理 ----
|
|
1152
1301
|
ctx.effect(() => () => {
|
|
1302
|
+
clearInterval(idleTimer);
|
|
1303
|
+
sessions.forEach(function (s) { try { s.close(); } catch (e) {} });
|
|
1304
|
+
sessions.clear();
|
|
1153
1305
|
terminals.forEach(function (s) { try { s.handle.terminate(); } catch (e) {} });
|
|
1154
1306
|
terminals.clear();
|
|
1155
1307
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhangfengshun/dsh-remote-ssh",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "DSH web plugin: VSCode Remote-SSH-like remote development (SSH to supercomputers/servers, remote workspace, file explorer, integrated terminal), integrated with dsh-better-sidebar and DSH settings.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|