diff --git a/src/main/index.ts b/src/main/index.ts index bc5e8ca..a236243 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -3,7 +3,7 @@ import type { WebContents } from 'electron' import { spawn, type ChildProcessWithoutNullStreams } from 'child_process' import * as pty from 'node-pty' import type { IPty } from 'node-pty' -import { createWriteStream, existsSync } from 'fs' +import { appendFileSync, createWriteStream, existsSync, mkdirSync } from 'fs' import { cp, mkdir, readdir, readFile, rename, rm, stat, writeFile } from 'fs/promises' import { homedir } from 'os' import { basename, dirname, join, relative, resolve } from 'path' @@ -33,6 +33,20 @@ const slamSimulationRuntimeImage = 'nav2-slam:humble' const slamSimulationRuntimeContainerName = 'nav2-slam' const rosBridgePort = 8765 const exampleCodeRoot = resolve(homedir(), '다운로드', 'example-code') + +function writeRendererDiagnostic(kind: string, message: string): void { + try { + const logDirectory = app.getPath('logs') + mkdirSync(logDirectory, { recursive: true }) + appendFileSync( + join(logDirectory, 'renderer.log'), + `[${new Date().toISOString()}] [${kind}] ${message}\n`, + 'utf8', + ) + } catch { + // Diagnostics must never interrupt the LMS window itself. + } +} const skippedCacheDirs = new Set(['.git', 'build', 'install', 'log', 'node_modules', 'docker']) const textFilePattern = /\.(c|cc|cpp|cxx|h|hpp|hh|py|xml|urdf|xacro|yaml|yml|json|txt|md|cmake|launch|sh|bash|toml|ini|cfg|rviz)$/i const imageFilePattern = /\.(png|jpe?g|webp|gif|svg)$/i @@ -4035,6 +4049,9 @@ function registerTerminalHandlers(): void { if (process.platform === 'linux' && !is.dev) { app.commandLine.appendSwitch('no-sandbox') + // Some packaged LMS launches run with an inaccessible /dev/shm. Keep Chromium + // shared-memory files under the app temp directory so DevTools can open. + app.commandLine.appendSwitch('disable-dev-shm-usage') app.commandLine.appendSwitch('ignore-gpu-blocklist') app.commandLine.appendSwitch('enable-unsafe-swiftshader') } @@ -4044,7 +4061,8 @@ function attachDevToolsShortcut(win: BrowserWindow): void { if (input.type !== 'keyDown') return const key = input.key.toLowerCase() const code = input.code.toLowerCase() - if (input.key === 'F12' || code === 'f12' || ((input.control || input.meta) && input.shift && key === 'i')) { + // Some Linux keyboard layouts report the physical F12 key as Help. + if (input.key === 'F12' || key === 'help' || code === 'f12' || code === 'help' || ((input.control || input.meta) && input.shift && key === 'i')) { event.preventDefault() win.webContents.toggleDevTools() } @@ -4174,6 +4192,19 @@ function createWindow(): void { attachDevToolsShortcut(mainWindow) suppressSystemContextMenu(mainWindow) + mainWindow.webContents.on('console-message', (_event, level, message, line, sourceId) => { + if (level < 2) return + writeRendererDiagnostic('console', `${sourceId}:${line} [level=${level}] ${message}`) + }) + mainWindow.webContents.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL, isMainFrame) => { + writeRendererDiagnostic( + 'load-failed', + `${isMainFrame ? 'main-frame' : 'sub-frame'} code=${errorCode} url=${validatedURL} message=${errorDescription}`, + ) + }) + mainWindow.webContents.on('render-process-gone', (_event, details) => { + writeRendererDiagnostic('renderer-gone', `reason=${details.reason} exitCode=${details.exitCode}`) + }) mainWindow.once('ready-to-show', () => { setTimeout(() => { updateSplashStatus('로그인 세션 확인 중...') @@ -4396,6 +4427,15 @@ app.whenReady().then(() => { if (!target) return target.isMaximized() ? target.unmaximize() : target.maximize() }) + ipcMain.handle('titlebar-open-devtools', (event) => { + const target = BrowserWindow.fromWebContents(event.sender) + if (!target || target.isDestroyed()) return { opened: false, message: 'LMS 창을 찾지 못했습니다.' } + target.show() + target.focus() + target.webContents.openDevTools({ mode: 'detach', activate: true }) + setImmediate(() => target.webContents.devToolsWebContents?.focus()) + return { opened: true } + }) ipcMain.on('titlebar-close', (event) => BrowserWindow.fromWebContents(event.sender)?.close()) ipcMain.on('app-quit', () => app.quit()) ipcMain.handle('learning-video:open', (event, input: LearningVideoInput) => openLearningVideoWindow(BrowserWindow.fromWebContents(event.sender), input)) diff --git a/src/preload/index.ts b/src/preload/index.ts index 25633e1..0f7b5eb 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -91,6 +91,7 @@ contextBridge.exposeInMainWorld('lmsBootSplash', { contextBridge.exposeInMainWorld('titlebar', { minimize: () => ipcRenderer.send('titlebar-minimize'), maximize: () => ipcRenderer.send('titlebar-maximize'), + openDevTools: () => ipcRenderer.invoke('titlebar-open-devtools'), close: () => ipcRenderer.send('titlebar-close') }) contextBridge.exposeInMainWorld('appLifecycle', { diff --git a/src/renderer/public/slam-viewer/index.html b/src/renderer/public/slam-viewer/index.html index 980bde4..9ed7cab 100644 --- a/src/renderer/public/slam-viewer/index.html +++ b/src/renderer/public/slam-viewer/index.html @@ -153,6 +153,11 @@ align-items:center; gap:6px; } + #splatToggle { + display:inline-flex; + align-items:center; + justify-content:center; + } #cameraPresetSelect { min-width:86px; padding:0 24px 0 9px; @@ -292,8 +297,8 @@