feat(lms): add Docker setup guidance and improve remote workspace handling
This commit is contained in:
+52
-4
@@ -1,4 +1,5 @@
|
||||
import { app, BrowserWindow, dialog, Menu, globalShortcut, ipcMain, screen } from 'electron'
|
||||
import { app, BrowserWindow, dialog, Menu, globalShortcut, ipcMain, screen, shell } from 'electron'
|
||||
import { checkDockerStatus } from './dockerStatus'
|
||||
import type { WebContents } from 'electron'
|
||||
import { spawn } from 'child_process'
|
||||
import * as pty from 'node-pty'
|
||||
@@ -2140,8 +2141,8 @@ async function readRemoteDockerStatsOutput(target: RemoteWorkspaceTarget): Promi
|
||||
try {
|
||||
const output = await runUnqueuedSshOutput(
|
||||
normalized,
|
||||
'timeout -k 1s 2s docker stats --no-stream --format "STATS\\t{{.Name}}\\t{{.CPUPerc}}\\t{{.MemPerc}}" 2>/dev/null || true',
|
||||
4_000,
|
||||
'timeout -k 1s 8s docker stats --no-stream --format "STATS\\t{{.Name}}\\t{{.CPUPerc}}\\t{{.MemPerc}}" 2>/dev/null || true',
|
||||
15_000,
|
||||
)
|
||||
remoteDockerStatsRetryAfter.delete(key)
|
||||
return output
|
||||
@@ -2218,6 +2219,47 @@ async function readRemoteSavedMapArtifacts(
|
||||
}))
|
||||
}
|
||||
|
||||
async function uploadRemoteRtabmapDatabase(
|
||||
targetInput: RemoteWorkspaceTarget,
|
||||
uploadUrl: string,
|
||||
databasePath?: string | null,
|
||||
): Promise<{ ok: boolean }> {
|
||||
const target = normalizeRemoteTarget(targetInput)
|
||||
const url = new URL(uploadUrl)
|
||||
if (url.protocol !== 'https:' || !url.hostname.endsWith('.r2.cloudflarestorage.com')
|
||||
|| url.username || url.password || url.port || !url.searchParams.has('X-Amz-Signature')) {
|
||||
throw new Error('R2 업로드 URL이 올바르지 않습니다.')
|
||||
}
|
||||
const fileName = databasePath ? savedMapArtifactFileName('database', databasePath) : 'rtabmap-active.db'
|
||||
const sourcePath = `${target.workspaceRoot}/maps/${fileName}`
|
||||
const snapshotScript = [
|
||||
'import sqlite3, sys',
|
||||
'source = sqlite3.connect("file:" + sys.argv[1] + "?mode=ro", uri=True)',
|
||||
'destination = sqlite3.connect(sys.argv[2])',
|
||||
'source.backup(destination)',
|
||||
'destination.close()',
|
||||
'source.close()',
|
||||
].join('; ')
|
||||
const script = [
|
||||
'set -e',
|
||||
`SOURCE=${quoteShellSingle(sourcePath)}`,
|
||||
'test -f "$SOURCE"',
|
||||
'command -v curl >/dev/null',
|
||||
'SNAPSHOT=$(mktemp --suffix=.db)',
|
||||
'trap \'rm -f "$SNAPSHOT"\' EXIT',
|
||||
`timeout -k 2s 120s python3 -c ${quoteShellSingle(snapshotScript)} "$SOURCE" "$SNAPSHOT"`,
|
||||
`curl --fail --silent --show-error --connect-timeout 15 --max-time 900 --header 'Content-Type: application/octet-stream' --upload-file "$SNAPSHOT" --output /dev/null ${quoteShellSingle(uploadUrl)}`,
|
||||
].join('\n')
|
||||
try {
|
||||
// Only commands cross SSH; the database bytes go from the platform to R2.
|
||||
await runUnqueuedSshOutput(target, script, 1_100_000)
|
||||
} catch {
|
||||
// The command contains a signed URL; do not expose it in renderer errors.
|
||||
throw new Error('플랫폼의 3D DB R2 업로드에 실패했습니다. DB 파일, curl 설치 및 인터넷 연결을 확인해 주세요.')
|
||||
}
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
async function copyRemoteActiveRtabmapDatabaseToLocal(
|
||||
targetInput: RemoteWorkspaceTarget,
|
||||
localWorkspaceRoot: string | null | undefined,
|
||||
@@ -5612,7 +5654,8 @@ async function collectRemoteModuleFiles(targetInput: RemoteWorkspaceTarget, modu
|
||||
' esac',
|
||||
'done'
|
||||
].join('\n')
|
||||
const output = await runSshOutput(target, script, 30000)
|
||||
// Source transfers must not hold up module start/stop on the control queue.
|
||||
const output = await runUnqueuedSshOutput(target, script, 30000)
|
||||
const files: Record<string, string> = {}
|
||||
const folders: string[] = []
|
||||
|
||||
@@ -6198,6 +6241,8 @@ function registerLocalWorkspaceHandlers(): void {
|
||||
})
|
||||
ipcMain.handle('local-workspace:modules:list', (_event, workspaceRoot?: string | null, courseModules?: CourseModuleInput[] | null) => collectAvailableModules(workspaceRoot, courseModules))
|
||||
ipcMain.handle('local-workspace:modules:docker-images', () => listLocalDockerImages())
|
||||
ipcMain.handle('local-workspace:docker:status', () => checkDockerStatus())
|
||||
ipcMain.handle('local-workspace:docker:guide', () => shell.openExternal('https://docs.docker.com/engine/install/'))
|
||||
ipcMain.handle('local-workspace:ros-graph', () => collectLocalRosGraph())
|
||||
ipcMain.handle('local-workspace:modules:install', (_event, moduleName: string, workspaceRoot?: string | null, folderName?: string | null, templateRepositoryUrl?: string | null, studentRepositoryUrl?: string | null, options?: ModuleInstallOptions | null) => installModuleFromCache(moduleName, workspaceRoot, folderName, templateRepositoryUrl, studentRepositoryUrl, options))
|
||||
ipcMain.handle('local-workspace:modules:create-custom', (_event, workspaceRoot?: string | null, input?: CustomModuleScaffoldInput | null) => {
|
||||
@@ -6258,6 +6303,9 @@ function registerLocalWorkspaceHandlers(): void {
|
||||
ipcMain.handle('remote-workspace:maps:copy-active-rtabmap-database', (_event, target: RemoteWorkspaceTarget, localWorkspaceRoot: string | null | undefined, destinationPath: string) => (
|
||||
copyRemoteActiveRtabmapDatabaseToLocal(target, localWorkspaceRoot, destinationPath)
|
||||
))
|
||||
ipcMain.handle('remote-workspace:maps:upload-rtabmap-database', (_event, target: RemoteWorkspaceTarget, uploadUrl: string, databasePath?: string | null) => (
|
||||
uploadRemoteRtabmapDatabase(target, uploadUrl, databasePath)
|
||||
))
|
||||
ipcMain.handle('remote-workspace:maps:sync', (_event, target: RemoteWorkspaceTarget, artifacts: SavedMapArtifactDownload[]) => (
|
||||
syncRemoteSavedMapArtifacts(target, artifacts)
|
||||
))
|
||||
|
||||
Reference in New Issue
Block a user