diff --git a/package.json b/package.json index c9ea32c..d596333 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lms", - "version": "1.1.19", + "version": "1.1.20", "description": "Autonomy Studio LMS Electron app", "main": "./out/main/index.js", "author": { diff --git a/src/main/index.ts b/src/main/index.ts index 969765f..72d79b0 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -977,7 +977,7 @@ function buildCustomModuleFiles(input: CustomModuleScaffoldInput): Record&2; exit 1; }', + 'if [[ "$MODULE_FOLDER" == *lms-nav2-custom* ]]; then', + ' test -f "$MODULE_PATH/package.xml" || { echo "Custom module source is missing: $MODULE_FOLDER" >&2; exit 1; }', + ' SOURCE_BASE_PATH=.', + 'else', + ' test -d "$MODULE_PATH/src" || { echo "Module source is missing: $MODULE_FOLDER" >&2; exit 1; }', + ' SOURCE_BASE_PATH=src', + 'fi', 'BUILD_CONTAINER="nav2-build-${TARGET}"', 'cleanup() {', ' docker run --rm -v "$MODULE_ROOT:/modules" "$IMAGE" bash -lc "chown -R $(id -u):$(id -g) \'/modules/$MODULE_FOLDER/build\' \'/modules/$MODULE_FOLDER/install\' \'/modules/$MODULE_FOLDER/log\' 2>/dev/null || true" >/dev/null 2>&1 || true', @@ -4302,10 +4308,13 @@ function buildNav2DockerBuildScript(): string { " \"$IMAGE\" bash -lc '", ' set -e', ' source /opt/ros/humble/setup.bash', - ' mapfile -t paths < <(colcon list --base-paths src --paths-only)', + ' mapfile -t paths < <(colcon list --base-paths "$3" --paths-only)', ' ROSDEP_SKIP_ARGS=()', + ' if [[ "$1" == *lms-nav2-custom* ]]; then', + ' ROSDEP_SKIP_ARGS+=(--skip-keys ament_python)', + ' fi', ' if ! apt-cache show ros-humble-gazebo-ros >/dev/null 2>&1; then', - ' ROSDEP_SKIP_ARGS=(--skip-keys gazebo_ros)', + ' ROSDEP_SKIP_ARGS+=(--skip-keys gazebo_ros)', ' fi', " rosdep install --from-paths \"${paths[@]}\" --ignore-src \"${ROSDEP_SKIP_ARGS[@]}\" \\", " --dependency-types buildtool --dependency-types build \\", @@ -4314,9 +4323,9 @@ function buildNav2DockerBuildScript(): string { ' if [[ "$1" == *lms-nav2-gazebo-slam* && "$1" != *lms-nav2-gazebo-slam3d* ]]; then', ' BUILD_TARGETS=(slam_pkg slam2d_pkg slam3d_pkg)', ' fi', - " colcon --log-base log build --base-paths src --build-base build --install-base install \\", + " colcon --log-base log build --base-paths \"$3\" --build-base build --install-base install \\", ' --executor sequential --cmake-args -DBUILD_TESTING=OFF --packages-up-to "${BUILD_TARGETS[@]}"', - "' bash \"$MODULE_FOLDER\" \"$TARGET\"", + "' bash \"$MODULE_FOLDER\" \"$TARGET\" \"$SOURCE_BASE_PATH\"", '' ].join('\n') } diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 8f9cc40..2664139 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -2081,10 +2081,12 @@ function LocalPcWorkspaceMockup({ fitTerminal, copyTerminalSelection, pasteTerminalClipboard, - startTerminalResize + startTerminalResize, + stopAllTerminalSessions } = useWorkspaceTerminal({ moduleName: selectedModule?.name, moduleFolderName: selectedModule?.folderName, + openModuleNames: moduleTabs.map((module) => module.name), clonePathPreview, remoteWorkspace, getRemoteWorkspaceTarget, @@ -2950,6 +2952,7 @@ function LocalPcWorkspaceMockup({ setPendingEditorCloseRequest({ kind: 'workspace' }) return } + stopAllTerminalSessions() onClose() } const performPendingEditorCloseRequest = (): void => { @@ -2995,6 +2998,7 @@ function LocalPcWorkspaceMockup({ } discardDirtyFiles(Object.keys(dirtyEditorFiles).filter((file) => dirtyEditorFiles[file])) + stopAllTerminalSessions() onClose() } const revealSearchMatch = (lineNumber: number, query: string): boolean => { diff --git a/src/renderer/src/features/workspace/useWorkspaceTerminal.ts b/src/renderer/src/features/workspace/useWorkspaceTerminal.ts index 45a4600..44b5d9f 100644 --- a/src/renderer/src/features/workspace/useWorkspaceTerminal.ts +++ b/src/renderer/src/features/workspace/useWorkspaceTerminal.ts @@ -10,9 +10,19 @@ type TerminalContextMenu = { y: number } +type ManagedTerminalSession = { + moduleName: string + remoteWorkspace: boolean + attachedTerminal: XTerm | null + suppressedData: string[] + removeDataListener: () => void + removeSuppressListener: () => void +} + type UseWorkspaceTerminalOptions = { moduleName?: string | null moduleFolderName?: string | null + openModuleNames: string[] clonePathPreview: string remoteWorkspace: boolean getRemoteWorkspaceTarget: () => RemoteWorkspaceTarget @@ -32,11 +42,13 @@ type UseWorkspaceTerminalResult = { copyTerminalSelection: () => void pasteTerminalClipboard: () => void startTerminalResize: (event: React.MouseEvent) => void + stopAllTerminalSessions: () => void } export function useWorkspaceTerminal({ moduleName, moduleFolderName, + openModuleNames, clonePathPreview, remoteWorkspace, getRemoteWorkspaceTarget, @@ -52,12 +64,31 @@ export function useWorkspaceTerminal({ const terminalFitRef = useRef(null) const terminalSessionIdRef = useRef(null) const remoteTargetResolverRef = useRef(getRemoteWorkspaceTarget) - const terminalSuppressQueueRef = useRef([]) const terminalOutputBufferRef = useRef>({}) + const terminalSessionsRef = useRef>(new Map()) + const resetKeyRef = useRef(resetKey) const [terminalPanelHeight, setTerminalPanelHeight] = useState(235) remoteTargetResolverRef.current = getRemoteWorkspaceTarget + const stopTerminalSession = (sessionId: string): void => { + const session = terminalSessionsRef.current.get(sessionId) + if (!session) return + + session.attachedTerminal = null + session.removeDataListener() + session.removeSuppressListener() + terminalSessionsRef.current.delete(sessionId) + delete terminalOutputBufferRef.current[sessionId] + + if (session.remoteWorkspace) window.remoteWorkspace.terminal.stop(sessionId) + else window.localWorkspace.terminal.stop(sessionId) + } + + const stopAllTerminalSessions = (): void => { + Array.from(terminalSessionsRef.current.keys()).forEach(stopTerminalSession) + } + const fitTerminal = (): void => { terminalFitRef.current?.fit() } @@ -115,6 +146,21 @@ export function useWorkspaceTerminal({ document.addEventListener('mouseup', onMouseUp) } + const openModuleNamesKey = openModuleNames.join('\u0000') + + useEffect(() => { + const openModuleNameSet = new Set(openModuleNames) + Array.from(terminalSessionsRef.current.entries()).forEach(([sessionId, session]) => { + if (!openModuleNameSet.has(session.moduleName)) stopTerminalSession(sessionId) + }) + }, [openModuleNamesKey]) + + useEffect(() => { + if (resetKeyRef.current === resetKey) return + resetKeyRef.current = resetKey + stopAllTerminalSessions() + }, [resetKey]) + useEffect(() => { if (!moduleName || !terminalActive || !terminalHostRef.current || terminalRef.current) return const terminalSessionId = getModuleTerminalSessionId(moduleName) @@ -164,22 +210,36 @@ export function useWorkspaceTerminal({ terminalFitRef.current = fitAddon const terminalApi = remoteWorkspace ? window.remoteWorkspace.terminal : window.localWorkspace.terminal - const removeDataListener = terminalApi.onData((data) => { - const suppressQueue = terminalSuppressQueueRef.current - if (suppressQueue.length && suppressQueue[0] === data) { - suppressQueue.shift() - return - } + let terminalSession = terminalSessionsRef.current.get(terminalSessionId) + if (!terminalSession || terminalSession.remoteWorkspace !== remoteWorkspace) { + if (terminalSession) stopTerminalSession(terminalSessionId) - const nextData = stripInternalTerminalCommands(data) - if (nextData) { + const suppressedData: string[] = [] + const removeDataListener = terminalApi.onData((data) => { + if (suppressedData.length && suppressedData[0] === data) { + suppressedData.shift() + return + } + + const nextData = stripInternalTerminalCommands(data) + if (!nextData) return terminalOutputBufferRef.current[terminalSessionId] = `${terminalOutputBufferRef.current[terminalSessionId] ?? ''}${nextData}`.slice(-60000) - term.write(nextData) + terminalSessionsRef.current.get(terminalSessionId)?.attachedTerminal?.write(nextData) + }, terminalSessionId) + const removeSuppressListener = terminalApi.onSuppressData((data) => { + suppressedData.push(data) + }, terminalSessionId) + terminalSession = { + moduleName, + remoteWorkspace, + attachedTerminal: null, + suppressedData, + removeDataListener, + removeSuppressListener } - }, terminalSessionId) - const removeSuppressListener = terminalApi.onSuppressData((data) => { - terminalSuppressQueueRef.current.push(data) - }, terminalSessionId) + terminalSessionsRef.current.set(terminalSessionId, terminalSession) + } + terminalSession.attachedTerminal = term const inputDisposable = term.onData((data) => terminalApi.write(data, terminalSessionId)) let sizeSyncTimer: number | null = null @@ -215,9 +275,10 @@ export function useWorkspaceTerminal({ }) }) void startTerminal.then((started) => { + if (disposed || !started) return fitAddon.fit() - if (!started) return sizeSyncTimer = window.setTimeout(() => { + if (disposed) return fitAddon.fit() if (remoteWorkspace) { void window.remoteWorkspace.terminal.resize(remoteTargetResolverRef.current(), term.cols, term.rows, terminalSessionId) @@ -237,19 +298,14 @@ export function useWorkspaceTerminal({ window.removeEventListener('resize', onResize) terminalElement.removeEventListener('contextmenu', handleTerminalContextMenu) terminalElement.removeEventListener('mousedown', focusTerminal) - removeDataListener() - removeSuppressListener() + const currentSession = terminalSessionsRef.current.get(terminalSessionId) + if (currentSession?.attachedTerminal === term) currentSession.attachedTerminal = null inputDisposable.dispose() if (sizeSyncTimer !== null) window.clearTimeout(sizeSyncTimer) - // Full-screen programs such as vi cannot be restored from a partial raw output buffer. - delete terminalOutputBufferRef.current[terminalSessionId] - if (remoteWorkspace) window.remoteWorkspace.terminal.stop(terminalSessionId) - else window.localWorkspace.terminal.stop(terminalSessionId) term.dispose() terminalRef.current = null terminalFitRef.current = null terminalSessionIdRef.current = null - terminalSuppressQueueRef.current = [] } }, [clonePathPreview, dark, moduleFolderName, moduleName, remoteWorkspace, resetKey, terminalActive]) @@ -262,6 +318,7 @@ export function useWorkspaceTerminal({ fitTerminal, copyTerminalSelection, pasteTerminalClipboard, - startTerminalResize + startTerminalResize, + stopAllTerminalSessions } }