(fix) bug remote workspace ssh..
This commit is contained in:
+65
-10
@@ -22,6 +22,8 @@ const terminalStopTimers = new Map<string, NodeJS.Timeout>()
|
||||
const intentionalTerminalStops = new Set<string>()
|
||||
const nav2RuntimeObstacleNames = new Set<string>()
|
||||
const remoteSshQueues = new Map<string, Promise<void>>()
|
||||
const remoteGitModuleRoots = new Map<string, string>()
|
||||
const remoteSshControlDirectory = resolve(homedir(), '.autonomy-studio', 'ssh', 'control')
|
||||
const moduleCacheRoot = resolve(homedir(), '.caches')
|
||||
const artifactCacheRoot = resolve(homedir(), '.cache', 'autonomy-studio', 'artifacts')
|
||||
const studioRuntimeRoot = resolve(homedir(), 'autonomy-studio', 'runtime')
|
||||
@@ -367,6 +369,19 @@ function normalizeWorldLayout(value: unknown): WorldLayout {
|
||||
return { version: 1, bounds: { width: size(bounds.width, 10), depth: size(bounds.depth, 10) }, objects }
|
||||
}
|
||||
|
||||
function isSavedMapCollisionObject(object: WorldLayoutObject): boolean {
|
||||
return /^saved-(?:map-obstacle|3d-map-voxel)-/i.test(object.id)
|
||||
}
|
||||
|
||||
// Saved-map collision layouts used to be copied into world-layout.json. Keep
|
||||
// that file exclusively for the custom map and discard only those old generated
|
||||
// objects while preserving any user-created objects that may coexist there.
|
||||
function normalizeCustomWorldLayout(value: unknown): WorldLayout {
|
||||
const layout = normalizeWorldLayout(value)
|
||||
const objects = layout.objects.filter((object) => !isSavedMapCollisionObject(object))
|
||||
return objects.length === layout.objects.length ? layout : { ...layout, objects }
|
||||
}
|
||||
|
||||
function worldLayoutPath(workspaceRoot?: string | null): string {
|
||||
const root = expandUserPath(workspaceRoot)
|
||||
if (!root) throw new Error('워크스페이스 경로가 설정되어 있지 않습니다.')
|
||||
@@ -527,7 +542,6 @@ async function generateWorkspaceMapCollisionLayout(workspaceRoot: string | null
|
||||
const layout = createMapCollisionLayout(await readFile(yamlFilePath, 'utf8'), await readFile(pgmFilePath))
|
||||
await mkdir(mapsDirectory, { recursive: true })
|
||||
await writeJsonAtomically(resolve(mapsDirectory, savedMapCollisionLayoutFileName(yamlFileName)), layout)
|
||||
await writeJsonAtomically(worldLayoutPath(root), layout)
|
||||
return layout
|
||||
}
|
||||
|
||||
@@ -681,7 +695,6 @@ async function generateWorkspacePointCloudCollisionLayout(
|
||||
&& cached.source.modifiedAtMs === sourceStat.mtimeMs
|
||||
&& cached.layout) {
|
||||
const layout = normalizeWorldLayout(cached.layout)
|
||||
await writeJsonAtomically(worldLayoutPath(root), layout)
|
||||
return layout
|
||||
}
|
||||
} catch {
|
||||
@@ -697,7 +710,6 @@ async function generateWorkspacePointCloudCollisionLayout(
|
||||
source: { fileName: databaseFileName, size: sourceStat.size, modifiedAtMs: sourceStat.mtimeMs },
|
||||
layout,
|
||||
} satisfies PointCloudLayoutCache)
|
||||
await writeJsonAtomically(worldLayoutPath(root), layout)
|
||||
return layout
|
||||
}
|
||||
|
||||
@@ -1365,7 +1377,7 @@ async function collectRemoteAvailableModules(
|
||||
}
|
||||
})
|
||||
.sort((a, b) => a.sortOrder - b.sortOrder)
|
||||
const output = await runSshOutput(targetInput ?? target, buildRemoteModuleProbeScript(target.workspaceRoot, moduleInputs), 15000)
|
||||
const output = await runSshOutput(targetInput ?? target, buildRemoteModuleProbeScript(target.workspaceRoot, moduleInputs), 30_000)
|
||||
const dockerStatuses = new Map<string, { running: boolean; exists: boolean; status: string }>()
|
||||
const dockerStats = new Map<string, { cpu: number; memory: number }>()
|
||||
const installedModules = new Map<string, { path: string; parameters: string[] }>()
|
||||
@@ -1843,6 +1855,7 @@ async function queueRemoteSsh<T>(target: ReturnType<typeof normalizeRemoteTarget
|
||||
|
||||
async function runSshOutput(target: RemoteWorkspaceTarget, command: string, timeoutMs = 120_000): Promise<string> {
|
||||
const normalized = normalizeRemoteTarget(target)
|
||||
await mkdir(remoteSshControlDirectory, { recursive: true })
|
||||
return queueRemoteSsh(normalized, () => runCommandOutput(
|
||||
'ssh',
|
||||
[
|
||||
@@ -1854,6 +1867,18 @@ async function runSshOutput(target: RemoteWorkspaceTarget, command: string, time
|
||||
'StrictHostKeyChecking=accept-new',
|
||||
'-o',
|
||||
'ConnectTimeout=5',
|
||||
'-o',
|
||||
'PreferredAuthentications=publickey',
|
||||
'-o',
|
||||
'PasswordAuthentication=no',
|
||||
'-o',
|
||||
'KbdInteractiveAuthentication=no',
|
||||
'-o',
|
||||
'ControlMaster=auto',
|
||||
'-o',
|
||||
'ControlPersist=120',
|
||||
'-o',
|
||||
`ControlPath=${join(remoteSshControlDirectory, 'cm-%C')}`,
|
||||
'-i',
|
||||
normalized.identityFile,
|
||||
`${normalized.user}@${normalized.host}`,
|
||||
@@ -2123,10 +2148,10 @@ function buildRemoteModuleProbeScript(workspaceRoot: string, modules: CourseModu
|
||||
'set +e',
|
||||
`ROOT=${quoteShellSingle(workspaceRoot)}`,
|
||||
...expandRemotePathVar('ROOT'),
|
||||
'timeout 12s docker ps -a --format "DOCKER\\t{{.Names}}\\t{{.Status}}" 2>/dev/null || true',
|
||||
'timeout 5s docker ps -a --format "DOCKER\\t{{.Names}}\\t{{.Status}}" 2>/dev/null || true',
|
||||
// docker stats can stall on a remote Jetson. Module state must not wait
|
||||
// behind a long statistics sample.
|
||||
'timeout 3s docker stats --no-stream --format "STATS\\t{{.Name}}\\t{{.CPUPerc}}\\t{{.MemPerc}}" 2>/dev/null || true',
|
||||
'timeout 2s docker stats --no-stream --format "STATS\\t{{.Name}}\\t{{.CPUPerc}}\\t{{.MemPerc}}" 2>/dev/null || true',
|
||||
...moduleFolders.map((folderName) => {
|
||||
const modulePath = remotePathJoin('$ROOT', folderName)
|
||||
return [
|
||||
@@ -2344,6 +2369,9 @@ async function resolveRemoteGitModuleRoot(
|
||||
): Promise<{ target: ReturnType<typeof normalizeRemoteTarget>; moduleRoot: string }> {
|
||||
const target = normalizeRemoteTarget(targetInput)
|
||||
const targetFolderName = folderName?.trim() || moduleName
|
||||
const cacheKey = `${target.user}@${target.host}:${target.port}:${target.workspaceRoot}:${targetFolderName}`
|
||||
const cachedModuleRoot = remoteGitModuleRoots.get(cacheKey)
|
||||
if (cachedModuleRoot) return { target, moduleRoot: cachedModuleRoot }
|
||||
const output = await runSshOutput(target, [
|
||||
'set -e',
|
||||
`ROOT=${quoteShellSingle(target.workspaceRoot)}`,
|
||||
@@ -2356,6 +2384,7 @@ async function resolveRemoteGitModuleRoot(
|
||||
].join('\n'), 8000)
|
||||
const moduleRoot = output.trim()
|
||||
if (!moduleRoot) throw new Error('원격 Git 저장소를 찾을 수 없습니다.')
|
||||
remoteGitModuleRoots.set(cacheKey, moduleRoot)
|
||||
return { target, moduleRoot }
|
||||
}
|
||||
|
||||
@@ -3347,6 +3376,7 @@ async function startVisualizationBridge(
|
||||
environmentProfile?: EnvironmentProfileConfig | null,
|
||||
rosNetwork?: RosNetworkConfig | null,
|
||||
savedMapDatabasePath?: string | null,
|
||||
savedMapWorldLayoutPath?: string | null,
|
||||
): Promise<VisualizationBridgeStatus> {
|
||||
const stack = normalizeVisualizationStack(stackInput ?? environmentProfile?.type?.toLowerCase())
|
||||
const resolvedProfile = withRuntimeProfileDefaults(environmentProfile, stack)
|
||||
@@ -3358,6 +3388,25 @@ async function startVisualizationBridge(
|
||||
const usesSlamBridge = stack === 'slam' || stack === 'slam3d' || stack === 'nav2'
|
||||
const layoutWorkspaceRoot = usesSlamBridge ? expandUserPath(workspaceRoot) : null
|
||||
if (layoutWorkspaceRoot) await mkdir(resolve(layoutWorkspaceRoot, 'maps'), { recursive: true })
|
||||
const savedMapWorldLayoutFileName = savedMapWorldLayoutPath
|
||||
? safeSavedMapFileName(basename(savedMapWorldLayoutPath))
|
||||
: null
|
||||
const requestedWorldLayoutPath = savedMapWorldLayoutFileName
|
||||
? `/workspace/maps/${savedMapWorldLayoutFileName}`
|
||||
: '/workspace/world-layout.json'
|
||||
if (layoutWorkspaceRoot && !savedMapWorldLayoutFileName) {
|
||||
const customLayoutPath = worldLayoutPath(layoutWorkspaceRoot)
|
||||
try {
|
||||
const currentLayout = await readFile(customLayoutPath, 'utf8')
|
||||
const parsedLayout = JSON.parse(currentLayout)
|
||||
const normalizedLayout = normalizeCustomWorldLayout(parsedLayout)
|
||||
if (JSON.stringify(normalizedLayout) !== JSON.stringify(parsedLayout)) {
|
||||
await writeJsonAtomically(customLayoutPath, normalizedLayout)
|
||||
}
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
||||
}
|
||||
}
|
||||
const requestedRosDomainId = String(rosDomainId ?? process.env.ROS_DOMAIN_ID ?? '0').trim() || '0'
|
||||
const requestedRosNetworkKey = rosNetworkEnabled(rosNetwork)
|
||||
? `peer:${rosNetwork?.peer?.host?.trim() ?? ''}`
|
||||
@@ -3384,10 +3433,16 @@ async function startVisualizationBridge(
|
||||
const runningWorkspaceRoot = layoutWorkspaceRoot
|
||||
? await readContainerMountSource(containerName, '/workspace')
|
||||
: null
|
||||
const runningWorldLayoutPath = layoutWorkspaceRoot
|
||||
? await readContainerEnvironmentValue(containerName, 'WORLD_LAYOUT_PATH')
|
||||
: null
|
||||
if (response?.ok
|
||||
&& (!runningRosDomainId || runningRosDomainId === requestedRosDomainId)
|
||||
&& runningRosNetworkKey === requestedRosNetworkKey
|
||||
&& (!layoutWorkspaceRoot || runningWorkspaceRoot === resolve(layoutWorkspaceRoot))) {
|
||||
&& (!layoutWorkspaceRoot || (
|
||||
runningWorkspaceRoot === resolve(layoutWorkspaceRoot)
|
||||
&& runningWorldLayoutPath === requestedWorldLayoutPath
|
||||
))) {
|
||||
return readVisualizationBridgeStatus()
|
||||
}
|
||||
await runCommand('docker', ['rm', '-f', containerName], undefined, undefined, 10000).catch(() => undefined)
|
||||
@@ -3448,7 +3503,7 @@ async function startVisualizationBridge(
|
||||
if (layoutWorkspaceRoot) {
|
||||
args.push(
|
||||
'-v', `${layoutWorkspaceRoot}:/workspace`,
|
||||
'-e', 'WORLD_LAYOUT_PATH=/workspace/world-layout.json',
|
||||
'-e', `WORLD_LAYOUT_PATH=${requestedWorldLayoutPath}`,
|
||||
'-e', 'SAVED_MAPS_PATH=/workspace/maps',
|
||||
'-e', 'RTABMAP_DATABASE_PATH=/workspace/maps/rtabmap-active.db',
|
||||
)
|
||||
@@ -5804,7 +5859,7 @@ function registerLocalWorkspaceHandlers(): void {
|
||||
ipcMain.handle('local-workspace:world-layout:read', async (_event, workspaceRoot?: string | null) => {
|
||||
const targetPath = worldLayoutPath(workspaceRoot)
|
||||
try {
|
||||
return normalizeWorldLayout(JSON.parse(await readFile(targetPath, 'utf8')))
|
||||
return normalizeCustomWorldLayout(JSON.parse(await readFile(targetPath, 'utf8')))
|
||||
} catch (error) {
|
||||
const code = (error as NodeJS.ErrnoException).code
|
||||
if (code === 'ENOENT') return defaultWorldLayout()
|
||||
@@ -5902,7 +5957,7 @@ function registerLocalWorkspaceHandlers(): void {
|
||||
return { ok: true }
|
||||
})
|
||||
ipcMain.handle('local-workspace:visualization:status', () => readVisualizationBridgeStatus())
|
||||
ipcMain.handle('local-workspace:visualization:start', (_event, stack?: string | null, workspaceRoot?: string | null, rosDomainId?: string | null, environmentProfile?: EnvironmentProfileConfig | null, rosNetwork?: RosNetworkConfig | null, savedMapDatabasePath?: string | null) => startVisualizationBridge(stack, workspaceRoot, rosDomainId, environmentProfile, rosNetwork, savedMapDatabasePath))
|
||||
ipcMain.handle('local-workspace:visualization:start', (_event, stack?: string | null, workspaceRoot?: string | null, rosDomainId?: string | null, environmentProfile?: EnvironmentProfileConfig | null, rosNetwork?: RosNetworkConfig | null, savedMapDatabasePath?: string | null, savedMapWorldLayoutPath?: string | null) => startVisualizationBridge(stack, workspaceRoot, rosDomainId, environmentProfile, rosNetwork, savedMapDatabasePath, savedMapWorldLayoutPath))
|
||||
ipcMain.handle('local-workspace:visualization:stop', (_event, environmentProfile?: EnvironmentProfileConfig | null) => stopVisualizationBridge(environmentProfile))
|
||||
ipcMain.handle('local-workspace:git:status', (_event, moduleName: string, workspaceRoot?: string | null, folderName?: string | null, selectedFilePath?: string | null) => readWorkspaceGitStatus(moduleName, workspaceRoot, folderName, selectedFilePath))
|
||||
ipcMain.handle('local-workspace:git:submit', (_event, moduleName: string, workspaceRoot?: string | null, folderName?: string | null, message?: string | null, authorName?: string | null, authorEmail?: string | null, submissionId?: string | null, repositoryUrl?: string | null) => submitWorkspaceGitChanges(moduleName, workspaceRoot, folderName, message, authorName, authorEmail, submissionId, repositoryUrl))
|
||||
|
||||
Reference in New Issue
Block a user