This commit is contained in:
2026-08-20 16:52:45 +09:00
parent fdffd4617a
commit 342fcaeb34
16 changed files with 306 additions and 84 deletions
+55 -14
View File
@@ -1,6 +1,6 @@
import { app, BrowserWindow, Menu, globalShortcut, ipcMain, screen } from 'electron'
import type { WebContents } from 'electron'
import { spawn, type ChildProcessWithoutNullStreams } from 'child_process'
import { spawn } from 'child_process'
import * as pty from 'node-pty'
import type { IPty } from 'node-pty'
import { appendFileSync, createWriteStream, existsSync, mkdirSync } from 'fs'
@@ -245,7 +245,7 @@ type RosNetworkConfig = {
peer?: RemoteWorkspaceTarget | null
}
type VisualizationStack = 'autoware' | 'nav2' | 'slam'
type VisualizationStack = 'autoware' | 'nav2' | 'slam' | 'slam3d'
type VisualizationBridgeStatus = {
ok: boolean
@@ -1212,8 +1212,9 @@ async function resolveRemoteCustomModuleRuntimeImage(
folderName: string,
fallbackImage: string,
): Promise<string> {
const output = await runSshOutput(target, [
`ROOT=${quoteShellSingle(target.workspaceRoot)}`,
const normalizedTarget = normalizeRemoteTarget(target)
const output = await runSshOutput(normalizedTarget, [
`ROOT=${quoteShellSingle(normalizedTarget.workspaceRoot)}`,
...expandRemotePathVar('ROOT'),
`CONFIG="$ROOT/${folderName}/.autonomy-studio-module.json"`,
'if [ -f "$CONFIG" ]; then python3 - "$CONFIG" <<\'PY\'',
@@ -1524,6 +1525,31 @@ async function runCommand(command: string, args: string[], cwd?: string, env?: N
await runCommandOutput(command, args, cwd, env, timeoutMs)
}
async function listLocalDockerImages(): Promise<{ images: string[]; message?: string }> {
try {
const output = await runCommandOutput('docker', ['image', 'ls', '--format', '{{json .}}'], undefined, undefined, 10_000)
const images = Array.from(new Set(output.split('\n').flatMap((line) => {
if (!line.trim()) return []
try {
const image = JSON.parse(line) as { Repository?: unknown; Tag?: unknown }
const repository = typeof image.Repository === 'string' ? image.Repository.trim() : ''
const tag = typeof image.Tag === 'string' ? image.Tag.trim() : ''
if (!repository || !tag || repository === '<none>' || tag === '<none>') return []
return [`${repository}:${tag}`]
} catch {
return []
}
}))).sort((left, right) => left.localeCompare(right))
return { images }
} catch (error) {
return {
images: [],
message: error instanceof Error ? error.message : '로컬 Docker 이미지를 불러오지 못했습니다.'
}
}
}
const rosGraphMarker = '__AUTONOMY_STUDIO_ROS_GRAPH__'
const rosGraphProbeScript = [
'import json, rclpy',
@@ -1799,7 +1825,7 @@ function remoteSshDestination(target: RemoteWorkspaceTarget): string {
async function queueRemoteSsh<T>(target: ReturnType<typeof normalizeRemoteTarget>, task: () => Promise<T>): Promise<T> {
const key = `${target.user}@${target.host}:${target.port}`
const previous = remoteSshQueues.get(key) ?? Promise.resolve()
let release: (() => void) | null = null
let release: () => void = () => undefined
const current = new Promise<void>((resolvePromise) => {
release = resolvePromise
})
@@ -1810,7 +1836,7 @@ async function queueRemoteSsh<T>(target: ReturnType<typeof normalizeRemoteTarget
try {
return await task()
} finally {
release?.()
release()
if (remoteSshQueues.get(key) === queued) remoteSshQueues.delete(key)
}
}
@@ -3219,7 +3245,7 @@ async function readVisualizationBridgeStatus(): Promise<VisualizationBridgeStatu
}
async function waitForVisualizationBridgeReady(stack: VisualizationStack, bridgePort: number, timeoutMs = 20000): Promise<void> {
const endpoint = stack === 'slam' || stack === 'nav2' ? 'health' : 'stack'
const endpoint = stack === 'slam' || stack === 'slam3d' || stack === 'nav2' ? 'health' : 'stack'
const startedAt = Date.now()
let lastMessage = ''
@@ -4993,6 +5019,26 @@ async function stopRemoteWorkspaceModules(
}
}
async function resetRemoteRosSession(
targetInput: RemoteWorkspaceTarget,
): Promise<{ ok: boolean; message: string }> {
try {
const target = normalizeRemoteTarget(targetInput)
await runSsh(target, [
'set -e',
// Module containers are always named from their LMS module folder. Remove every
// one so a prior control session cannot keep publishing on the shared domain.
'MODULE_CONTAINERS="$(docker ps -a --format \"{{.ID}} {{.Names}}\" 2>/dev/null | awk \'$2 ~ /^lms-/ { print $1 }\')"',
'if [ -n "$MODULE_CONTAINERS" ]; then docker rm -f $MODULE_CONTAINERS >/dev/null 2>&1 || true; fi',
'docker rm -f nav2-ros-bridge autoware-ros-bridge slam-ros-bridge >/dev/null 2>&1 || true',
'find "$HOME/autonomy-studio/runtime" "$HOME/.autonomy-studio/runtime" -type f -path "*/docker/fastdds-peer-*.xml" -delete 2>/dev/null || true',
].join('\n'), 45_000)
return { ok: true, message: '원격 ROS 연결을 초기화했습니다.' }
} catch (error) {
return { ok: false, message: error instanceof Error ? error.message : String(error) }
}
}
async function resetRemoteWorkspaceSession(
targetInput: RemoteWorkspaceTarget,
modules: Array<{ moduleName: string; folderName?: string | null; templateRepositoryUrl?: string | null }>,
@@ -5328,13 +5374,6 @@ function stopTerminalsForWebContents(webContentsId: number): void {
}
}
function scheduleStopTerminal(terminalKey: string): void {
const currentTimer = terminalStopTimers.get(terminalKey)
if (currentTimer) clearTimeout(currentTimer)
const timer = setTimeout(() => stopTerminal(terminalKey), 300)
terminalStopTimers.set(terminalKey, timer)
}
function cancelStopTerminal(terminalKey: string): void {
const timer = terminalStopTimers.get(terminalKey)
if (!timer) return
@@ -5743,6 +5782,7 @@ function registerLocalWorkspaceHandlers(): void {
closeSplashAndShowMain()
})
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: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) => {
@@ -5815,6 +5855,7 @@ function registerLocalWorkspaceHandlers(): void {
ipcMain.handle('remote-workspace:modules:stop', (_event, target: RemoteWorkspaceTarget, moduleName: string, folderName?: string | null) => stopRemoteWorkspaceModule(target, moduleName, folderName))
ipcMain.handle('remote-workspace:modules:stop-many', (_event, target: RemoteWorkspaceTarget, modules: Array<{ moduleName: string; folderName?: string | null }>) => stopRemoteWorkspaceModules(target, modules))
ipcMain.handle('remote-workspace:modules:reset-session', (_event, target: RemoteWorkspaceTarget, modules: Array<{ moduleName: string; folderName?: string | null; templateRepositoryUrl?: string | null }>) => resetRemoteWorkspaceSession(target, modules))
ipcMain.handle('remote-workspace:ros:reset-session', (_event, target: RemoteWorkspaceTarget) => resetRemoteRosSession(target))
ipcMain.handle('remote-workspace:modules:restart', (_event, target: RemoteWorkspaceTarget, moduleName: string, folderName?: string | null, rosParams?: string | null, gpuEnabled?: boolean | null, rosDomainId?: string | null, runtimeImages?: RuntimeImageArtifact[] | null, rosNetwork?: RosNetworkConfig | null) => restartRemoteWorkspaceModule(target, moduleName, folderName, rosParams, gpuEnabled, rosDomainId, runtimeImages, rosNetwork))
ipcMain.handle('remote-workspace:modules:logs', (_event, target: RemoteWorkspaceTarget, moduleName: string, folderName?: string | null) => readRemoteWorkspaceModuleLogs(target, moduleName, folderName))
ipcMain.handle('remote-workspace:visualization:status', (_event, target: RemoteWorkspaceTarget) => readRemoteVisualizationBridgeStatus(target))