feat(M4): file permissions/ownership + monitor charts

- SftpEntry carries uid/gid/mode (drwxr-xr-x style) from readdir attrs
- chmod/chown via SSH exec (JD sftp subsystem silently ignores SETSTAT)
- FilePanel: permission + owner columns, chmod/chown dialog (rwx grid +
  octal input + uid/gid)
- MonitorPanel: canvas sparkline charts — CPU history (60 samples, 3min)
  and network rx/tx rates, dpr-aware, no deps
- tests: sftp-chmod real-server verification (chmod 700/755 round trip)
This commit is contained in:
954801926@qq.com committed 2026-09-06 17:07:11 +08:00
1 parent 06fa4ca023
commit 2849fc30dc
60 files changed
+1975 -17

No files matched your search

+16
View File
@@ -7,6 +7,7 @@ import { broadcast } from './broadcast'
import { connectSsh, type SshSessionHandle } from './ssh'
import type { HostKeyCheckResult } from './knownHosts'
import { configureSysinfo, registerSysinfoClient, stopPolling } from './sysinfo'
import { registerSftpClientProvider, closeSftp } from './sftp'
// Re-export so the session-layer loopback bundle (tests/*-e2e.mjs) can drive the
// M3 polling engine without importing src/main/sysinfo.ts separately.
@@ -19,6 +20,12 @@ export { startPolling, stopPolling } from './sysinfo'
*/
type Session = { kind: 'local'; pty: IPty } | { kind: 'ssh'; ssh: SshSessionHandle }
/** The live ssh2 client behind an ssh session, or undefined. */
export function getSshClient(id: string): SshSessionHandle['client'] | undefined {
const session = sessions.get(id)
return session?.kind === 'ssh' ? session.ssh.client : undefined
}
const sessions = new Map<string, Session>()
/**
@@ -77,6 +84,11 @@ export function configureSessionRuntime(deps: SessionRuntimeDeps): void {
if (session?.kind === 'ssh') return session.ssh.client
return undefined
})
registerSftpClientProvider((id) => {
const session = sessions.get(id)
if (session?.kind === 'ssh') return session.ssh.client
return undefined
})
}
function defaultShell(): string {
@@ -170,6 +182,7 @@ export async function openSession(opts: SessionOpenOptions): Promise<{ id: strin
handle.stream.on('close', () => {
try {
stopPolling(handle.id)
closeSftp(handle.id)
sessions.delete(handle.id)
deps.broadcast(Ipc.PTY_EXIT, { id: handle.id, exitCode: 0 })
} catch {
@@ -205,6 +218,8 @@ export function resizePty(id: string, cols: number, rows: number): void {
export function killPty(id: string): void {
stopPolling(id)
closeSftp(id)
closeSftp(id)
replayBuffers.delete(id)
const session = sessions.get(id)
if (!session) return
@@ -232,6 +247,7 @@ export function killPty(id: string): void {
export function killAllPtys(): void {
for (const id of sessions.keys()) {
stopPolling(id)
closeSftp(id)
const session = sessions.get(id)
if (!session) continue
if (session.kind === 'ssh') {