dsh-winrm 0.4.0 → 0.4.1
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/README.en.md +3 -1
- package/README.md +4 -2
- package/lib/client.js +82 -82
- package/lib/index.js +72 -26
- package/lib/types/engine/client.d.ts +30 -4
- package/lib/types/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/engine/client.ts +66 -16
- package/src/engine.ts +9 -2
- package/src/index.ts +1 -1
- package/src/smb.ts +4 -1
- package/src/tools.ts +3 -3
package/README.en.md
CHANGED
|
@@ -123,8 +123,10 @@ Transport relies on the native Node.js [winrm-client](https://github.com/shide19
|
|
|
123
123
|
- The console is a command session: it does not preserve PowerShell variables or the current directory, and every command runs independently
|
|
124
124
|
- Uploads go over **one stdin stream**: a single shell, a single command, one `Send` per 256 KB, with per-chunk gzip for compressible content (a 1.7 MB JSON fixture travels as 91 KB on the wire). The old 48 KB chunked path is kept as a fallback, used only when the streaming channel is unavailable
|
|
125
125
|
- Downloads go over **parallel byte ranges**: up to four workers by default, each with its own shell reading an absolute byte range of the same file, compressed on the remote side and written locally at the matching offset (measured: 2 MiB in about 10-12 s, 16 MiB in about 21-23 s, 16 MiB of compressible content in about 6 s; the old 48 KB chunked path needed 129 s for 2 MiB and remains as a fallback). A single server `Receive` returns at most 128 KB regardless of the envelope size, so downloads are round-trip bound rather than bandwidth bound
|
|
126
|
-
- `channel=auto` (the default) probes port 445 first: `net use` has no short timeout of its own and takes about 60 s to fail when SMB is unavailable, and that wait used to happen before every upload. It is now decided within 3 s and the transfer goes straight over WinRM
|
|
126
|
+
- `channel=auto` (the default) probes port 445 first: `net use` has no short timeout of its own and takes about 60 s to fail when SMB is unavailable, and that wait used to happen before every upload. It is now decided within 3 s and the transfer goes straight over WinRM. An explicit `channel=smb` on a host whose 445 is closed fails immediately and names `winrm` as the way out
|
|
127
|
+
- A streaming transfer is bounded by **whether bytes are still moving**, not by a fixed deadline for the whole transfer: it keeps going as long as data arrives, and only a range that delivers nothing for 180 s is treated as stalled. A 700 MiB file therefore completes normally at the ~0.73 MiB/s this link measures even though it takes about sixteen minutes; the absolute cap exists only so a wedged transfer cannot stay alive forever, and it scales with the payload
|
|
127
128
|
- A failed download leaves no residue: bytes already written are discarded and the target file is removed. If the failure happens before any byte arrives (missing path, target is a directory, host unreachable), an existing local file is left untouched — the target file is only created on the first write
|
|
129
|
+
- When an attempt is abandoned mid-transfer (for example a failed auth candidate retried from the start), the sender process on the target can outlive it and keep the file open until its shell is reclaimed; a transfer that finishes normally exits on its own. If a remote file refuses to delete, end the corresponding `powershell.exe` sender on the target first
|
|
128
130
|
- A single command times out after 60 s by default; pass `timeoutMs` to change it
|
|
129
131
|
- The target machine must already have WinRM enabled (see above); cleartext HTTP Basic must not be used on the public internet
|
|
130
132
|
- Closing and reopening the panel returns to the Hosts tab: the panel occupies the `main` slot, so it unmounts when you switch back to the conversation and the active tab is not preserved across toggles
|
package/README.md
CHANGED
|
@@ -123,8 +123,10 @@ scripts/
|
|
|
123
123
|
- 控制台是命令会话,不保持 PowerShell 变量和当前目录状态;每条命令独立执行
|
|
124
124
|
- 上传走**单条 stdin 流**:一个 shell、一个命令,每 256KB 一个 `Send`,可压缩内容自动按块 gzip(实测 1.7MB JSON 线上仅 91KB)。旧的 48KB 分块路径保留为回退,仅在流式通道不可用时启用
|
|
125
125
|
- 下载走**并行区间流**:默认最多 4 个 worker,各自一个 shell 读同一文件的绝对字节区间,远端 gzip 后回传,本地按 offset 落位(实测 2MiB 约 10-12 秒,16MiB 约 21-23 秒,可压缩的 16MiB 约 6 秒;旧的 48KB 分块路径 2MiB 需 129 秒,保留为回退)。服务端单次 `Receive` 最多回 128KB,与信封大小无关,所以下载是往返次数受限而非带宽受限
|
|
126
|
-
- `channel=auto`(默认)先探测目标机 445 端口:`net use` 自身没有短超时,SMB 不可用时要等约 60 秒才失败,此前这段等待发生在每次上传之前。现在 3 秒内判定并直接走 WinRM
|
|
127
|
-
-
|
|
126
|
+
- `channel=auto`(默认)先探测目标机 445 端口:`net use` 自身没有短超时,SMB 不可用时要等约 60 秒才失败,此前这段等待发生在每次上传之前。现在 3 秒内判定并直接走 WinRM;显式 `channel=smb` 且 445 未开放时立即报错,并提示改用 `winrm`
|
|
127
|
+
- 流式传输以**字节是否还在流动**判定超时,而不是给整次传输一个固定时限:只要还有数据到达就继续,某条区间连续 180 秒没有任何字节才判失败。因此大文件(实测 700MiB)在该链路的约 0.73MiB/s 速率下会跑十几分钟并正常完成;绝对上限只是防止传输卡死后永久占用,按载荷大小放大
|
|
128
|
+
- 下载失败不留残留文件:已写入的数据会被丢弃、目标文件被删除;若失败发生在任何字节到达之前(路径不存在、目标是目录、主机不可达),已存在的本地文件保持原样不动(目标文件在首次写入时才创建)
|
|
129
|
+
- 一次传输被中途放弃时(例如某次认证尝试失败后从头重试),目标机上的发送进程可能残留并继续占用该文件,直到其 shell 被回收;正常结束的传输会自行退出。遇到远端文件删不掉时,先结束目标机上对应的 `powershell.exe` 发送进程再删
|
|
128
130
|
- 单命令默认 60s 超时,可传 `timeoutMs`
|
|
129
131
|
- 目标机需已启用 WinRM(见上);HTTP 明文 Basic 不应用于公网
|
|
130
132
|
- 面板关闭再打开会回到「主机」页签:面板是 `main` 插槽的占用者,切回对话时卸载,页签状态不跨开关保留
|
package/lib/client.js
CHANGED
|
@@ -707,17 +707,17 @@ if (typeof document !== "undefined" && document.querySelector("style[data-plugin
|
|
|
707
707
|
document.head.appendChild(tag);
|
|
708
708
|
}
|
|
709
709
|
var cards_module_css_default = {
|
|
710
|
+
"diffStat": "X_s05W_diffStat",
|
|
710
711
|
"rowError": "X_s05W_rowError",
|
|
711
|
-
"sep": "X_s05W_sep",
|
|
712
|
-
"removed": "X_s05W_removed",
|
|
713
|
-
"meta": "X_s05W_meta",
|
|
714
|
-
"summary": "X_s05W_summary",
|
|
715
712
|
"detail": "X_s05W_detail",
|
|
716
|
-
"wrap": "X_s05W_wrap",
|
|
717
713
|
"added": "X_s05W_added",
|
|
718
|
-
"
|
|
719
|
-
"
|
|
720
|
-
"
|
|
714
|
+
"note": "X_s05W_note",
|
|
715
|
+
"removed": "X_s05W_removed",
|
|
716
|
+
"wrap": "X_s05W_wrap",
|
|
717
|
+
"summary": "X_s05W_summary",
|
|
718
|
+
"sep": "X_s05W_sep",
|
|
719
|
+
"meta": "X_s05W_meta",
|
|
720
|
+
"title": "X_s05W_title"
|
|
721
721
|
};
|
|
722
722
|
//#endregion
|
|
723
723
|
//#region src/client/tool/cards.tsx
|
|
@@ -1080,93 +1080,93 @@ if (typeof document !== "undefined" && document.querySelector("style[data-plugin
|
|
|
1080
1080
|
document.head.appendChild(tag);
|
|
1081
1081
|
}
|
|
1082
1082
|
var panel_module_css_default = {
|
|
1083
|
-
"
|
|
1084
|
-
"
|
|
1085
|
-
"tunnelRow": "hAbY3W_tunnelRow",
|
|
1086
|
-
"actions": "hAbY3W_actions",
|
|
1087
|
-
"primaryButton": "hAbY3W_primaryButton",
|
|
1088
|
-
"search": "hAbY3W_search",
|
|
1089
|
-
"cellPre": "hAbY3W_cellPre",
|
|
1090
|
-
"consoleOutput": "hAbY3W_consoleOutput",
|
|
1091
|
-
"controls": "hAbY3W_controls",
|
|
1092
|
-
"utf8Note": "hAbY3W_utf8Note",
|
|
1093
|
-
"formCard": "hAbY3W_formCard",
|
|
1094
|
-
"inlineTest": "hAbY3W_inlineTest",
|
|
1095
|
-
"iconButton": "hAbY3W_iconButton",
|
|
1096
|
-
"loading": "hAbY3W_loading",
|
|
1097
|
-
"browsePanel": "hAbY3W_browsePanel",
|
|
1098
|
-
"clusterForm": "hAbY3W_clusterForm",
|
|
1099
|
-
"spinner": "hAbY3W_spinner",
|
|
1100
|
-
"groupSection": "hAbY3W_groupSection",
|
|
1101
|
-
"modal": "hAbY3W_modal",
|
|
1102
|
-
"radioRow": "hAbY3W_radioRow",
|
|
1103
|
-
"consoleWrap": "hAbY3W_consoleWrap",
|
|
1104
|
-
"groupChevron": "hAbY3W_groupChevron",
|
|
1105
|
-
"table": "hAbY3W_table",
|
|
1106
|
-
"empty": "hAbY3W_empty",
|
|
1107
|
-
"toolbar": "hAbY3W_toolbar",
|
|
1108
|
-
"termWrap": "hAbY3W_termWrap",
|
|
1109
|
-
"dirType": "hAbY3W_dirType",
|
|
1110
|
-
"cellDetails": "hAbY3W_cellDetails",
|
|
1111
|
-
"badge": "hAbY3W_badge",
|
|
1112
|
-
"progressTrack": "hAbY3W_progressTrack",
|
|
1113
|
-
"panelTitle": "hAbY3W_panelTitle",
|
|
1114
|
-
"termContainer": "hAbY3W_termContainer",
|
|
1083
|
+
"linkButton": "hAbY3W_linkButton",
|
|
1084
|
+
"browsePath": "hAbY3W_browsePath",
|
|
1115
1085
|
"dirRow": "hAbY3W_dirRow",
|
|
1116
|
-
"
|
|
1117
|
-
"
|
|
1118
|
-
"
|
|
1119
|
-
"
|
|
1120
|
-
"dialog": "hAbY3W_dialog",
|
|
1086
|
+
"dirType": "hAbY3W_dirType",
|
|
1087
|
+
"toolbarSpacer": "hAbY3W_toolbarSpacer",
|
|
1088
|
+
"dialogOverlay": "hAbY3W_dialogOverlay",
|
|
1089
|
+
"table": "hAbY3W_table",
|
|
1121
1090
|
"backButton": "hAbY3W_backButton",
|
|
1122
|
-
"
|
|
1091
|
+
"ghostButton": "hAbY3W_ghostButton",
|
|
1092
|
+
"dirName": "hAbY3W_dirName",
|
|
1093
|
+
"groupBySelect": "hAbY3W_groupBySelect",
|
|
1094
|
+
"dirSize": "hAbY3W_dirSize",
|
|
1095
|
+
"tabBody": "hAbY3W_tabBody",
|
|
1096
|
+
"tunnelLabel": "hAbY3W_tunnelLabel",
|
|
1097
|
+
"clusterForm": "hAbY3W_clusterForm",
|
|
1098
|
+
"checkRow": "hAbY3W_checkRow",
|
|
1123
1099
|
"formError": "hAbY3W_formError",
|
|
1124
|
-
"groupHeader": "hAbY3W_groupHeader",
|
|
1125
1100
|
"progressBar": "hAbY3W_progressBar",
|
|
1126
|
-
"
|
|
1127
|
-
"
|
|
1128
|
-
"
|
|
1129
|
-
"
|
|
1130
|
-
"
|
|
1131
|
-
"
|
|
1132
|
-
"
|
|
1133
|
-
"
|
|
1134
|
-
"
|
|
1135
|
-
"modalFooter": "hAbY3W_modalFooter",
|
|
1101
|
+
"banner": "hAbY3W_banner",
|
|
1102
|
+
"modal": "hAbY3W_modal",
|
|
1103
|
+
"groupName": "hAbY3W_groupName",
|
|
1104
|
+
"input": "hAbY3W_input",
|
|
1105
|
+
"entryIcon": "hAbY3W_entryIcon",
|
|
1106
|
+
"hint": "hAbY3W_hint",
|
|
1107
|
+
"fillBody": "hAbY3W_fillBody",
|
|
1108
|
+
"transferBlock": "hAbY3W_transferBlock",
|
|
1109
|
+
"groupSection": "hAbY3W_groupSection",
|
|
1136
1110
|
"modalTitle": "hAbY3W_modalTitle",
|
|
1111
|
+
"progressTrack": "hAbY3W_progressTrack",
|
|
1112
|
+
"actions": "hAbY3W_actions",
|
|
1137
1113
|
"tab": "hAbY3W_tab",
|
|
1138
|
-
"
|
|
1139
|
-
"
|
|
1114
|
+
"utf8Note": "hAbY3W_utf8Note",
|
|
1115
|
+
"panelTitle": "hAbY3W_panelTitle",
|
|
1116
|
+
"consoleOutput": "hAbY3W_consoleOutput",
|
|
1117
|
+
"hiddenFile": "hAbY3W_hiddenFile",
|
|
1118
|
+
"tunnelRow": "hAbY3W_tunnelRow",
|
|
1119
|
+
"groupCount": "hAbY3W_groupCount",
|
|
1120
|
+
"spinner": "hAbY3W_spinner",
|
|
1121
|
+
"termPlaceholder": "hAbY3W_termPlaceholder",
|
|
1122
|
+
"mono": "hAbY3W_mono",
|
|
1123
|
+
"modalBackdrop": "hAbY3W_modalBackdrop",
|
|
1124
|
+
"cellDetails": "hAbY3W_cellDetails",
|
|
1140
1125
|
"termBody": "hAbY3W_termBody",
|
|
1141
|
-
"
|
|
1142
|
-
"
|
|
1126
|
+
"cellPre": "hAbY3W_cellPre",
|
|
1127
|
+
"fieldHint": "hAbY3W_fieldHint",
|
|
1143
1128
|
"tableWrap": "hAbY3W_tableWrap",
|
|
1144
|
-
"
|
|
1145
|
-
"
|
|
1146
|
-
"
|
|
1147
|
-
"checkRow": "hAbY3W_checkRow",
|
|
1148
|
-
"toolbarSpacer": "hAbY3W_toolbarSpacer",
|
|
1129
|
+
"dshSshSpin": "hAbY3W_dshSshSpin",
|
|
1130
|
+
"field": "hAbY3W_field",
|
|
1131
|
+
"fieldLabel": "hAbY3W_fieldLabel",
|
|
1149
1132
|
"commandInput": "hAbY3W_commandInput",
|
|
1150
|
-
"
|
|
1151
|
-
"groupBySelect": "hAbY3W_groupBySelect",
|
|
1152
|
-
"transferBlock": "hAbY3W_transferBlock",
|
|
1153
|
-
"fillBody": "hAbY3W_fillBody",
|
|
1154
|
-
"input": "hAbY3W_input",
|
|
1155
|
-
"tunnelLabel": "hAbY3W_tunnelLabel",
|
|
1133
|
+
"consoleWrap": "hAbY3W_consoleWrap",
|
|
1156
1134
|
"panel": "hAbY3W_panel",
|
|
1157
|
-
"groupCount": "hAbY3W_groupCount",
|
|
1158
|
-
"linkButton": "hAbY3W_linkButton",
|
|
1159
|
-
"formRow": "hAbY3W_formRow",
|
|
1160
1135
|
"dialogTitle": "hAbY3W_dialogTitle",
|
|
1161
|
-
"
|
|
1162
|
-
"
|
|
1163
|
-
"
|
|
1164
|
-
"
|
|
1136
|
+
"browseHeader": "hAbY3W_browseHeader",
|
|
1137
|
+
"inlineTest": "hAbY3W_inlineTest",
|
|
1138
|
+
"browseList": "hAbY3W_browseList",
|
|
1139
|
+
"consoleHeader": "hAbY3W_consoleHeader",
|
|
1140
|
+
"consoleInputRow": "hAbY3W_consoleInputRow",
|
|
1141
|
+
"formRow": "hAbY3W_formRow",
|
|
1142
|
+
"badge": "hAbY3W_badge",
|
|
1143
|
+
"clusterFilters": "hAbY3W_clusterFilters",
|
|
1144
|
+
"primaryButton": "hAbY3W_primaryButton",
|
|
1145
|
+
"toolbar": "hAbY3W_toolbar",
|
|
1146
|
+
"groupToggle": "hAbY3W_groupToggle",
|
|
1147
|
+
"modalFooter": "hAbY3W_modalFooter",
|
|
1148
|
+
"radioRow": "hAbY3W_radioRow",
|
|
1149
|
+
"progressMeta": "hAbY3W_progressMeta",
|
|
1150
|
+
"search": "hAbY3W_search",
|
|
1151
|
+
"browsePanel": "hAbY3W_browsePanel",
|
|
1152
|
+
"termWrap": "hAbY3W_termWrap",
|
|
1153
|
+
"entry": "hAbY3W_entry",
|
|
1154
|
+
"controls": "hAbY3W_controls",
|
|
1155
|
+
"empty": "hAbY3W_empty",
|
|
1156
|
+
"panelHeader": "hAbY3W_panelHeader",
|
|
1157
|
+
"loading": "hAbY3W_loading",
|
|
1158
|
+
"dialog": "hAbY3W_dialog",
|
|
1159
|
+
"formCard": "hAbY3W_formCard",
|
|
1165
1160
|
"panelContent": "hAbY3W_panelContent",
|
|
1166
|
-
"
|
|
1167
|
-
"
|
|
1161
|
+
"cellMuted": "hAbY3W_cellMuted",
|
|
1162
|
+
"tabBar": "hAbY3W_tabBar",
|
|
1163
|
+
"tunnelList": "hAbY3W_tunnelList",
|
|
1164
|
+
"entryLabel": "hAbY3W_entryLabel",
|
|
1165
|
+
"groupChevron": "hAbY3W_groupChevron",
|
|
1166
|
+
"iconButton": "hAbY3W_iconButton",
|
|
1168
1167
|
"radioLabel": "hAbY3W_radioLabel",
|
|
1169
|
-
"
|
|
1168
|
+
"groupHeader": "hAbY3W_groupHeader",
|
|
1169
|
+
"termContainer": "hAbY3W_termContainer"
|
|
1170
1170
|
};
|
|
1171
1171
|
//#endregion
|
|
1172
1172
|
//#region src/client/panel/ConsoleTab.tsx
|
package/lib/index.js
CHANGED
|
@@ -1307,6 +1307,41 @@ const STREAM_GZIP_MIN_RATIO = .85;
|
|
|
1307
1307
|
/** Per-Send HTTP budget; a single slice is small, so this only bounds a stalled connection. */
|
|
1308
1308
|
const STREAM_SEND_TIMEOUT_MS = 12e4;
|
|
1309
1309
|
/**
|
|
1310
|
+
* How long a streaming transfer may deliver nothing before it is treated as
|
|
1311
|
+
* stalled.
|
|
1312
|
+
*
|
|
1313
|
+
* A wall-clock cap on the whole transfer cannot work here: this transport is
|
|
1314
|
+
* latency-bound and measured at ~0.73 MiB/s against a real target, so a 700 MiB
|
|
1315
|
+
* file legitimately needs about sixteen minutes and any smaller fixed cap fails
|
|
1316
|
+
* a transfer that is making steady progress. What a transfer actually needs
|
|
1317
|
+
* bounding against is a *stall* — a range that has stopped producing bytes —
|
|
1318
|
+
* which is what this idle window measures.
|
|
1319
|
+
*/
|
|
1320
|
+
const STREAM_IDLE_TIMEOUT_MS = 18e4;
|
|
1321
|
+
/**
|
|
1322
|
+
* Floor used only to size the runaway backstop, never as a throughput target.
|
|
1323
|
+
* Deliberately pessimistic (64 KiB/s): it must not fail a slow link, only a
|
|
1324
|
+
* transfer that is somehow still "busy" hours after it should have finished.
|
|
1325
|
+
*/
|
|
1326
|
+
const STREAM_MIN_BYTES_PER_SEC = 65536;
|
|
1327
|
+
/**
|
|
1328
|
+
* Budget for one streaming transfer of `bytes` bytes.
|
|
1329
|
+
*
|
|
1330
|
+
* The idle window is the effective limit; the hard window exists so a wedged
|
|
1331
|
+
* transfer cannot stay alive forever, and it scales with the payload because
|
|
1332
|
+
* the payload is what sets the legitimate duration.
|
|
1333
|
+
* @param bytes - total bytes the transfer must move.
|
|
1334
|
+
* @param idleMs - how long the transfer may go without delivering anything.
|
|
1335
|
+
* @returns the idle window and the absolute backstop, both in milliseconds.
|
|
1336
|
+
*/
|
|
1337
|
+
function transferBudget(bytes, idleMs = STREAM_IDLE_TIMEOUT_MS) {
|
|
1338
|
+
const optimisticMs = Math.ceil(Math.max(0, bytes) / STREAM_MIN_BYTES_PER_SEC) * 1e3;
|
|
1339
|
+
return {
|
|
1340
|
+
idleMs,
|
|
1341
|
+
hardMs: Math.max(idleMs * 4, optimisticMs + idleMs)
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1310
1345
|
* One streaming transfer over a single shell: create → run the receiver →
|
|
1311
1346
|
* push the receiver script, the header frame and every data frame down stdin →
|
|
1312
1347
|
* receive the envelope → delete the shell.
|
|
@@ -1318,13 +1353,13 @@ const STREAM_SEND_TIMEOUT_MS = 12e4;
|
|
|
1318
1353
|
* @param remotePath - destination path on the target.
|
|
1319
1354
|
* @param data - the file contents to send.
|
|
1320
1355
|
* @param onProgress - called with cumulative bytes after each frame.
|
|
1321
|
-
* @param
|
|
1356
|
+
* @param budget - idle window and absolute backstop for the transfer.
|
|
1322
1357
|
* @param authMethod - the auth scheme for this attempt.
|
|
1323
1358
|
* @param sendHttp - the resolved library send function.
|
|
1324
1359
|
* @returns the number of file bytes the receiver acknowledged writing.
|
|
1325
1360
|
* @throws WinrmTimedOut when the deadline passes; other errors are transport failures.
|
|
1326
1361
|
*/
|
|
1327
|
-
async function streamOnce(conn, remotePath, data, onProgress,
|
|
1362
|
+
async function streamOnce(conn, remotePath, data, onProgress, budget, authMethod, sendHttp) {
|
|
1328
1363
|
const base = baseParams(conn, authMethod);
|
|
1329
1364
|
const target = {
|
|
1330
1365
|
host: conn.host,
|
|
@@ -1336,7 +1371,9 @@ async function streamOnce(conn, remotePath, data, onProgress, timeoutMs, authMet
|
|
|
1336
1371
|
useHttps: conn.useHttps ?? false,
|
|
1337
1372
|
rejectUnauthorized: conn.rejectUnauthorized ?? true
|
|
1338
1373
|
};
|
|
1339
|
-
const hardDeadline = Date.now() +
|
|
1374
|
+
const hardDeadline = Date.now() + budget.hardMs + SETUP_GRACE_MS;
|
|
1375
|
+
let lastProgressAt = Date.now();
|
|
1376
|
+
const stalled = () => Date.now() - lastProgressAt >= budget.idleMs;
|
|
1340
1377
|
const state = {
|
|
1341
1378
|
shellId: void 0,
|
|
1342
1379
|
timedOut: false,
|
|
@@ -1365,7 +1402,7 @@ async function streamOnce(conn, remotePath, data, onProgress, timeoutMs, authMet
|
|
|
1365
1402
|
let slice = STREAM_CHUNK_BYTES;
|
|
1366
1403
|
let ceiling = STREAM_CHUNK_BYTES;
|
|
1367
1404
|
while (offset < total) {
|
|
1368
|
-
if (state.settled ||
|
|
1405
|
+
if (state.settled || stalled()) throw new WinrmTimedOut();
|
|
1369
1406
|
const take = Math.min(slice, total - offset);
|
|
1370
1407
|
const chunk = data.subarray(offset, offset + take);
|
|
1371
1408
|
const gz = gzipSync(chunk, { level: 6 });
|
|
@@ -1382,6 +1419,7 @@ async function streamOnce(conn, remotePath, data, onProgress, timeoutMs, authMet
|
|
|
1382
1419
|
throw error;
|
|
1383
1420
|
}
|
|
1384
1421
|
offset += take;
|
|
1422
|
+
lastProgressAt = Date.now();
|
|
1385
1423
|
onProgress?.(offset, total);
|
|
1386
1424
|
if (slice < ceiling) slice = Math.min(ceiling, slice * 2);
|
|
1387
1425
|
}
|
|
@@ -1390,15 +1428,15 @@ async function streamOnce(conn, remotePath, data, onProgress, timeoutMs, authMet
|
|
|
1390
1428
|
shellId,
|
|
1391
1429
|
commandId
|
|
1392
1430
|
};
|
|
1393
|
-
const commandDeadline = Date.now() + timeoutMs;
|
|
1394
1431
|
let stdout = "";
|
|
1395
1432
|
let stderr = "";
|
|
1396
1433
|
for (;;) {
|
|
1397
|
-
if (state.settled ||
|
|
1434
|
+
if (state.settled || stalled()) throw new WinrmTimedOut();
|
|
1398
1435
|
const chunk = await Command.doReceiveOutputNonBlocking(receive);
|
|
1399
1436
|
stdout += chunk.output;
|
|
1400
1437
|
stderr += chunk.stderr;
|
|
1401
1438
|
if (chunk.isComplete) break;
|
|
1439
|
+
if (chunk.output !== "" || chunk.stderr !== "") lastProgressAt = Date.now();
|
|
1402
1440
|
}
|
|
1403
1441
|
const parsed = parseEnvelope(stdout);
|
|
1404
1442
|
if (parsed === null) throw new Error("streaming upload did not report completion: " + (stripClixml(stderr) || stdout).trim());
|
|
@@ -1443,16 +1481,17 @@ async function streamOnce(conn, remotePath, data, onProgress, timeoutMs, authMet
|
|
|
1443
1481
|
* @param remotePath - destination path on the target.
|
|
1444
1482
|
* @param data - the file contents to send.
|
|
1445
1483
|
* @param onProgress - called with cumulative bytes after each frame.
|
|
1446
|
-
* @param
|
|
1484
|
+
* @param idleMs - how long the transfer may go without moving a slice.
|
|
1447
1485
|
* @returns bytes written, or null when the streaming path is unavailable.
|
|
1448
1486
|
*/
|
|
1449
|
-
async function uploadStream(conn, remotePath, data, onProgress,
|
|
1487
|
+
async function uploadStream(conn, remotePath, data, onProgress, idleMs = STREAM_IDLE_TIMEOUT_MS) {
|
|
1450
1488
|
const sendHttp = loadSendHttp();
|
|
1451
1489
|
if (sendHttp === null) return null;
|
|
1490
|
+
const budget = transferBudget(data.length, idleMs);
|
|
1452
1491
|
const candidates = conn.useHttps ? ["basic", "ntlm"] : ["ntlm", "basic"];
|
|
1453
1492
|
let lastError;
|
|
1454
1493
|
for (const authMethod of candidates) try {
|
|
1455
|
-
return await streamOnce(conn, remotePath, data, onProgress,
|
|
1494
|
+
return await streamOnce(conn, remotePath, data, onProgress, budget, authMethod, sendHttp);
|
|
1456
1495
|
} catch (error) {
|
|
1457
1496
|
if (error instanceof WinrmTimedOut) throw error;
|
|
1458
1497
|
lastError = error;
|
|
@@ -1472,12 +1511,12 @@ const DOWNLOAD_RECEIVE_TIMEOUT_MS = 12e4;
|
|
|
1472
1511
|
* @param start - first byte offset of this worker's range.
|
|
1473
1512
|
* @param length - bytes this worker must deliver.
|
|
1474
1513
|
* @param onData - receives each decompressed piece together with its file offset.
|
|
1475
|
-
* @param
|
|
1514
|
+
* @param budget - idle window and absolute backstop for this worker's range.
|
|
1476
1515
|
* @param authMethod - the auth scheme for this attempt.
|
|
1477
1516
|
* @param sendHttp - the resolved library HTTP function.
|
|
1478
1517
|
* @returns the number of file bytes this worker delivered.
|
|
1479
1518
|
*/
|
|
1480
|
-
async function downloadWorkerOnce(conn, remotePath, start, length, onData,
|
|
1519
|
+
async function downloadWorkerOnce(conn, remotePath, start, length, onData, budget, authMethod, sendHttp) {
|
|
1481
1520
|
const base = baseParams(conn, authMethod);
|
|
1482
1521
|
const target = {
|
|
1483
1522
|
host: conn.host,
|
|
@@ -1489,7 +1528,9 @@ async function downloadWorkerOnce(conn, remotePath, start, length, onData, timeo
|
|
|
1489
1528
|
useHttps: conn.useHttps ?? false,
|
|
1490
1529
|
rejectUnauthorized: conn.rejectUnauthorized ?? true
|
|
1491
1530
|
};
|
|
1492
|
-
const hardDeadline = Date.now() +
|
|
1531
|
+
const hardDeadline = Date.now() + budget.hardMs + SETUP_GRACE_MS;
|
|
1532
|
+
let lastDataAt = Date.now();
|
|
1533
|
+
const stalled = () => Date.now() - lastDataAt >= budget.idleMs;
|
|
1493
1534
|
const state = {
|
|
1494
1535
|
shellId: void 0,
|
|
1495
1536
|
timedOut: false,
|
|
@@ -1524,7 +1565,7 @@ async function downloadWorkerOnce(conn, remotePath, start, length, onData, timeo
|
|
|
1524
1565
|
let stdoutEnded = false;
|
|
1525
1566
|
let exitCode;
|
|
1526
1567
|
while (!stdoutEnded) {
|
|
1527
|
-
if (state.settled ||
|
|
1568
|
+
if (state.settled || stalled()) throw new WinrmTimedOut();
|
|
1528
1569
|
const result = await receiveOutput(target, shellId, commandId, sendHttp, DOWNLOAD_RECEIVE_TIMEOUT_MS);
|
|
1529
1570
|
let sawData = false;
|
|
1530
1571
|
for (const stream of result.streams) {
|
|
@@ -1538,6 +1579,7 @@ async function downloadWorkerOnce(conn, remotePath, start, length, onData, timeo
|
|
|
1538
1579
|
if (stream.end) stdoutEnded = true;
|
|
1539
1580
|
} else stderr += stream.data.toString("utf8");
|
|
1540
1581
|
}
|
|
1582
|
+
if (sawData) lastDataAt = Date.now();
|
|
1541
1583
|
if (inflateError !== void 0) throw inflateError;
|
|
1542
1584
|
if (result.exitCode !== void 0) exitCode = result.exitCode;
|
|
1543
1585
|
if (!sawData && exitCode !== void 0) break;
|
|
@@ -1586,15 +1628,16 @@ async function downloadWorkerOnce(conn, remotePath, start, length, onData, timeo
|
|
|
1586
1628
|
* @param remotePath - file to read.
|
|
1587
1629
|
* @param onData - receives each decompressed piece with its absolute file offset.
|
|
1588
1630
|
* @param onProgress - receives cumulative bytes delivered and the total size.
|
|
1589
|
-
* @param
|
|
1631
|
+
* @param idleMs - how long a worker may go without delivering bytes.
|
|
1590
1632
|
* @returns the number of bytes delivered, or null when streaming is unavailable.
|
|
1591
1633
|
*/
|
|
1592
|
-
async function downloadStream(conn, remotePath, onData, onProgress,
|
|
1634
|
+
async function downloadStream(conn, remotePath, onData, onProgress, idleMs = STREAM_IDLE_TIMEOUT_MS) {
|
|
1593
1635
|
const sendHttp = loadSendHttp();
|
|
1594
1636
|
if (sendHttp === null) return null;
|
|
1595
1637
|
const total = await remoteFileSize(conn, remotePath);
|
|
1596
1638
|
if (total === null) throw new Error(`remote file not found: ${remotePath}`);
|
|
1597
1639
|
if (total === 0) return 0;
|
|
1640
|
+
const budget = transferBudget(total, idleMs);
|
|
1598
1641
|
const workers = Math.max(1, Math.min(DOWNLOAD_MAX_WORKERS, Math.ceil(total / DOWNLOAD_MIN_RANGE_BYTES)));
|
|
1599
1642
|
const rangeSize = Math.ceil(total / workers);
|
|
1600
1643
|
let transferred = 0;
|
|
@@ -1618,7 +1661,7 @@ async function downloadStream(conn, remotePath, onData, onProgress, timeoutMs =
|
|
|
1618
1661
|
start,
|
|
1619
1662
|
length: Math.min(rangeSize, total - start)
|
|
1620
1663
|
});
|
|
1621
|
-
await Promise.all(ranges.map((range) => downloadWorkerOnce(conn, remotePath, range.start, range.length, report,
|
|
1664
|
+
await Promise.all(ranges.map((range) => downloadWorkerOnce(conn, remotePath, range.start, range.length, report, budget, authMethod, sendHttp)));
|
|
1622
1665
|
return transferred;
|
|
1623
1666
|
} catch (error) {
|
|
1624
1667
|
if (error instanceof WinrmTimedOut) throw error;
|
|
@@ -1781,7 +1824,7 @@ async function smbReachable(host) {
|
|
|
1781
1824
|
}
|
|
1782
1825
|
/** Error thrown when SMB was requested or probed and the target is not listening. */
|
|
1783
1826
|
function smbUnavailable(host) {
|
|
1784
|
-
return /* @__PURE__ */ new Error("SMB is not reachable on " + host + ":445 (the admin share needs TCP 445 open and the Server service running)");
|
|
1827
|
+
return /* @__PURE__ */ new Error("SMB is not reachable on " + host + ":445 (the admin share needs TCP 445 open and the Server service running); retry with channel \"winrm\" to transfer over WinRM instead");
|
|
1785
1828
|
}
|
|
1786
1829
|
/**
|
|
1787
1830
|
* Copy one local file to a remote Windows host over SMB, verifying the
|
|
@@ -2430,16 +2473,19 @@ var WinRmEngine = class {
|
|
|
2430
2473
|
percent: total > 0 ? Math.round(transferred / total * 1e3) / 10 : 0
|
|
2431
2474
|
});
|
|
2432
2475
|
};
|
|
2476
|
+
let delivered = 0;
|
|
2433
2477
|
try {
|
|
2434
2478
|
const conn = connOf(entry);
|
|
2435
|
-
|
|
2479
|
+
const streamed = await downloadStream(conn, remotePath, put, report);
|
|
2480
|
+
if (streamed === null) {
|
|
2436
2481
|
let offset = 0;
|
|
2437
2482
|
await downloadChunks(conn, remotePath, (b64) => {
|
|
2438
2483
|
const buffer = Buffer.from(b64, "base64");
|
|
2439
2484
|
put(offset, buffer);
|
|
2440
2485
|
offset += buffer.length;
|
|
2441
2486
|
}, report);
|
|
2442
|
-
|
|
2487
|
+
delivered = bytes;
|
|
2488
|
+
} else delivered = streamed;
|
|
2443
2489
|
await queue;
|
|
2444
2490
|
if (sink === null) sink = await open(localPath, "w", 384);
|
|
2445
2491
|
completed = true;
|
|
@@ -2451,12 +2497,12 @@ var WinRmEngine = class {
|
|
|
2451
2497
|
onProgress?.({
|
|
2452
2498
|
phase: "done",
|
|
2453
2499
|
file: remotePath,
|
|
2454
|
-
transferred:
|
|
2455
|
-
total:
|
|
2500
|
+
transferred: delivered,
|
|
2501
|
+
total: delivered,
|
|
2456
2502
|
percent: 100
|
|
2457
2503
|
});
|
|
2458
2504
|
return {
|
|
2459
|
-
bytes,
|
|
2505
|
+
bytes: delivered,
|
|
2460
2506
|
channel: "winrm"
|
|
2461
2507
|
};
|
|
2462
2508
|
});
|
|
@@ -3656,7 +3702,7 @@ function winrmUploadTool(engine) {
|
|
|
3656
3702
|
"smb",
|
|
3657
3703
|
"winrm"
|
|
3658
3704
|
],
|
|
3659
|
-
description: "Transfer channel:
|
|
3705
|
+
description: "Transfer channel: auto (default) uses the SMB admin share when TCP 445 answers and otherwise WinRM; smb fails outright when 445 is closed; winrm always streams over the WinRS stdin channel."
|
|
3660
3706
|
}
|
|
3661
3707
|
},
|
|
3662
3708
|
output: {
|
|
@@ -3700,7 +3746,7 @@ function winrmUploadTool(engine) {
|
|
|
3700
3746
|
function winrmDownloadTool(engine) {
|
|
3701
3747
|
return defineTool({
|
|
3702
3748
|
name: "winrm_download",
|
|
3703
|
-
description: "Download a remote FILE from a configured Windows host to a local path on this machine. The bytes come over the SMB admin share when it is reachable, otherwise they are streamed over WinRM as parallel byte ranges (the 48 KiB base64 chunk path is kept only as a fallback). Directory download is not supported — download files individually. Triggers: download file from Windows server, fetch remote log/artifact.",
|
|
3749
|
+
description: "Download a remote FILE from a configured Windows host to a local path on this machine. The bytes come over the SMB admin share when it is reachable, otherwise they are streamed over WinRM as parallel byte ranges (the 48 KiB base64 chunk path is kept only as a fallback). Large files are fine: the WinRM path runs for as long as bytes keep arriving and only fails once a range stalls. Directory download is not supported — download files individually. Triggers: download file from Windows server, fetch remote log/artifact.",
|
|
3704
3750
|
parameters: {
|
|
3705
3751
|
alias: {
|
|
3706
3752
|
type: "string",
|
|
@@ -3724,7 +3770,7 @@ function winrmDownloadTool(engine) {
|
|
|
3724
3770
|
"smb",
|
|
3725
3771
|
"winrm"
|
|
3726
3772
|
],
|
|
3727
|
-
description: "Transfer channel:
|
|
3773
|
+
description: "Transfer channel: auto (default) uses the SMB admin share when TCP 445 answers and otherwise WinRM; smb fails outright when 445 is closed; winrm always streams over parallel WinRS byte ranges."
|
|
3728
3774
|
}
|
|
3729
3775
|
},
|
|
3730
3776
|
output: {
|
|
@@ -4258,7 +4304,7 @@ const DEFAULT_ENABLED = true;
|
|
|
4258
4304
|
/** Order of the announcement section within the tool-guidance band. */
|
|
4259
4305
|
const SECTION_ORDER = 151;
|
|
4260
4306
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
4261
|
-
const WINRM_GUIDANCE = "本机已安装 dsh-winrm 插件(DSH 远程 Windows 运维,WinRM/PowerShell Remoting):侧边栏「Windows」入口;仿照 dsh-ssh 插件开发。能力:主机配置存 ~/.dsh/dsh-winrm.json(GUI 配置后 agent 方可使用);winrm_list 列出主机、winrm_exec 执行 PowerShell 命令(UTF-8 信封,中文不乱码)、winrm_service 服务管理(start/stop/restart/启动类型)、winrm_process 进程管理(list/kill)、winrm_read/winrm_write/winrm_edit 远程文本文件读写(按行分页、编码感知、精确串替换,作用于远端主机而非本机,本机文件仍用 read/write/edit)、winrm_upload/winrm_download 文件传输(SMB
|
|
4307
|
+
const WINRM_GUIDANCE = "本机已安装 dsh-winrm 插件(DSH 远程 Windows 运维,WinRM/PowerShell Remoting):侧边栏「Windows」入口;仿照 dsh-ssh 插件开发。能力:主机配置存 ~/.dsh/dsh-winrm.json(GUI 配置后 agent 方可使用);winrm_list 列出主机、winrm_exec 执行 PowerShell 命令(UTF-8 信封,中文不乱码)、winrm_service 服务管理(start/stop/restart/启动类型)、winrm_process 进程管理(list/kill)、winrm_read/winrm_write/winrm_edit 远程文本文件读写(按行分页、编码感知、精确串替换,作用于远端主机而非本机,本机文件仍用 read/write/edit)、winrm_upload/winrm_download 文件传输(channel=auto 时若目标 445 可达走 SMB 管理共享,否则走 WinRM 流式传输;channel=smb 在 445 关闭时直接报错、需改用 winrm;大文件可下载,流式通道按字节持续推进、仅在字节断流时才判超时、完成后经 SHA256 校验)、winrm_cluster 集群并发执行;Web 控制台走 WebSocket。认证:通过原生 Node.js WinRM 客户端(winrm-client)的 NTLM/Negotiate 与 Basic 实现,兼容本地账户与域账户;传输支持 http(5985)/https(5986)。前提:目标机需启用 WinRM(scripts/enable-winrm.ps1),本机无需 Python —— WinRM 走纯 Node 依赖。HTTP 仅建议受信内网使用;密码以明文存在用户主目录私有文件(权限 0600);命令输出可能含敏感信息;传输/执行消耗真实远程资源,先确认再操作。用户提到「Windows 服务器 / WinRM / PowerShell 远程 / 服务管理 / 进程管理」时即指本插件,请据此协作。";
|
|
4262
4308
|
/**
|
|
4263
4309
|
* Mount the WinRM engine, routes, tools, and announcement.
|
|
4264
4310
|
* @param ctx - host plugin context carrying webServer/tools/systemPrompt.
|
|
@@ -53,6 +53,32 @@ export declare const TRANSFER_CHUNK: number;
|
|
|
53
53
|
export declare function remoteFileSize(conn: WinRMParams, remotePath: string, timeoutMs?: number): Promise<number | null>;
|
|
54
54
|
export declare function downloadChunks(conn: WinRMParams, remotePath: string, onChunk: (b64: string, bytes: number) => void, onProgress?: (transferred: number, total: number) => void, timeoutMs?: number): Promise<number>;
|
|
55
55
|
export declare function uploadBuffer(conn: WinRMParams, remotePath: string, data: Buffer, onProgress?: (transferred: number, total: number) => void, timeoutMs?: number): Promise<number>;
|
|
56
|
+
/**
|
|
57
|
+
* How long a streaming transfer may deliver nothing before it is treated as
|
|
58
|
+
* stalled.
|
|
59
|
+
*
|
|
60
|
+
* A wall-clock cap on the whole transfer cannot work here: this transport is
|
|
61
|
+
* latency-bound and measured at ~0.73 MiB/s against a real target, so a 700 MiB
|
|
62
|
+
* file legitimately needs about sixteen minutes and any smaller fixed cap fails
|
|
63
|
+
* a transfer that is making steady progress. What a transfer actually needs
|
|
64
|
+
* bounding against is a *stall* — a range that has stopped producing bytes —
|
|
65
|
+
* which is what this idle window measures.
|
|
66
|
+
*/
|
|
67
|
+
export declare const STREAM_IDLE_TIMEOUT_MS = 180000;
|
|
68
|
+
/**
|
|
69
|
+
* Budget for one streaming transfer of `bytes` bytes.
|
|
70
|
+
*
|
|
71
|
+
* The idle window is the effective limit; the hard window exists so a wedged
|
|
72
|
+
* transfer cannot stay alive forever, and it scales with the payload because
|
|
73
|
+
* the payload is what sets the legitimate duration.
|
|
74
|
+
* @param bytes - total bytes the transfer must move.
|
|
75
|
+
* @param idleMs - how long the transfer may go without delivering anything.
|
|
76
|
+
* @returns the idle window and the absolute backstop, both in milliseconds.
|
|
77
|
+
*/
|
|
78
|
+
export declare function transferBudget(bytes: number, idleMs?: number): {
|
|
79
|
+
idleMs: number;
|
|
80
|
+
hardMs: number;
|
|
81
|
+
};
|
|
56
82
|
/**
|
|
57
83
|
* Upload a buffer over a single WinRS stdin stream.
|
|
58
84
|
*
|
|
@@ -68,10 +94,10 @@ export declare function uploadBuffer(conn: WinRMParams, remotePath: string, data
|
|
|
68
94
|
* @param remotePath - destination path on the target.
|
|
69
95
|
* @param data - the file contents to send.
|
|
70
96
|
* @param onProgress - called with cumulative bytes after each frame.
|
|
71
|
-
* @param
|
|
97
|
+
* @param idleMs - how long the transfer may go without moving a slice.
|
|
72
98
|
* @returns bytes written, or null when the streaming path is unavailable.
|
|
73
99
|
*/
|
|
74
|
-
export declare function uploadStream(conn: WinRMParams, remotePath: string, data: Buffer, onProgress?: (transferred: number, total: number) => void,
|
|
100
|
+
export declare function uploadStream(conn: WinRMParams, remotePath: string, data: Buffer, onProgress?: (transferred: number, total: number) => void, idleMs?: number): Promise<number | null>;
|
|
75
101
|
/**
|
|
76
102
|
* Stream a remote file to a consumer, in parallel byte ranges.
|
|
77
103
|
*
|
|
@@ -90,7 +116,7 @@ export declare function uploadStream(conn: WinRMParams, remotePath: string, data
|
|
|
90
116
|
* @param remotePath - file to read.
|
|
91
117
|
* @param onData - receives each decompressed piece with its absolute file offset.
|
|
92
118
|
* @param onProgress - receives cumulative bytes delivered and the total size.
|
|
93
|
-
* @param
|
|
119
|
+
* @param idleMs - how long a worker may go without delivering bytes.
|
|
94
120
|
* @returns the number of bytes delivered, or null when streaming is unavailable.
|
|
95
121
|
*/
|
|
96
|
-
export declare function downloadStream(conn: WinRMParams, remotePath: string, onData: (offset: number, data: Buffer) => void, onProgress?: (transferred: number, total: number) => void,
|
|
122
|
+
export declare function downloadStream(conn: WinRMParams, remotePath: string, onData: (offset: number, data: Buffer) => void, onProgress?: (transferred: number, total: number) => void, idleMs?: number): Promise<number | null>;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -35,7 +35,7 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
35
35
|
enabled: z<boolean, boolean, "volatile-defined">;
|
|
36
36
|
}>>, "plain">;
|
|
37
37
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
38
|
-
export declare const WINRM_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-winrm \u63D2\u4EF6\uFF08DSH \u8FDC\u7A0B Windows \u8FD0\u7EF4\uFF0CWinRM/PowerShell Remoting\uFF09\uFF1A\u4FA7\u8FB9\u680F\u300CWindows\u300D\u5165\u53E3\uFF1B\u4EFF\u7167 dsh-ssh \u63D2\u4EF6\u5F00\u53D1\u3002\u80FD\u529B\uFF1A\u4E3B\u673A\u914D\u7F6E\u5B58 ~/.dsh/dsh-winrm.json\uFF08GUI \u914D\u7F6E\u540E agent \u65B9\u53EF\u4F7F\u7528\uFF09\uFF1Bwinrm_list \u5217\u51FA\u4E3B\u673A\u3001winrm_exec \u6267\u884C PowerShell \u547D\u4EE4\uFF08UTF-8 \u4FE1\u5C01\uFF0C\u4E2D\u6587\u4E0D\u4E71\u7801\uFF09\u3001winrm_service \u670D\u52A1\u7BA1\u7406\uFF08start/stop/restart/\u542F\u52A8\u7C7B\u578B\uFF09\u3001winrm_process \u8FDB\u7A0B\u7BA1\u7406\uFF08list/kill\uFF09\u3001winrm_read/winrm_write/winrm_edit \u8FDC\u7A0B\u6587\u672C\u6587\u4EF6\u8BFB\u5199\uFF08\u6309\u884C\u5206\u9875\u3001\u7F16\u7801\u611F\u77E5\u3001\u7CBE\u786E\u4E32\u66FF\u6362\uFF0C\u4F5C\u7528\u4E8E\u8FDC\u7AEF\u4E3B\u673A\u800C\u975E\u672C\u673A\uFF0C\u672C\u673A\u6587\u4EF6\u4ECD\u7528 read/write/edit\uFF09\u3001winrm_upload/winrm_download \u6587\u4EF6\u4F20\u8F93\
|
|
38
|
+
export declare const WINRM_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-winrm \u63D2\u4EF6\uFF08DSH \u8FDC\u7A0B Windows \u8FD0\u7EF4\uFF0CWinRM/PowerShell Remoting\uFF09\uFF1A\u4FA7\u8FB9\u680F\u300CWindows\u300D\u5165\u53E3\uFF1B\u4EFF\u7167 dsh-ssh \u63D2\u4EF6\u5F00\u53D1\u3002\u80FD\u529B\uFF1A\u4E3B\u673A\u914D\u7F6E\u5B58 ~/.dsh/dsh-winrm.json\uFF08GUI \u914D\u7F6E\u540E agent \u65B9\u53EF\u4F7F\u7528\uFF09\uFF1Bwinrm_list \u5217\u51FA\u4E3B\u673A\u3001winrm_exec \u6267\u884C PowerShell \u547D\u4EE4\uFF08UTF-8 \u4FE1\u5C01\uFF0C\u4E2D\u6587\u4E0D\u4E71\u7801\uFF09\u3001winrm_service \u670D\u52A1\u7BA1\u7406\uFF08start/stop/restart/\u542F\u52A8\u7C7B\u578B\uFF09\u3001winrm_process \u8FDB\u7A0B\u7BA1\u7406\uFF08list/kill\uFF09\u3001winrm_read/winrm_write/winrm_edit \u8FDC\u7A0B\u6587\u672C\u6587\u4EF6\u8BFB\u5199\uFF08\u6309\u884C\u5206\u9875\u3001\u7F16\u7801\u611F\u77E5\u3001\u7CBE\u786E\u4E32\u66FF\u6362\uFF0C\u4F5C\u7528\u4E8E\u8FDC\u7AEF\u4E3B\u673A\u800C\u975E\u672C\u673A\uFF0C\u672C\u673A\u6587\u4EF6\u4ECD\u7528 read/write/edit\uFF09\u3001winrm_upload/winrm_download \u6587\u4EF6\u4F20\u8F93\uFF08channel=auto \u65F6\u82E5\u76EE\u6807 445 \u53EF\u8FBE\u8D70 SMB \u7BA1\u7406\u5171\u4EAB\uFF0C\u5426\u5219\u8D70 WinRM \u6D41\u5F0F\u4F20\u8F93\uFF1Bchannel=smb \u5728 445 \u5173\u95ED\u65F6\u76F4\u63A5\u62A5\u9519\u3001\u9700\u6539\u7528 winrm\uFF1B\u5927\u6587\u4EF6\u53EF\u4E0B\u8F7D\uFF0C\u6D41\u5F0F\u901A\u9053\u6309\u5B57\u8282\u6301\u7EED\u63A8\u8FDB\u3001\u4EC5\u5728\u5B57\u8282\u65AD\u6D41\u65F6\u624D\u5224\u8D85\u65F6\u3001\u5B8C\u6210\u540E\u7ECF SHA256 \u6821\u9A8C\uFF09\u3001winrm_cluster \u96C6\u7FA4\u5E76\u53D1\u6267\u884C\uFF1BWeb \u63A7\u5236\u53F0\u8D70 WebSocket\u3002\u8BA4\u8BC1\uFF1A\u901A\u8FC7\u539F\u751F Node.js WinRM \u5BA2\u6237\u7AEF\uFF08winrm-client\uFF09\u7684 NTLM/Negotiate \u4E0E Basic \u5B9E\u73B0\uFF0C\u517C\u5BB9\u672C\u5730\u8D26\u6237\u4E0E\u57DF\u8D26\u6237\uFF1B\u4F20\u8F93\u652F\u6301 http(5985)/https(5986)\u3002\u524D\u63D0\uFF1A\u76EE\u6807\u673A\u9700\u542F\u7528 WinRM\uFF08scripts/enable-winrm.ps1\uFF09\uFF0C\u672C\u673A\u65E0\u9700 Python \u2014\u2014 WinRM \u8D70\u7EAF Node \u4F9D\u8D56\u3002HTTP \u4EC5\u5EFA\u8BAE\u53D7\u4FE1\u5185\u7F51\u4F7F\u7528\uFF1B\u5BC6\u7801\u4EE5\u660E\u6587\u5B58\u5728\u7528\u6237\u4E3B\u76EE\u5F55\u79C1\u6709\u6587\u4EF6\uFF08\u6743\u9650 0600\uFF09\uFF1B\u547D\u4EE4\u8F93\u51FA\u53EF\u80FD\u542B\u654F\u611F\u4FE1\u606F\uFF1B\u4F20\u8F93/\u6267\u884C\u6D88\u8017\u771F\u5B9E\u8FDC\u7A0B\u8D44\u6E90\uFF0C\u5148\u786E\u8BA4\u518D\u64CD\u4F5C\u3002\u7528\u6237\u63D0\u5230\u300CWindows \u670D\u52A1\u5668 / WinRM / PowerShell \u8FDC\u7A0B / \u670D\u52A1\u7BA1\u7406 / \u8FDB\u7A0B\u7BA1\u7406\u300D\u65F6\u5373\u6307\u672C\u63D2\u4EF6\uFF0C\u8BF7\u636E\u6B64\u534F\u4F5C\u3002";
|
|
39
39
|
/**
|
|
40
40
|
* Mount the WinRM engine, routes, tools, and announcement.
|
|
41
41
|
* @param ctx - host plugin context carrying webServer/tools/systemPrompt.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-winrm",
|
|
3
3
|
"description": "Remote Windows administration for the dsh web GUI: WinRM/PowerShell Remoting host config store (~/.dsh/dsh-winrm.json), PowerShell exec / streaming console / service & process management / remote text read-write-edit / streamed file transfer / cluster execution, plus agent tools (winrm_list, winrm_exec, winrm_service, winrm_process, winrm_read, winrm_write, winrm_edit, winrm_upload, winrm_download, winrm_cluster). Runs entirely on a native Node.js WinRM client (winrm-client) — no Python/pywinrm required. Standalone Cordis plugin — mounted via cordis.patch.yml + a profile node_modules link, no dsh source changes.",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.7.0",
|
|
7
7
|
"engines": {
|
package/src/engine/client.ts
CHANGED
|
@@ -337,6 +337,41 @@ const STREAM_GZIP_MIN_RATIO = 0.85
|
|
|
337
337
|
/** Per-Send HTTP budget; a single slice is small, so this only bounds a stalled connection. */
|
|
338
338
|
const STREAM_SEND_TIMEOUT_MS = 120_000
|
|
339
339
|
|
|
340
|
+
/**
|
|
341
|
+
* How long a streaming transfer may deliver nothing before it is treated as
|
|
342
|
+
* stalled.
|
|
343
|
+
*
|
|
344
|
+
* A wall-clock cap on the whole transfer cannot work here: this transport is
|
|
345
|
+
* latency-bound and measured at ~0.73 MiB/s against a real target, so a 700 MiB
|
|
346
|
+
* file legitimately needs about sixteen minutes and any smaller fixed cap fails
|
|
347
|
+
* a transfer that is making steady progress. What a transfer actually needs
|
|
348
|
+
* bounding against is a *stall* — a range that has stopped producing bytes —
|
|
349
|
+
* which is what this idle window measures.
|
|
350
|
+
*/
|
|
351
|
+
export const STREAM_IDLE_TIMEOUT_MS = 180_000
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Floor used only to size the runaway backstop, never as a throughput target.
|
|
355
|
+
* Deliberately pessimistic (64 KiB/s): it must not fail a slow link, only a
|
|
356
|
+
* transfer that is somehow still "busy" hours after it should have finished.
|
|
357
|
+
*/
|
|
358
|
+
const STREAM_MIN_BYTES_PER_SEC = 65_536
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Budget for one streaming transfer of `bytes` bytes.
|
|
362
|
+
*
|
|
363
|
+
* The idle window is the effective limit; the hard window exists so a wedged
|
|
364
|
+
* transfer cannot stay alive forever, and it scales with the payload because
|
|
365
|
+
* the payload is what sets the legitimate duration.
|
|
366
|
+
* @param bytes - total bytes the transfer must move.
|
|
367
|
+
* @param idleMs - how long the transfer may go without delivering anything.
|
|
368
|
+
* @returns the idle window and the absolute backstop, both in milliseconds.
|
|
369
|
+
*/
|
|
370
|
+
export function transferBudget(bytes: number, idleMs = STREAM_IDLE_TIMEOUT_MS): { idleMs: number; hardMs: number } {
|
|
371
|
+
const optimisticMs = Math.ceil(Math.max(0, bytes) / STREAM_MIN_BYTES_PER_SEC) * 1000
|
|
372
|
+
return { idleMs, hardMs: Math.max(idleMs * 4, optimisticMs + idleMs) }
|
|
373
|
+
}
|
|
374
|
+
|
|
340
375
|
/**
|
|
341
376
|
* One streaming transfer over a single shell: create → run the receiver →
|
|
342
377
|
* push the receiver script, the header frame and every data frame down stdin →
|
|
@@ -349,7 +384,7 @@ const STREAM_SEND_TIMEOUT_MS = 120_000
|
|
|
349
384
|
* @param remotePath - destination path on the target.
|
|
350
385
|
* @param data - the file contents to send.
|
|
351
386
|
* @param onProgress - called with cumulative bytes after each frame.
|
|
352
|
-
* @param
|
|
387
|
+
* @param budget - idle window and absolute backstop for the transfer.
|
|
353
388
|
* @param authMethod - the auth scheme for this attempt.
|
|
354
389
|
* @param sendHttp - the resolved library send function.
|
|
355
390
|
* @returns the number of file bytes the receiver acknowledged writing.
|
|
@@ -360,7 +395,7 @@ async function streamOnce(
|
|
|
360
395
|
remotePath: string,
|
|
361
396
|
data: Buffer,
|
|
362
397
|
onProgress: ((transferred: number, total: number) => void) | undefined,
|
|
363
|
-
|
|
398
|
+
budget: { idleMs: number; hardMs: number },
|
|
364
399
|
authMethod: WinrmAuth,
|
|
365
400
|
sendHttp: ReturnType<typeof loadSendHttp> & object,
|
|
366
401
|
): Promise<number> {
|
|
@@ -375,7 +410,12 @@ async function streamOnce(
|
|
|
375
410
|
useHttps: conn.useHttps ?? false,
|
|
376
411
|
rejectUnauthorized: conn.rejectUnauthorized ?? true,
|
|
377
412
|
}
|
|
378
|
-
const hardDeadline = Date.now() +
|
|
413
|
+
const hardDeadline = Date.now() + budget.hardMs + SETUP_GRACE_MS
|
|
414
|
+
// Progress is what keeps a transfer alive: the send loop and the final
|
|
415
|
+
// receive loop both refresh this, and only a window with no progress at all
|
|
416
|
+
// is treated as a stall.
|
|
417
|
+
let lastProgressAt = Date.now()
|
|
418
|
+
const stalled = (): boolean => Date.now() - lastProgressAt >= budget.idleMs
|
|
379
419
|
const state = { shellId: undefined as string | undefined, timedOut: false, settled: false }
|
|
380
420
|
const total = data.length
|
|
381
421
|
|
|
@@ -405,7 +445,7 @@ async function streamOnce(
|
|
|
405
445
|
let slice = STREAM_CHUNK_BYTES
|
|
406
446
|
let ceiling = STREAM_CHUNK_BYTES
|
|
407
447
|
while (offset < total) {
|
|
408
|
-
if (state.settled ||
|
|
448
|
+
if (state.settled || stalled()) throw new WinrmTimedOut()
|
|
409
449
|
const take = Math.min(slice, total - offset)
|
|
410
450
|
const chunk = data.subarray(offset, offset + take)
|
|
411
451
|
const gz = gzipSync(chunk, { level: 6 })
|
|
@@ -422,6 +462,7 @@ async function streamOnce(
|
|
|
422
462
|
throw error
|
|
423
463
|
}
|
|
424
464
|
offset += take
|
|
465
|
+
lastProgressAt = Date.now()
|
|
425
466
|
onProgress?.(offset, total)
|
|
426
467
|
if (slice < ceiling) slice = Math.min(ceiling, slice * 2)
|
|
427
468
|
}
|
|
@@ -429,15 +470,15 @@ async function streamOnce(
|
|
|
429
470
|
// 4. The receiver stops on its own once the declared byte count lands, so
|
|
430
471
|
// there is nothing to close — just wait for its envelope.
|
|
431
472
|
const receive = { ...base, shellId, commandId }
|
|
432
|
-
const commandDeadline = Date.now() + timeoutMs
|
|
433
473
|
let stdout = ''
|
|
434
474
|
let stderr = ''
|
|
435
475
|
for (;;) {
|
|
436
|
-
if (state.settled ||
|
|
476
|
+
if (state.settled || stalled()) throw new WinrmTimedOut()
|
|
437
477
|
const chunk = await Command.doReceiveOutputNonBlocking(receive)
|
|
438
478
|
stdout += chunk.output
|
|
439
479
|
stderr += chunk.stderr
|
|
440
480
|
if (chunk.isComplete) break
|
|
481
|
+
if (chunk.output !== '' || chunk.stderr !== '') lastProgressAt = Date.now()
|
|
441
482
|
}
|
|
442
483
|
|
|
443
484
|
const parsed = parseEnvelope(stdout)
|
|
@@ -488,7 +529,7 @@ async function streamOnce(
|
|
|
488
529
|
* @param remotePath - destination path on the target.
|
|
489
530
|
* @param data - the file contents to send.
|
|
490
531
|
* @param onProgress - called with cumulative bytes after each frame.
|
|
491
|
-
* @param
|
|
532
|
+
* @param idleMs - how long the transfer may go without moving a slice.
|
|
492
533
|
* @returns bytes written, or null when the streaming path is unavailable.
|
|
493
534
|
*/
|
|
494
535
|
export async function uploadStream(
|
|
@@ -496,15 +537,16 @@ export async function uploadStream(
|
|
|
496
537
|
remotePath: string,
|
|
497
538
|
data: Buffer,
|
|
498
539
|
onProgress?: (transferred: number, total: number) => void,
|
|
499
|
-
|
|
540
|
+
idleMs = STREAM_IDLE_TIMEOUT_MS,
|
|
500
541
|
): Promise<number | null> {
|
|
501
542
|
const sendHttp = loadSendHttp()
|
|
502
543
|
if (sendHttp === null) return null
|
|
544
|
+
const budget = transferBudget(data.length, idleMs)
|
|
503
545
|
const candidates: WinrmAuth[] = conn.useHttps ? ['basic', 'ntlm'] : ['ntlm', 'basic']
|
|
504
546
|
let lastError: unknown
|
|
505
547
|
for (const authMethod of candidates) {
|
|
506
548
|
try {
|
|
507
|
-
return await streamOnce(conn, remotePath, data, onProgress,
|
|
549
|
+
return await streamOnce(conn, remotePath, data, onProgress, budget, authMethod, sendHttp)
|
|
508
550
|
} catch (error) {
|
|
509
551
|
if (error instanceof WinrmTimedOut) throw error
|
|
510
552
|
lastError = error
|
|
@@ -527,7 +569,7 @@ const DOWNLOAD_RECEIVE_TIMEOUT_MS = 120_000
|
|
|
527
569
|
* @param start - first byte offset of this worker's range.
|
|
528
570
|
* @param length - bytes this worker must deliver.
|
|
529
571
|
* @param onData - receives each decompressed piece together with its file offset.
|
|
530
|
-
* @param
|
|
572
|
+
* @param budget - idle window and absolute backstop for this worker's range.
|
|
531
573
|
* @param authMethod - the auth scheme for this attempt.
|
|
532
574
|
* @param sendHttp - the resolved library HTTP function.
|
|
533
575
|
* @returns the number of file bytes this worker delivered.
|
|
@@ -538,7 +580,7 @@ async function downloadWorkerOnce(
|
|
|
538
580
|
start: number,
|
|
539
581
|
length: number,
|
|
540
582
|
onData: (offset: number, data: Buffer) => void,
|
|
541
|
-
|
|
583
|
+
budget: { idleMs: number; hardMs: number },
|
|
542
584
|
authMethod: WinrmAuth,
|
|
543
585
|
sendHttp: ReturnType<typeof loadSendHttp> & object,
|
|
544
586
|
): Promise<number> {
|
|
@@ -553,7 +595,12 @@ async function downloadWorkerOnce(
|
|
|
553
595
|
useHttps: conn.useHttps ?? false,
|
|
554
596
|
rejectUnauthorized: conn.rejectUnauthorized ?? true,
|
|
555
597
|
}
|
|
556
|
-
const hardDeadline = Date.now() +
|
|
598
|
+
const hardDeadline = Date.now() + budget.hardMs + SETUP_GRACE_MS
|
|
599
|
+
// Same rule as the upload path: bytes arriving keep the worker alive, and
|
|
600
|
+
// only a window with no bytes at all counts as a stall. A flat whole-transfer
|
|
601
|
+
// cap would fail a large file on a slow link even while it is moving.
|
|
602
|
+
let lastDataAt = Date.now()
|
|
603
|
+
const stalled = (): boolean => Date.now() - lastDataAt >= budget.idleMs
|
|
557
604
|
const state = { shellId: undefined as string | undefined, timedOut: false, settled: false }
|
|
558
605
|
|
|
559
606
|
const inner = async (): Promise<number> => {
|
|
@@ -589,7 +636,7 @@ async function downloadWorkerOnce(
|
|
|
589
636
|
// A worker that lost the deadline race keeps running in the background
|
|
590
637
|
// unless it checks; without this it would go on fetching and pushing
|
|
591
638
|
// bytes into a file the caller has already given up on.
|
|
592
|
-
if (state.settled ||
|
|
639
|
+
if (state.settled || stalled()) throw new WinrmTimedOut()
|
|
593
640
|
const result = await receiveOutput(target, shellId, commandId, sendHttp, DOWNLOAD_RECEIVE_TIMEOUT_MS)
|
|
594
641
|
let sawData = false
|
|
595
642
|
for (const stream of result.streams) {
|
|
@@ -605,6 +652,7 @@ async function downloadWorkerOnce(
|
|
|
605
652
|
stderr += stream.data.toString('utf8')
|
|
606
653
|
}
|
|
607
654
|
}
|
|
655
|
+
if (sawData) lastDataAt = Date.now()
|
|
608
656
|
if (inflateError !== undefined) throw inflateError
|
|
609
657
|
if (result.exitCode !== undefined) exitCode = result.exitCode
|
|
610
658
|
// A finished command whose final response carried nothing means the
|
|
@@ -663,7 +711,7 @@ async function downloadWorkerOnce(
|
|
|
663
711
|
* @param remotePath - file to read.
|
|
664
712
|
* @param onData - receives each decompressed piece with its absolute file offset.
|
|
665
713
|
* @param onProgress - receives cumulative bytes delivered and the total size.
|
|
666
|
-
* @param
|
|
714
|
+
* @param idleMs - how long a worker may go without delivering bytes.
|
|
667
715
|
* @returns the number of bytes delivered, or null when streaming is unavailable.
|
|
668
716
|
*/
|
|
669
717
|
export async function downloadStream(
|
|
@@ -671,13 +719,15 @@ export async function downloadStream(
|
|
|
671
719
|
remotePath: string,
|
|
672
720
|
onData: (offset: number, data: Buffer) => void,
|
|
673
721
|
onProgress?: (transferred: number, total: number) => void,
|
|
674
|
-
|
|
722
|
+
idleMs = STREAM_IDLE_TIMEOUT_MS,
|
|
675
723
|
): Promise<number | null> {
|
|
676
724
|
const sendHttp = loadSendHttp()
|
|
677
725
|
if (sendHttp === null) return null
|
|
678
726
|
const total = await remoteFileSize(conn, remotePath)
|
|
679
727
|
if (total === null) throw new Error(`remote file not found: ${remotePath}`)
|
|
680
728
|
if (total === 0) return 0
|
|
729
|
+
// The payload size, not a constant, is what sets a legitimate duration here.
|
|
730
|
+
const budget = transferBudget(total, idleMs)
|
|
681
731
|
const workers = Math.max(1, Math.min(DOWNLOAD_MAX_WORKERS, Math.ceil(total / DOWNLOAD_MIN_RANGE_BYTES)))
|
|
682
732
|
const rangeSize = Math.ceil(total / workers)
|
|
683
733
|
let transferred = 0
|
|
@@ -708,7 +758,7 @@ export async function downloadStream(
|
|
|
708
758
|
}
|
|
709
759
|
await Promise.all(
|
|
710
760
|
ranges.map(range =>
|
|
711
|
-
downloadWorkerOnce(conn, remotePath, range.start, range.length, report,
|
|
761
|
+
downloadWorkerOnce(conn, remotePath, range.start, range.length, report, budget, authMethod, sendHttp),
|
|
712
762
|
),
|
|
713
763
|
)
|
|
714
764
|
return transferred
|
package/src/engine.ts
CHANGED
|
@@ -536,6 +536,7 @@ export class WinRmEngine {
|
|
|
536
536
|
const report = (transferred: number, total: number): void => {
|
|
537
537
|
onProgress?.({ phase: 'transferring', file: remotePath, transferred, total, percent: total > 0 ? Math.round((transferred / total) * 1000) / 10 : 0 })
|
|
538
538
|
}
|
|
539
|
+
let delivered = 0
|
|
539
540
|
try {
|
|
540
541
|
const conn = connOf(entry)
|
|
541
542
|
const streamed = await downloadStream(conn, remotePath, put, report)
|
|
@@ -551,6 +552,12 @@ export class WinRmEngine {
|
|
|
551
552
|
},
|
|
552
553
|
report,
|
|
553
554
|
)
|
|
555
|
+
delivered = bytes
|
|
556
|
+
} else {
|
|
557
|
+
// `bytes` counts every positional write, and a retry re-sends the
|
|
558
|
+
// ranges from their start — so it would report more than the file
|
|
559
|
+
// holds. The successful attempt's own total is the file size.
|
|
560
|
+
delivered = streamed
|
|
554
561
|
}
|
|
555
562
|
await queue
|
|
556
563
|
// A zero-byte remote file produces no writes, so the sink is still
|
|
@@ -570,8 +577,8 @@ export class WinRmEngine {
|
|
|
570
577
|
// already truncated that file to nothing.
|
|
571
578
|
if (!completed && sink !== null) await rm(localPath, { force: true }).catch(() => undefined)
|
|
572
579
|
}
|
|
573
|
-
onProgress?.({ phase: 'done', file: remotePath, transferred:
|
|
574
|
-
return { bytes, channel: 'winrm' }
|
|
580
|
+
onProgress?.({ phase: 'done', file: remotePath, transferred: delivered, total: delivered, percent: 100 })
|
|
581
|
+
return { bytes: delivered, channel: 'winrm' }
|
|
575
582
|
})
|
|
576
583
|
}
|
|
577
584
|
|
package/src/index.ts
CHANGED
|
@@ -68,7 +68,7 @@ const DEFAULT_ENABLED = true
|
|
|
68
68
|
const SECTION_ORDER = 151
|
|
69
69
|
|
|
70
70
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
71
|
-
export const WINRM_GUIDANCE = '本机已安装 dsh-winrm 插件(DSH 远程 Windows 运维,WinRM/PowerShell Remoting):侧边栏「Windows」入口;仿照 dsh-ssh 插件开发。能力:主机配置存 ~/.dsh/dsh-winrm.json(GUI 配置后 agent 方可使用);winrm_list 列出主机、winrm_exec 执行 PowerShell 命令(UTF-8 信封,中文不乱码)、winrm_service 服务管理(start/stop/restart/启动类型)、winrm_process 进程管理(list/kill)、winrm_read/winrm_write/winrm_edit 远程文本文件读写(按行分页、编码感知、精确串替换,作用于远端主机而非本机,本机文件仍用 read/write/edit)、winrm_upload/winrm_download 文件传输(SMB
|
|
71
|
+
export const WINRM_GUIDANCE = '本机已安装 dsh-winrm 插件(DSH 远程 Windows 运维,WinRM/PowerShell Remoting):侧边栏「Windows」入口;仿照 dsh-ssh 插件开发。能力:主机配置存 ~/.dsh/dsh-winrm.json(GUI 配置后 agent 方可使用);winrm_list 列出主机、winrm_exec 执行 PowerShell 命令(UTF-8 信封,中文不乱码)、winrm_service 服务管理(start/stop/restart/启动类型)、winrm_process 进程管理(list/kill)、winrm_read/winrm_write/winrm_edit 远程文本文件读写(按行分页、编码感知、精确串替换,作用于远端主机而非本机,本机文件仍用 read/write/edit)、winrm_upload/winrm_download 文件传输(channel=auto 时若目标 445 可达走 SMB 管理共享,否则走 WinRM 流式传输;channel=smb 在 445 关闭时直接报错、需改用 winrm;大文件可下载,流式通道按字节持续推进、仅在字节断流时才判超时、完成后经 SHA256 校验)、winrm_cluster 集群并发执行;Web 控制台走 WebSocket。认证:通过原生 Node.js WinRM 客户端(winrm-client)的 NTLM/Negotiate 与 Basic 实现,兼容本地账户与域账户;传输支持 http(5985)/https(5986)。前提:目标机需启用 WinRM(scripts/enable-winrm.ps1),本机无需 Python —— WinRM 走纯 Node 依赖。HTTP 仅建议受信内网使用;密码以明文存在用户主目录私有文件(权限 0600);命令输出可能含敏感信息;传输/执行消耗真实远程资源,先确认再操作。用户提到「Windows 服务器 / WinRM / PowerShell 远程 / 服务管理 / 进程管理」时即指本插件,请据此协作。'
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
74
|
* Mount the WinRM engine, routes, tools, and announcement.
|
package/src/smb.ts
CHANGED
|
@@ -134,7 +134,10 @@ export async function smbReachable(host: string): Promise<boolean> {
|
|
|
134
134
|
|
|
135
135
|
/** Error thrown when SMB was requested or probed and the target is not listening. */
|
|
136
136
|
export function smbUnavailable(host: string): Error {
|
|
137
|
-
return new Error(
|
|
137
|
+
return new Error(
|
|
138
|
+
'SMB is not reachable on ' + host + ':445 (the admin share needs TCP 445 open and the Server service running); '
|
|
139
|
+
+ 'retry with channel "winrm" to transfer over WinRM instead',
|
|
140
|
+
)
|
|
138
141
|
}
|
|
139
142
|
|
|
140
143
|
/**
|
package/src/tools.ts
CHANGED
|
@@ -273,7 +273,7 @@ export function winrmUploadTool(engine: WinRmEngine) {
|
|
|
273
273
|
alias: { type: 'string', required: true, description: 'Host alias from winrm_list.' },
|
|
274
274
|
localPath: { type: 'string', required: true, description: 'Absolute local file path on this machine.' },
|
|
275
275
|
remotePath: { type: 'string', required: true, description: 'Destination path on the remote host, e.g. C:\\temp\\file.txt (parent dirs are created).' },
|
|
276
|
-
channel: { type: 'string', enum: ['auto', 'smb', 'winrm'], description: 'Transfer channel:
|
|
276
|
+
channel: { type: 'string', enum: ['auto', 'smb', 'winrm'], description: 'Transfer channel: auto (default) uses the SMB admin share when TCP 445 answers and otherwise WinRM; smb fails outright when 445 is closed; winrm always streams over the WinRS stdin channel.' },
|
|
277
277
|
},
|
|
278
278
|
output: {
|
|
279
279
|
schema: {
|
|
@@ -306,13 +306,13 @@ export function winrmUploadTool(engine: WinRmEngine) {
|
|
|
306
306
|
export function winrmDownloadTool(engine: WinRmEngine) {
|
|
307
307
|
return defineTool({
|
|
308
308
|
name: 'winrm_download',
|
|
309
|
-
description: 'Download a remote FILE from a configured Windows host to a local path on this machine. The bytes come over the SMB admin share when it is reachable, otherwise they are streamed over WinRM as parallel byte ranges (the 48 KiB base64 chunk path is kept only as a fallback). Directory download is not supported — download files individually. ' +
|
|
309
|
+
description: 'Download a remote FILE from a configured Windows host to a local path on this machine. The bytes come over the SMB admin share when it is reachable, otherwise they are streamed over WinRM as parallel byte ranges (the 48 KiB base64 chunk path is kept only as a fallback). Large files are fine: the WinRM path runs for as long as bytes keep arriving and only fails once a range stalls. Directory download is not supported — download files individually. ' +
|
|
310
310
|
'Triggers: download file from Windows server, fetch remote log/artifact.',
|
|
311
311
|
parameters: {
|
|
312
312
|
alias: { type: 'string', required: true, description: 'Host alias from winrm_list.' },
|
|
313
313
|
remotePath: { type: 'string', required: true, description: 'Remote file path, e.g. C:\\temp\\file.txt.' },
|
|
314
314
|
localPath: { type: 'string', required: true, description: 'Absolute destination path on this machine.' },
|
|
315
|
-
channel: { type: 'string', enum: ['auto', 'smb', 'winrm'], description: 'Transfer channel:
|
|
315
|
+
channel: { type: 'string', enum: ['auto', 'smb', 'winrm'], description: 'Transfer channel: auto (default) uses the SMB admin share when TCP 445 answers and otherwise WinRM; smb fails outright when 445 is closed; winrm always streams over parallel WinRS byte ranges.' },
|
|
316
316
|
},
|
|
317
317
|
output: {
|
|
318
318
|
schema: {
|