dsh-ssh-manage 0.1.1 → 0.2.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 +6 -6
- package/README.md +6 -6
- package/client.js +228 -67
- package/index.js +50 -5
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -34,7 +34,7 @@ No more jumping between a terminal and DSH, and no need to remember where each h
|
|
|
34
34
|
|
|
35
35
|
### Host management
|
|
36
36
|
|
|
37
|
-
- Group tree on the left, host table on the right: search, filter by group, favorite, tags, notes, connection state and latency.
|
|
37
|
+
- Group tree on the left, host table on the right: search, filter by group, favorite, tags, notes, connection state and latency. Drag the divider to resize the group sidebar (double-click resets; the width is remembered).
|
|
38
38
|
- Create, edit, rename and delete hosts; renaming migrates group, tags, favorites, notes, inspection data and the saved password.
|
|
39
39
|
- Configurable table columns (info / notes / compact rows), responsive on narrow screens.
|
|
40
40
|
- Host details drawer: address, user, port, group, tags, notes, distro, CPU, memory, disk, latency, plus "Test connection" and "Inspect".
|
|
@@ -44,7 +44,7 @@ No more jumping between a terminal and DSH, and no need to remember where each h
|
|
|
44
44
|
### Interactive terminal
|
|
45
45
|
|
|
46
46
|
- Click "Connect" to open a real PTY shell, not a one-command-at-a-time console.
|
|
47
|
-
- Multiple hosts stay as tabs
|
|
47
|
+
- Multiple hosts stay as tabs: switching to another DSH session and coming back re-attaches to the existing shell and replays roughly the last 256 KB of output; only closing the tab ends the session.
|
|
48
48
|
- xterm.js 6.0.0 rendering with 256 colors, cursor, scrollback, selection copy, paste and resize.
|
|
49
49
|
- Terminal theme follows the DSH light/dark theme; the session bar has quick actions for address, password status, test, files, details, clear and close.
|
|
50
50
|
- Output is streamed over a same-origin HTTP/SSE connection; input, resize and close use same-origin HTTP POST. A tested WebSocket channel is kept as a fallback.
|
|
@@ -96,16 +96,16 @@ Creating a host requires alias, hostname and password. The password is not writt
|
|
|
96
96
|
|
|
97
97
|
### From npm (recommended)
|
|
98
98
|
|
|
99
|
-
npm package: [`dsh-ssh-manage`](https://www.npmjs.com/package/dsh-ssh-manage) (current version `0.
|
|
99
|
+
npm package: [`dsh-ssh-manage`](https://www.npmjs.com/package/dsh-ssh-manage) (current version `0.2.1`).
|
|
100
100
|
|
|
101
101
|
```text
|
|
102
|
-
plugin_manager action=install_bundle target=dsh-ssh-manage@0.
|
|
102
|
+
plugin_manager action=install_bundle target=dsh-ssh-manage@0.2.1
|
|
103
103
|
```
|
|
104
104
|
|
|
105
105
|
Or with the DSH CLI:
|
|
106
106
|
|
|
107
107
|
```bash
|
|
108
|
-
dsh plugin add dsh-ssh-manage@0.
|
|
108
|
+
dsh plugin add dsh-ssh-manage@0.2.1
|
|
109
109
|
```
|
|
110
110
|
|
|
111
111
|
After installation, refresh the Web page; the sidebar shows an "SSH Hosts" entry with id `dsh-ssh-manage`.
|
|
@@ -216,7 +216,7 @@ The terminal uses vendored xterm.js 6.0.0 (MIT, see `assets/LICENSE.xterm`) and
|
|
|
216
216
|
## Known limitations
|
|
217
217
|
|
|
218
218
|
- **Host key verification** is not implemented; ssh2 accepts the server-provided key. Use trusted networks only.
|
|
219
|
-
- **Session lifetime**:
|
|
219
|
+
- **Session lifetime**: sessions are kept on the Host; switching to another DSH session, temporarily leaving the panel, or refreshing the page re-attaches to the existing shell and replays roughly the last 256 KB of output. Only closing the tab or exiting the Host process ends them; orphaned sessions are reaped after 6 hours of inactivity (max 16 sessions).
|
|
220
220
|
- **Password auth only**: IdentityFile / SSH agent / ProxyJump are not used for transport; those config fields are preserved but ignored. ProxyJump hosts fail with a clear error.
|
|
221
221
|
- **Config editing scope**: the editor only changes `Host`, `HostName`, `User`, `Port` and plugin metadata; maintain other SSH directives in `~/.ssh/config`.
|
|
222
222
|
- **Batch commands** run one command; they are not an interactive terminal. Per-host output is capped at 1 MB and marked `truncated` when exceeded.
|
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
|
|
35
35
|
### 主机管理
|
|
36
36
|
|
|
37
|
-
- 左侧分组树 +
|
|
37
|
+
- 左侧分组树 + 右侧主机表格:搜索、按分组筛选、收藏、标签、备注、连接状态与延迟一目了然;分组栏可拖拽调整宽度(双击恢复默认),宽度会记住。
|
|
38
38
|
- 新建、编辑、重命名、删除主机;重命名自动迁移分组、标签、收藏、备注、巡检信息和已保存密码。
|
|
39
39
|
- 表格列可配置(信息列 / 备注列 / 紧凑行高),窄屏自适应。
|
|
40
40
|
- 主机详情抽屉:地址、用户、端口、分组、标签、备注、发行版、CPU、内存、磁盘、延迟,以及「测试连接」「巡检」。
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
### 交互式终端
|
|
45
45
|
|
|
46
46
|
- 点「连接」打开真正的 PTY shell,不是一条命令一条命令地执行。
|
|
47
|
-
-
|
|
47
|
+
- 多台主机以标签页常驻:切换到其它 DSH 会话再回来,终端会重新附着到原来的 shell,并回放最近约 256 KB 输出;关闭标签才结束会话。
|
|
48
48
|
- xterm.js 6.0.0 渲染,支持 256 色、光标、滚动回看、选中复制、粘贴、窗口 resize。
|
|
49
49
|
- 终端主题跟随 DSH 明暗主题;会话顶部有地址、密码状态、测试连接、文件、详情、清屏、关闭等快捷操作。
|
|
50
50
|
- 输出走同源 HTTP/SSE 长连接,输入 / 尺寸 / 关闭走同源 HTTP POST;插件同时保留经过测试的 WebSocket 备用通道。
|
|
@@ -98,16 +98,16 @@
|
|
|
98
98
|
|
|
99
99
|
### 从 npm 安装(推荐)
|
|
100
100
|
|
|
101
|
-
npm 包:[`dsh-ssh-manage`](https://www.npmjs.com/package/dsh-ssh-manage)(当前版本 `0.
|
|
101
|
+
npm 包:[`dsh-ssh-manage`](https://www.npmjs.com/package/dsh-ssh-manage)(当前版本 `0.2.1`)。
|
|
102
102
|
|
|
103
103
|
```text
|
|
104
|
-
plugin_manager action=install_bundle target=dsh-ssh-manage@0.
|
|
104
|
+
plugin_manager action=install_bundle target=dsh-ssh-manage@0.2.1
|
|
105
105
|
```
|
|
106
106
|
|
|
107
107
|
或使用 DSH CLI:
|
|
108
108
|
|
|
109
109
|
```bash
|
|
110
|
-
dsh plugin add dsh-ssh-manage@0.
|
|
110
|
+
dsh plugin add dsh-ssh-manage@0.2.1
|
|
111
111
|
```
|
|
112
112
|
|
|
113
113
|
安装后刷新 Web 页面,左侧栏会出现「SSH 主机」入口,入口 id 为 `dsh-ssh-manage`。
|
|
@@ -218,7 +218,7 @@ npm test
|
|
|
218
218
|
## 已知限制
|
|
219
219
|
|
|
220
220
|
- **主机密钥校验**:未接入 `known_hosts`,ssh2 默认接受服务器主机密钥;仅在可信网络使用。
|
|
221
|
-
-
|
|
221
|
+
- **会话生命周期**:终端会话由 Host 保留,切换 DSH 会话/暂时离开插件面板/刷新页面后会重新附着并回放最近约 256 KB 输出;只有关闭标签或 Host 进程退出才真正结束。Host 侧空闲超过 6 小时会清理孤儿会话(最大 16 个)。
|
|
222
222
|
- **仅密码认证**:不支持 IdentityFile / SSH agent / ProxyJump 传输;配置中的这些字段会原样保留,但不会用于连接。ProxyJump 主机会明确报错。
|
|
223
223
|
- **配置编辑范围**:编辑器只修改 `Host`、`HostName`、`User`、`Port` 与插件元数据;其它 SSH 指令请在 `~/.ssh/config` 中维护。
|
|
224
224
|
- **批量命令**:一次执行一条命令,不等同于交互式终端;单台输出上限 1 MB,超出会标记 `truncated`。
|
package/client.js
CHANGED
|
@@ -26,11 +26,12 @@ window.__ModuleLoader__.load({
|
|
|
26
26
|
'app.configFile': ['配置文件', 'Config file'],
|
|
27
27
|
'app.copyConfig': ['复制配置路径', 'Copy config path'],
|
|
28
28
|
'toolbar.collapse': ['收起', 'Collapse'],
|
|
29
|
+
'toolbar.resize': ['拖动调整分组栏宽度,双击恢复默认', 'Drag to resize the group sidebar; double-click to reset'],
|
|
29
30
|
'toolbar.expand': ['展开', 'Expand'],
|
|
30
31
|
'toolbar.refresh': ['刷新', 'Refresh'],
|
|
31
32
|
'toolbar.groups': ['分组', 'Groups'],
|
|
32
33
|
'toolbar.new': ['新建主机', 'New host'],
|
|
33
|
-
'toolbar.newShort': ['
|
|
34
|
+
'toolbar.newShort': ['SSH', 'SSH'],
|
|
34
35
|
'toolbar.filter': ['只看收藏', 'Favorites only'],
|
|
35
36
|
'toolbar.info': ['信息', 'Info'],
|
|
36
37
|
'toolbar.settings': ['显示设置', 'View options'],
|
|
@@ -279,6 +280,12 @@ window.__ModuleLoader__.load({
|
|
|
279
280
|
return `${user}${target}${port}`
|
|
280
281
|
}
|
|
281
282
|
|
|
283
|
+
// Table / session chrome shows only the hostname (the name column already
|
|
284
|
+
// carries the alias); the full user@host:port stays in titles and copy.
|
|
285
|
+
function hostTarget(host) {
|
|
286
|
+
return host.hostname || host.host
|
|
287
|
+
}
|
|
288
|
+
|
|
282
289
|
function hostGroupKey(host) {
|
|
283
290
|
return host.group || ''
|
|
284
291
|
}
|
|
@@ -1047,7 +1054,7 @@ window.__ModuleLoader__.load({
|
|
|
1047
1054
|
}
|
|
1048
1055
|
let result = 0
|
|
1049
1056
|
if (sort.key === 'system') result = String(a.system ?? '').localeCompare(String(b.system ?? ''))
|
|
1050
|
-
else if (sort.key === 'address') result =
|
|
1057
|
+
else if (sort.key === 'address') result = hostTarget(a).localeCompare(hostTarget(b))
|
|
1051
1058
|
else if (sort.key === 'notes') result = String(a.note ?? '').localeCompare(String(b.note ?? ''))
|
|
1052
1059
|
else result = hostName(a).localeCompare(hostName(b), undefined, { numeric: true, sensitivity: 'base' })
|
|
1053
1060
|
if (result === 0) result = a.host.localeCompare(b.host)
|
|
@@ -1067,10 +1074,10 @@ window.__ModuleLoader__.load({
|
|
|
1067
1074
|
}
|
|
1068
1075
|
|
|
1069
1076
|
function tableTracks(prefs) {
|
|
1070
|
-
const tracks = ['32px', '
|
|
1071
|
-
if (prefs.info) tracks.push('minmax(
|
|
1072
|
-
if (prefs.notes) tracks.push('minmax(
|
|
1073
|
-
tracks.push('
|
|
1077
|
+
const tracks = ['32px', '96px', '80px', 'minmax(90px,1fr)', 'minmax(150px,1.25fr)']
|
|
1078
|
+
if (prefs.info) tracks.push('minmax(210px,1.4fr)')
|
|
1079
|
+
if (prefs.notes) tracks.push('minmax(120px,0.8fr)')
|
|
1080
|
+
tracks.push('150px', '28px')
|
|
1074
1081
|
return tracks.join(' ')
|
|
1075
1082
|
}
|
|
1076
1083
|
|
|
@@ -1093,6 +1100,48 @@ window.__ModuleLoader__.load({
|
|
|
1093
1100
|
// App
|
|
1094
1101
|
// ---------------------------------------------------------------------
|
|
1095
1102
|
|
|
1103
|
+
// Survives the plugin panel unmounting when the user switches to another
|
|
1104
|
+
// DSH session (and a client-module reload). Host-side PTYs stay alive;
|
|
1105
|
+
// mounting again attaches to them and replays recent output.
|
|
1106
|
+
const PANEL_STATE_KEY = 'dsh-ssh-manage:panel-state'
|
|
1107
|
+
const TREE_WIDTH_KEY = 'dsh-ssh-manage:tree-width'
|
|
1108
|
+
|
|
1109
|
+
function readPanelState() {
|
|
1110
|
+
try {
|
|
1111
|
+
const parsed = JSON.parse(String(window.sessionStorage.getItem(PANEL_STATE_KEY) ?? 'null'))
|
|
1112
|
+
if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.sessions)) {
|
|
1113
|
+
return { sessions: [], activeSessionId: null, tab: 'hosts' }
|
|
1114
|
+
}
|
|
1115
|
+
const sessions = parsed.sessions
|
|
1116
|
+
.filter((session) => session && typeof session.id === 'string' && typeof session.hostAlias === 'string')
|
|
1117
|
+
.map((session) => ({ id: session.id, hostAlias: session.hostAlias }))
|
|
1118
|
+
const tab = ['hosts', 'sessions', 'batch', 'settings'].includes(parsed.tab) ? parsed.tab : 'hosts'
|
|
1119
|
+
const activeSessionId = typeof parsed.activeSessionId === 'string' ? parsed.activeSessionId : null
|
|
1120
|
+
return { sessions, activeSessionId, tab }
|
|
1121
|
+
} catch {
|
|
1122
|
+
return { sessions: [], activeSessionId: null, tab: 'hosts' }
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
const persistentPanelState = readPanelState()
|
|
1127
|
+
const TREE_MIN_WIDTH = 220
|
|
1128
|
+
const TREE_MAX_WIDTH = 560
|
|
1129
|
+
|
|
1130
|
+
function clampTreeWidth(value) {
|
|
1131
|
+
const numeric = Number(value)
|
|
1132
|
+
if (!Number.isFinite(numeric)) return 300
|
|
1133
|
+
return Math.min(TREE_MAX_WIDTH, Math.max(TREE_MIN_WIDTH, Math.round(numeric)))
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function readTreeWidth() {
|
|
1137
|
+
try {
|
|
1138
|
+
const stored = window.localStorage.getItem(TREE_WIDTH_KEY)
|
|
1139
|
+
return stored === null ? 300 : clampTreeWidth(stored)
|
|
1140
|
+
} catch {
|
|
1141
|
+
return 300
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1096
1145
|
function App({ ctx }) {
|
|
1097
1146
|
const t = useT(ctx)
|
|
1098
1147
|
const localeId = useLocaleId(ctx)
|
|
@@ -1100,7 +1149,11 @@ window.__ModuleLoader__.load({
|
|
|
1100
1149
|
const [loading, setLoading] = React.useState(true)
|
|
1101
1150
|
const [refreshing, setRefreshing] = React.useState(false)
|
|
1102
1151
|
const [error, setError] = React.useState('')
|
|
1103
|
-
const [tab, setTab] = React.useState(
|
|
1152
|
+
const [tab, setTab] = React.useState(() => {
|
|
1153
|
+
const saved = persistentPanelState.tab
|
|
1154
|
+
if (saved === 'sessions' && persistentPanelState.sessions.length === 0) return 'hosts'
|
|
1155
|
+
return ['hosts', 'sessions', 'batch', 'settings'].includes(saved) ? saved : 'hosts'
|
|
1156
|
+
})
|
|
1104
1157
|
const [filter, setFilter] = React.useState('')
|
|
1105
1158
|
const [favoriteOnly, setFavoriteOnly] = React.useState(false)
|
|
1106
1159
|
const [groupFilter, setGroupFilter] = React.useState(null)
|
|
@@ -1119,12 +1172,14 @@ window.__ModuleLoader__.load({
|
|
|
1119
1172
|
const [pending, setPending] = React.useState({})
|
|
1120
1173
|
const [sort, setSort] = React.useState({ key: 'name', dir: 'asc' })
|
|
1121
1174
|
const [columns, setColumns] = React.useState({ info: true, notes: true, compact: false })
|
|
1122
|
-
const [sessions, setSessions] = React.useState(
|
|
1123
|
-
|
|
1175
|
+
const [sessions, setSessions] = React.useState(() =>
|
|
1176
|
+
persistentPanelState.sessions.map((session) => ({ id: session.id, hostAlias: session.hostAlias })),
|
|
1177
|
+
)
|
|
1178
|
+
const [activeSessionId, setActiveSessionId] = React.useState(() => persistentPanelState.activeSessionId)
|
|
1179
|
+
const [treeWidth, setTreeWidth] = React.useState(readTreeWidth)
|
|
1124
1180
|
const [clearSignals, setClearSignals] = React.useState({})
|
|
1125
1181
|
const toastTimer = React.useRef(null)
|
|
1126
1182
|
const floatingRef = React.useRef(null)
|
|
1127
|
-
const sessionCounter = React.useRef(0)
|
|
1128
1183
|
|
|
1129
1184
|
const notify = React.useCallback((message, tone = 'info') => {
|
|
1130
1185
|
if (toastTimer.current) clearTimeout(toastTimer.current)
|
|
@@ -1139,6 +1194,25 @@ window.__ModuleLoader__.load({
|
|
|
1139
1194
|
[],
|
|
1140
1195
|
)
|
|
1141
1196
|
|
|
1197
|
+
React.useEffect(() => {
|
|
1198
|
+
persistentPanelState.sessions = sessions.map((session) => ({ id: session.id, hostAlias: session.hostAlias }))
|
|
1199
|
+
persistentPanelState.activeSessionId = activeSessionId
|
|
1200
|
+
persistentPanelState.tab = tab
|
|
1201
|
+
try {
|
|
1202
|
+
window.sessionStorage.setItem(PANEL_STATE_KEY, JSON.stringify(persistentPanelState))
|
|
1203
|
+
} catch {}
|
|
1204
|
+
}, [sessions, activeSessionId, tab])
|
|
1205
|
+
|
|
1206
|
+
React.useEffect(() => {
|
|
1207
|
+
try {
|
|
1208
|
+
window.localStorage.setItem(TREE_WIDTH_KEY, String(treeWidth))
|
|
1209
|
+
} catch {}
|
|
1210
|
+
}, [treeWidth])
|
|
1211
|
+
|
|
1212
|
+
React.useEffect(() => {
|
|
1213
|
+
for (const session of sessions) ptyActiveIds.add(session.id)
|
|
1214
|
+
}, [sessions])
|
|
1215
|
+
|
|
1142
1216
|
const applySnapshot = React.useCallback((next) => {
|
|
1143
1217
|
if (!next) return
|
|
1144
1218
|
const aliases = new Set((next.hosts ?? []).map((host) => host.host))
|
|
@@ -1217,8 +1291,11 @@ window.__ModuleLoader__.load({
|
|
|
1217
1291
|
if (!snapshot) return
|
|
1218
1292
|
const aliases = new Set(hosts.map((host) => host.host))
|
|
1219
1293
|
setSessions((prev) => {
|
|
1294
|
+
const removed = prev.filter((session) => !aliases.has(session.hostAlias))
|
|
1220
1295
|
const next = prev.filter((session) => aliases.has(session.hostAlias))
|
|
1221
|
-
|
|
1296
|
+
if (removed.length === 0) return prev
|
|
1297
|
+
for (const session of removed) closePtySession(session.id)
|
|
1298
|
+
return next
|
|
1222
1299
|
})
|
|
1223
1300
|
}, [hosts, snapshot])
|
|
1224
1301
|
|
|
@@ -1298,8 +1375,8 @@ window.__ModuleLoader__.load({
|
|
|
1298
1375
|
if (existing) {
|
|
1299
1376
|
setActiveSessionId(existing.id)
|
|
1300
1377
|
} else {
|
|
1301
|
-
|
|
1302
|
-
|
|
1378
|
+
const id = newPtySessionId()
|
|
1379
|
+
ptyActiveIds.add(id)
|
|
1303
1380
|
setSessions((prev) => [...prev, { id, hostAlias: host.host }])
|
|
1304
1381
|
setActiveSessionId(id)
|
|
1305
1382
|
}
|
|
@@ -1309,6 +1386,7 @@ window.__ModuleLoader__.load({
|
|
|
1309
1386
|
function closeSession(id) {
|
|
1310
1387
|
const index = sessions.findIndex((session) => session.id === id)
|
|
1311
1388
|
if (index < 0) return
|
|
1389
|
+
closePtySession(id)
|
|
1312
1390
|
const next = sessions.filter((session) => session.id !== id)
|
|
1313
1391
|
setSessions(next)
|
|
1314
1392
|
setClearSignals((prev) => {
|
|
@@ -1602,6 +1680,8 @@ window.__ModuleLoader__.load({
|
|
|
1602
1680
|
setFloating,
|
|
1603
1681
|
setEditor,
|
|
1604
1682
|
setDrawer,
|
|
1683
|
+
treeWidth,
|
|
1684
|
+
setTreeWidth,
|
|
1605
1685
|
sessions,
|
|
1606
1686
|
activeSessionId,
|
|
1607
1687
|
setActiveSessionId,
|
|
@@ -2320,7 +2400,7 @@ window.__ModuleLoader__.load({
|
|
|
2320
2400
|
h(
|
|
2321
2401
|
'div',
|
|
2322
2402
|
{ className: 'ssh-cell ssh-colAddress' },
|
|
2323
|
-
h('span', { className: 'ssh-address', title: hostAddress(host) },
|
|
2403
|
+
h('span', { className: 'ssh-address', title: hostAddress(host) }, hostTarget(host)),
|
|
2324
2404
|
h(
|
|
2325
2405
|
'span',
|
|
2326
2406
|
{ className: 'ssh-inlineActions' },
|
|
@@ -2429,9 +2509,11 @@ window.__ModuleLoader__.load({
|
|
|
2429
2509
|
observer.observe(node)
|
|
2430
2510
|
return () => observer.disconnect()
|
|
2431
2511
|
}, [])
|
|
2512
|
+
const hasInfo = visible.some((host) => Boolean(host.system || host.cpuCores || host.memoryGB || host.diskGB))
|
|
2513
|
+
const hasNotes = visible.some((host) => Boolean(host.note))
|
|
2432
2514
|
const effective = {
|
|
2433
|
-
info: columns.info && tableWidth >=
|
|
2434
|
-
notes: columns.notes && tableWidth >=
|
|
2515
|
+
info: columns.info && hasInfo && tableWidth >= 900,
|
|
2516
|
+
notes: columns.notes && hasNotes && tableWidth >= 1100,
|
|
2435
2517
|
}
|
|
2436
2518
|
const style = { gridTemplateColumns: tableTracks(effective) }
|
|
2437
2519
|
const visibleAliases = visible.map((host) => host.host)
|
|
@@ -2498,10 +2580,55 @@ window.__ModuleLoader__.load({
|
|
|
2498
2580
|
}
|
|
2499
2581
|
|
|
2500
2582
|
function HostsTab({ model }) {
|
|
2583
|
+
const { t } = model
|
|
2584
|
+
const beginResize = (event) => {
|
|
2585
|
+
if (!model.sidebarOpen || event.button !== 0) return
|
|
2586
|
+
event.preventDefault()
|
|
2587
|
+
event.stopPropagation()
|
|
2588
|
+
const startX = event.clientX
|
|
2589
|
+
const startWidth = model.treeWidth
|
|
2590
|
+
document.body.classList.add('ssh-col-resizing')
|
|
2591
|
+
const onMove = (moveEvent) => {
|
|
2592
|
+
model.setTreeWidth(clampTreeWidth(startWidth + moveEvent.clientX - startX))
|
|
2593
|
+
}
|
|
2594
|
+
const onUp = () => {
|
|
2595
|
+
document.body.classList.remove('ssh-col-resizing')
|
|
2596
|
+
window.removeEventListener('pointermove', onMove)
|
|
2597
|
+
window.removeEventListener('pointerup', onUp)
|
|
2598
|
+
window.removeEventListener('pointercancel', onUp)
|
|
2599
|
+
}
|
|
2600
|
+
window.addEventListener('pointermove', onMove)
|
|
2601
|
+
window.addEventListener('pointerup', onUp)
|
|
2602
|
+
window.addEventListener('pointercancel', onUp)
|
|
2603
|
+
}
|
|
2604
|
+
const nudgeWidth = (event) => {
|
|
2605
|
+
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return
|
|
2606
|
+
event.preventDefault()
|
|
2607
|
+
model.setTreeWidth(clampTreeWidth(model.treeWidth + (event.key === 'ArrowLeft' ? -20 : 20)))
|
|
2608
|
+
}
|
|
2501
2609
|
return h(
|
|
2502
2610
|
'div',
|
|
2503
|
-
{
|
|
2611
|
+
{
|
|
2612
|
+
className: cx('ssh-body', !model.sidebarOpen && 'collapsed'),
|
|
2613
|
+
style: { '--ssh-tree-width': `${model.treeWidth}px` },
|
|
2614
|
+
},
|
|
2504
2615
|
h(Sidebar, { model }),
|
|
2616
|
+
model.sidebarOpen
|
|
2617
|
+
? h('div', {
|
|
2618
|
+
className: 'ssh-treeResizer',
|
|
2619
|
+
role: 'separator',
|
|
2620
|
+
tabIndex: 0,
|
|
2621
|
+
'aria-orientation': 'vertical',
|
|
2622
|
+
'aria-label': t('toolbar.resize'),
|
|
2623
|
+
'aria-valuemin': TREE_MIN_WIDTH,
|
|
2624
|
+
'aria-valuemax': TREE_MAX_WIDTH,
|
|
2625
|
+
'aria-valuenow': model.treeWidth,
|
|
2626
|
+
title: t('toolbar.resize'),
|
|
2627
|
+
onPointerDown: beginResize,
|
|
2628
|
+
onKeyDown: nudgeWidth,
|
|
2629
|
+
onDoubleClick: () => model.setTreeWidth(300),
|
|
2630
|
+
})
|
|
2631
|
+
: null,
|
|
2505
2632
|
model.sidebarOpen ? h('div', { className: 'ssh-sideScrim', onClick: () => model.setSidebarOpen(false) }) : null,
|
|
2506
2633
|
h(
|
|
2507
2634
|
'main',
|
|
@@ -2622,7 +2749,7 @@ window.__ModuleLoader__.load({
|
|
|
2622
2749
|
'div',
|
|
2623
2750
|
{ className: 'ssh-sessionActions' },
|
|
2624
2751
|
activeHost
|
|
2625
|
-
? h('span', { className: 'ssh-sessionAddress', title: hostAddress(activeHost) },
|
|
2752
|
+
? h('span', { className: 'ssh-sessionAddress', title: hostAddress(activeHost) }, hostTarget(activeHost))
|
|
2626
2753
|
: null,
|
|
2627
2754
|
activeCredential ? h(Badge, { tone: activeCredential.tone }, activeCredential.label) : null,
|
|
2628
2755
|
h(
|
|
@@ -2734,7 +2861,7 @@ window.__ModuleLoader__.load({
|
|
|
2734
2861
|
'div',
|
|
2735
2862
|
{ className: 'ssh-drawerTitles' },
|
|
2736
2863
|
h('div', { className: 'ssh-drawerTitle', title: hostName(host) }, hostName(host)),
|
|
2737
|
-
h('div', { className: 'ssh-drawerSub' },
|
|
2864
|
+
h('div', { className: 'ssh-drawerSub' }, hostTarget(host)),
|
|
2738
2865
|
),
|
|
2739
2866
|
h(
|
|
2740
2867
|
Badge,
|
|
@@ -2913,10 +3040,26 @@ window.__ModuleLoader__.load({
|
|
|
2913
3040
|
}
|
|
2914
3041
|
|
|
2915
3042
|
const PTY_EVENTS_ROUTE = '/dsh-ssh-manage/pty/events'
|
|
3043
|
+
const PTY_ATTACH_ROUTE = '/dsh-ssh-manage/pty/attach'
|
|
2916
3044
|
const ptyStream = { source: null, ready: false, promise: null, listeners: new Map() }
|
|
2917
3045
|
const ptyActiveIds = new Set()
|
|
3046
|
+
const ptyClosedIds = new Set()
|
|
2918
3047
|
const ptyStreamLostHandlers = new Set()
|
|
2919
3048
|
|
|
3049
|
+
function closePtySession(id) {
|
|
3050
|
+
if (!id) return
|
|
3051
|
+
ptyClosedIds.add(id)
|
|
3052
|
+
ptyActiveIds.delete(id)
|
|
3053
|
+
try {
|
|
3054
|
+
void fetch('/dsh-ssh-manage/pty/close', {
|
|
3055
|
+
method: 'POST',
|
|
3056
|
+
headers: { 'content-type': 'application/json' },
|
|
3057
|
+
body: JSON.stringify({ id }),
|
|
3058
|
+
keepalive: true,
|
|
3059
|
+
})
|
|
3060
|
+
} catch {}
|
|
3061
|
+
}
|
|
3062
|
+
|
|
2920
3063
|
function onPtyStreamLost(handler) {
|
|
2921
3064
|
ptyStreamLostHandlers.add(handler)
|
|
2922
3065
|
return () => ptyStreamLostHandlers.delete(handler)
|
|
@@ -2934,17 +3077,6 @@ window.__ModuleLoader__.load({
|
|
|
2934
3077
|
}
|
|
2935
3078
|
}
|
|
2936
3079
|
|
|
2937
|
-
if (typeof window !== 'undefined') {
|
|
2938
|
-
window.addEventListener('pagehide', () => {
|
|
2939
|
-
for (const id of ptyActiveIds) {
|
|
2940
|
-
try {
|
|
2941
|
-
navigator.sendBeacon('/dsh-ssh-manage/pty/close', new Blob([JSON.stringify({ id })], { type: 'application/json' }))
|
|
2942
|
-
} catch {}
|
|
2943
|
-
}
|
|
2944
|
-
ptyActiveIds.clear()
|
|
2945
|
-
})
|
|
2946
|
-
}
|
|
2947
|
-
|
|
2948
3080
|
function subscribePty(id, listener) {
|
|
2949
3081
|
ptyStream.listeners.set(id, listener)
|
|
2950
3082
|
return () => {
|
|
@@ -3043,7 +3175,6 @@ window.__ModuleLoader__.load({
|
|
|
3043
3175
|
const containerRef = React.useRef(null)
|
|
3044
3176
|
const termRef = React.useRef(null)
|
|
3045
3177
|
const fitRef = React.useRef(null)
|
|
3046
|
-
const sessionIdRef = React.useRef(null)
|
|
3047
3178
|
const activeRef = React.useRef(active === true)
|
|
3048
3179
|
const [status, setStatus] = React.useState('loading')
|
|
3049
3180
|
const [message, setMessage] = React.useState('')
|
|
@@ -3099,24 +3230,7 @@ window.__ModuleLoader__.load({
|
|
|
3099
3230
|
unsubscribe()
|
|
3100
3231
|
unsubscribe = null
|
|
3101
3232
|
}
|
|
3102
|
-
const id = sessionKey
|
|
3103
3233
|
sessionKey = null
|
|
3104
|
-
sessionIdRef.current = null
|
|
3105
|
-
if (id) {
|
|
3106
|
-
ptyActiveIds.delete(id)
|
|
3107
|
-
postClose(id)
|
|
3108
|
-
}
|
|
3109
|
-
}
|
|
3110
|
-
|
|
3111
|
-
const postClose = (id) => {
|
|
3112
|
-
try {
|
|
3113
|
-
void fetch('/dsh-ssh-manage/pty/close', {
|
|
3114
|
-
method: 'POST',
|
|
3115
|
-
headers: { 'content-type': 'application/json' },
|
|
3116
|
-
body: JSON.stringify({ id }),
|
|
3117
|
-
keepalive: true,
|
|
3118
|
-
})
|
|
3119
|
-
} catch {}
|
|
3120
3234
|
}
|
|
3121
3235
|
|
|
3122
3236
|
const failSession = (error) => {
|
|
@@ -3207,42 +3321,83 @@ window.__ModuleLoader__.load({
|
|
|
3207
3321
|
return
|
|
3208
3322
|
}
|
|
3209
3323
|
if (removed) return
|
|
3210
|
-
sessionKey =
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3324
|
+
sessionKey = sessionId
|
|
3325
|
+
const id = sessionKey
|
|
3326
|
+
let synced = false
|
|
3327
|
+
let lastSeq = 0
|
|
3328
|
+
const pendingEvents = []
|
|
3329
|
+
|
|
3330
|
+
const applyOutput = (event) => {
|
|
3331
|
+
const seq = Number(event.seq)
|
|
3332
|
+
if (Number.isFinite(seq)) {
|
|
3333
|
+
if (seq <= lastSeq) return
|
|
3334
|
+
lastSeq = seq
|
|
3335
|
+
}
|
|
3336
|
+
try { term.write(decodeBase64(event.data)) } catch {}
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3339
|
+
const handleEvent = (event) => {
|
|
3215
3340
|
if (event.kind === 'output') {
|
|
3216
|
-
|
|
3341
|
+
if (!synced) pendingEvents.push(event)
|
|
3342
|
+
else applyOutput(event)
|
|
3217
3343
|
} else if (event.kind === 'exit') {
|
|
3344
|
+
if (!synced) {
|
|
3345
|
+
pendingEvents.push(event)
|
|
3346
|
+
return
|
|
3347
|
+
}
|
|
3218
3348
|
finished = true
|
|
3219
3349
|
setStatus('closed')
|
|
3220
3350
|
setMessage(t('terminal.exited'))
|
|
3221
3351
|
teardown()
|
|
3222
3352
|
} else if (event.kind === 'error') {
|
|
3353
|
+
if (!synced) {
|
|
3354
|
+
pendingEvents.push(event)
|
|
3355
|
+
return
|
|
3356
|
+
}
|
|
3223
3357
|
finished = true
|
|
3224
3358
|
setStatus('error')
|
|
3225
3359
|
setMessage(String(event.message ?? t('terminal.failed')))
|
|
3226
3360
|
teardown()
|
|
3227
3361
|
}
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
unsubscribe = subscribePty(id, (event) => {
|
|
3365
|
+
if (removed) return
|
|
3366
|
+
handleEvent(event)
|
|
3228
3367
|
})
|
|
3229
3368
|
let dims = { cols: 80, rows: 24 }
|
|
3230
3369
|
try {
|
|
3231
3370
|
const proposed = fit?.proposeDimensions?.()
|
|
3232
3371
|
if (proposed && proposed.cols > 0 && proposed.rows > 0) dims = proposed
|
|
3233
3372
|
} catch {}
|
|
3234
|
-
const openedId = sessionKey
|
|
3235
3373
|
try {
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3374
|
+
let value
|
|
3375
|
+
try {
|
|
3376
|
+
value = await ptyPost(PTY_ATTACH_ROUTE, { id, cols: dims.cols, rows: dims.rows }, 15_000)
|
|
3377
|
+
} catch {
|
|
3378
|
+
value = await ptyPost('/dsh-ssh-manage/pty/open', {
|
|
3379
|
+
id,
|
|
3380
|
+
host: hostRef.current.host,
|
|
3381
|
+
cols: dims.cols,
|
|
3382
|
+
rows: dims.rows,
|
|
3383
|
+
}, 40_000)
|
|
3384
|
+
}
|
|
3385
|
+
if (ptyClosedIds.has(id)) {
|
|
3386
|
+
closePtySession(id)
|
|
3244
3387
|
return
|
|
3245
3388
|
}
|
|
3389
|
+
if (removed) return
|
|
3390
|
+
if (value && value.output) {
|
|
3391
|
+
try { term.write(decodeBase64(value.output)) } catch {}
|
|
3392
|
+
}
|
|
3393
|
+
const seq = Number(value?.seq)
|
|
3394
|
+
if (Number.isFinite(seq)) lastSeq = seq
|
|
3395
|
+
synced = true
|
|
3396
|
+
for (const event of pendingEvents.splice(0)) {
|
|
3397
|
+
if (finished) break
|
|
3398
|
+
handleEvent(event)
|
|
3399
|
+
}
|
|
3400
|
+
if (finished) return
|
|
3246
3401
|
opened = true
|
|
3247
3402
|
setStatus('ready')
|
|
3248
3403
|
setMessage('')
|
|
@@ -4057,7 +4212,7 @@ window.__ModuleLoader__.load({
|
|
|
4057
4212
|
label: t('table.select'),
|
|
4058
4213
|
}),
|
|
4059
4214
|
h('span', { className: 'ssh-targetName' }, hostName(host)),
|
|
4060
|
-
h('span', { className: 'ssh-targetAddr' },
|
|
4215
|
+
h('span', { className: 'ssh-targetAddr' }, hostTarget(host)),
|
|
4061
4216
|
h('span', { className: 'ssh-targetCred' }, h(Badge, { tone: credential.tone }, credential.label)),
|
|
4062
4217
|
)
|
|
4063
4218
|
}),
|
|
@@ -4283,8 +4438,13 @@ window.__ModuleLoader__.load({
|
|
|
4283
4438
|
.ssh-errorBar{flex:0 0 auto;display:flex;align-items:center;gap:8px;padding:8px 16px;border-bottom:.5px solid var(--ssh-border);color:var(--ssh-danger);font-size:11.5px;background:color-mix(in srgb,var(--ssh-danger) 7%,var(--ssh-bg))}
|
|
4284
4439
|
.ssh-errorText{flex:1;min-width:0}
|
|
4285
4440
|
.ssh-loadingPage{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;color:var(--ssh-text-3);font-size:12.5px;padding:40px}
|
|
4286
|
-
.ssh-body{flex:1;min-height:0;display:grid;grid-template-columns:300px minmax(0,1fr);position:relative;overflow:hidden}
|
|
4441
|
+
.ssh-body{flex:1;min-height:0;display:grid;grid-template-columns:var(--ssh-tree-width,300px) minmax(0,1fr);position:relative;overflow:hidden}
|
|
4287
4442
|
.ssh-body.collapsed{grid-template-columns:0 minmax(0,1fr)}
|
|
4443
|
+
.ssh-treeResizer{position:absolute;top:0;bottom:0;left:var(--ssh-tree-width,300px);width:10px;transform:translateX(-5px);z-index:26;cursor:col-resize;touch-action:none;outline:none}
|
|
4444
|
+
.ssh-treeResizer::after{content:"";position:absolute;top:0;bottom:0;left:4px;width:1px;background:transparent;transition:background .12s ease}
|
|
4445
|
+
.ssh-treeResizer:hover::after,.ssh-treeResizer:focus-visible::after{background:var(--ssh-brand);box-shadow:0 0 0 1px color-mix(in srgb,var(--ssh-brand) 35%,transparent)}
|
|
4446
|
+
.ssh-body.collapsed .ssh-treeResizer{display:none}
|
|
4447
|
+
body.ssh-col-resizing,body.ssh-col-resizing *{cursor:col-resize!important;user-select:none!important}
|
|
4288
4448
|
.ssh-sidebar{display:flex;flex-direction:column;min-height:0;border-right:.5px solid var(--ssh-border);background:var(--ssh-bg)}
|
|
4289
4449
|
.ssh-body.collapsed .ssh-sidebar{display:none}
|
|
4290
4450
|
.ssh-sideScrim{display:none}
|
|
@@ -4369,8 +4529,8 @@ window.__ModuleLoader__.load({
|
|
|
4369
4529
|
.ssh-row:hover .ssh-inlineActions{opacity:1}
|
|
4370
4530
|
.ssh-miniBtn{appearance:none;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border:0;border-radius:4px;background:transparent;color:var(--ssh-text-3);cursor:pointer}
|
|
4371
4531
|
.ssh-miniBtn:hover{background:var(--ssh-hover);color:var(--ssh-text)}
|
|
4372
|
-
.ssh-colInfo{gap:4px;flex-wrap:
|
|
4373
|
-
.ssh-chip{display:inline-flex;align-items:center;height:21px;max-width:100%;padding:0 7px;border-radius:6px;border:.5px solid var(--ssh-border);background:var(--ssh-layer-3);font-size:10.5px;color:var(--ssh-text-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
4532
|
+
.ssh-colInfo{gap:4px;row-gap:3px;flex-wrap:wrap;align-content:center;overflow:visible}
|
|
4533
|
+
.ssh-chip{display:inline-flex;flex:0 0 auto;align-items:center;height:21px;max-width:100%;padding:0 7px;border-radius:6px;border:.5px solid var(--ssh-border);background:var(--ssh-layer-3);font-size:10.5px;color:var(--ssh-text-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
4374
4534
|
.ssh-mutedText{font-size:11.5px;color:var(--ssh-dim)}
|
|
4375
4535
|
.ssh-colNotes{min-width:0}
|
|
4376
4536
|
.ssh-noteText{font-size:11.5px;color:var(--ssh-text-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
@@ -4620,6 +4780,7 @@ window.__ModuleLoader__.load({
|
|
|
4620
4780
|
.ssh-tree::-webkit-scrollbar-thumb:hover,.ssh-tableScroll::-webkit-scrollbar-thumb:hover,.ssh-details::-webkit-scrollbar-thumb:hover,.ssh-terminalLog::-webkit-scrollbar-thumb:hover,.ssh-fileList::-webkit-scrollbar-thumb:hover,.ssh-page::-webkit-scrollbar-thumb:hover,.ssh-modalBody::-webkit-scrollbar-thumb:hover,.ssh-floating::-webkit-scrollbar-thumb:hover,.ssh-targetList::-webkit-scrollbar-thumb:hover{background:var(--dsw-alias-scrollbar-hover-l1,var(--ssh-border-strong));border:2px solid transparent;background-clip:padding-box}
|
|
4621
4781
|
@media (max-width: 900px){
|
|
4622
4782
|
.ssh-body{grid-template-columns:minmax(0,1fr)}
|
|
4783
|
+
.ssh-treeResizer{display:none}
|
|
4623
4784
|
.ssh-sidebar{position:absolute;top:0;bottom:0;left:0;width:290px;z-index:25;display:none;box-shadow:var(--ssh-shadow)}
|
|
4624
4785
|
.ssh-sidebar.open{display:flex}
|
|
4625
4786
|
.ssh-body:not(.collapsed) .ssh-sideScrim{display:block;position:absolute;inset:0;z-index:20;background:color-mix(in srgb,var(--ssh-bg) 45%,transparent)}
|
package/index.js
CHANGED
|
@@ -15,9 +15,11 @@ const PTY_EVENTS_ROUTE = '/dsh-ssh-manage/pty/events'
|
|
|
15
15
|
const PTY_OPEN_ROUTE = '/dsh-ssh-manage/pty/open'
|
|
16
16
|
const PTY_INPUT_ROUTE = '/dsh-ssh-manage/pty/input'
|
|
17
17
|
const PTY_RESIZE_ROUTE = '/dsh-ssh-manage/pty/resize'
|
|
18
|
+
const PTY_ATTACH_ROUTE = '/dsh-ssh-manage/pty/attach'
|
|
18
19
|
const PTY_CLOSE_ROUTE = '/dsh-ssh-manage/pty/close'
|
|
19
20
|
const MAX_PTY_SESSIONS = 16
|
|
20
|
-
const
|
|
21
|
+
const PTY_BUFFER_BYTES = 256 * 1024
|
|
22
|
+
const PTY_IDLE_MS = 6 * 60 * 60 * 1000
|
|
21
23
|
const MAX_PTY_BUFFER_BYTES = 4 * 1024 * 1024
|
|
22
24
|
const PACKAGE_DIRECTORY = path.dirname(fileURLToPath(import.meta.url))
|
|
23
25
|
const ASSETS = [
|
|
@@ -330,11 +332,18 @@ async function openTextSession(paths, payload) {
|
|
|
330
332
|
try { session.close() } catch {}
|
|
331
333
|
throw new Error('Terminal session already exists')
|
|
332
334
|
}
|
|
333
|
-
const entry = { id, session, cols, rows, lastActivity: Date.now() }
|
|
335
|
+
const entry = { id, session, cols, rows, lastActivity: Date.now(), seq: 0, buffer: [], bufferBytes: 0 }
|
|
334
336
|
ptyTextSessions.set(id, entry)
|
|
335
337
|
session.stream.on('data', (chunk) => {
|
|
336
338
|
entry.lastActivity = Date.now()
|
|
337
|
-
|
|
339
|
+
const copy = Buffer.from(chunk)
|
|
340
|
+
entry.seq += 1
|
|
341
|
+
entry.buffer.push(copy)
|
|
342
|
+
entry.bufferBytes += copy.length
|
|
343
|
+
while (entry.bufferBytes > PTY_BUFFER_BYTES && entry.buffer.length > 0) {
|
|
344
|
+
entry.bufferBytes -= entry.buffer.shift().length
|
|
345
|
+
}
|
|
346
|
+
broadcastPty({ kind: 'output', id, seq: entry.seq, data: copy.toString('base64') })
|
|
338
347
|
})
|
|
339
348
|
session.stream.on('close', () => {
|
|
340
349
|
closeTextSession(id)
|
|
@@ -347,7 +356,7 @@ async function openTextSession(paths, payload) {
|
|
|
347
356
|
closeTextSession(id, false)
|
|
348
357
|
broadcastPty({ kind: 'error', id, message: errorMessage(error) })
|
|
349
358
|
})
|
|
350
|
-
return { id, cols, rows }
|
|
359
|
+
return { id, seq: 0, cols, rows, output: '' }
|
|
351
360
|
} finally {
|
|
352
361
|
ptyPendingIds.delete(id)
|
|
353
362
|
ptyOpening -= 1
|
|
@@ -424,6 +433,42 @@ function registerPtyHttpRoutes(ctx, paths) {
|
|
|
424
433
|
'pty open route',
|
|
425
434
|
)
|
|
426
435
|
|
|
436
|
+
register(
|
|
437
|
+
{
|
|
438
|
+
kind: 'exact',
|
|
439
|
+
path: PTY_ATTACH_ROUTE,
|
|
440
|
+
handler: async (req, res) => {
|
|
441
|
+
if (rejectUnauthenticated(ctx, req, res)) return
|
|
442
|
+
if (req.method !== 'POST') {
|
|
443
|
+
res.statusCode = 405
|
|
444
|
+
res.end()
|
|
445
|
+
return
|
|
446
|
+
}
|
|
447
|
+
const body = (await readJsonBody(req)) ?? {}
|
|
448
|
+
const id = typeof body.id === 'string' ? body.id : ''
|
|
449
|
+
const entry = ptyTextSessions.get(id)
|
|
450
|
+
if (!entry) {
|
|
451
|
+
sendJson(res, 200, { ok: false, error: { code: 'pty-session-missing', message: 'Terminal session was not found' } })
|
|
452
|
+
return
|
|
453
|
+
}
|
|
454
|
+
try {
|
|
455
|
+
entry.lastActivity = Date.now()
|
|
456
|
+
entry.cols = boundedInteger(body.cols, 20, 500, entry.cols)
|
|
457
|
+
entry.rows = boundedInteger(body.rows, 5, 300, entry.rows)
|
|
458
|
+
entry.session.stream.setWindow(entry.rows, entry.cols, 0, 0)
|
|
459
|
+
const output = Buffer.concat(entry.buffer, entry.bufferBytes).toString('base64')
|
|
460
|
+
sendJson(res, 200, {
|
|
461
|
+
ok: true,
|
|
462
|
+
value: { id, seq: entry.seq, cols: entry.cols, rows: entry.rows, output },
|
|
463
|
+
})
|
|
464
|
+
} catch (error) {
|
|
465
|
+
sendJson(res, 200, { ok: false, error: { code: 'pty-attach-failed', message: errorMessage(error) } })
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
'pty attach route',
|
|
470
|
+
)
|
|
471
|
+
|
|
427
472
|
for (const [path, action, label] of [
|
|
428
473
|
[PTY_INPUT_ROUTE, 'input', 'pty input route'],
|
|
429
474
|
[PTY_RESIZE_ROUTE, 'resize', 'pty resize route'],
|
|
@@ -669,7 +714,7 @@ function registerAssetRoutes(ctx) {
|
|
|
669
714
|
}
|
|
670
715
|
}
|
|
671
716
|
|
|
672
|
-
export const __test = { resolveSshPaths, listSshHosts, saveSshHost, deleteSshHost, runAction, managerTool, MAX_BODY_BYTES, PTY_ROUTE, PTY_EVENTS_ROUTE, PTY_OPEN_ROUTE, PTY_INPUT_ROUTE, PTY_RESIZE_ROUTE, PTY_CLOSE_ROUTE, MAX_PTY_SESSIONS, ASSETS, handlePtyUpgrade, ptyTextSessions }
|
|
717
|
+
export const __test = { resolveSshPaths, listSshHosts, saveSshHost, deleteSshHost, runAction, managerTool, MAX_BODY_BYTES, PTY_ROUTE, PTY_EVENTS_ROUTE, PTY_OPEN_ROUTE, PTY_ATTACH_ROUTE, PTY_INPUT_ROUTE, PTY_RESIZE_ROUTE, PTY_CLOSE_ROUTE, MAX_PTY_SESSIONS, PTY_BUFFER_BYTES, ASSETS, handlePtyUpgrade, ptyTextSessions }
|
|
673
718
|
|
|
674
719
|
export const inject = ['webServer']
|
|
675
720
|
|