feat: add daily logging and improve workspace UI and ROS status
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import { app } from 'electron'
|
||||
import { dirname, join } from 'path'
|
||||
import { inspect } from 'util'
|
||||
import { createDailyLog } from './dailyLog'
|
||||
|
||||
const fallback = app.getPath('logs')
|
||||
export const appLog = createDailyLog([
|
||||
process.env.APPIMAGE
|
||||
? join(dirname(process.env.APPIMAGE), 'logs')
|
||||
: app.isPackaged ? join(dirname(process.execPath), 'logs') : fallback,
|
||||
fallback
|
||||
])
|
||||
|
||||
for (const level of ['warn', 'error'] as const) {
|
||||
const original = console[level].bind(console)
|
||||
console[level] = (...args: unknown[]) => {
|
||||
appLog.write(level, args.map((value) => typeof value === 'string' ? value : inspect(value, { depth: 4 })).join(' '))
|
||||
original(...args)
|
||||
}
|
||||
}
|
||||
|
||||
// Monitor fatal exceptions without changing Node's normal termination behavior.
|
||||
process.on('uncaughtExceptionMonitor', (error, origin) => {
|
||||
appLog.write(origin, error.stack ?? String(error))
|
||||
})
|
||||
process.on('warning', (warning) => appLog.write('warning', warning.stack ?? warning.message))
|
||||
process.on('exit', (code) => appLog.write('exit', `code=${code}`))
|
||||
app.on('child-process-gone', (_event, details) => appLog.write('child-process-gone', JSON.stringify(details)))
|
||||
appLog.write('startup', `version=${app.getVersion()} platform=${process.platform} arch=${process.arch} electron=${process.versions.electron} executable=${process.env.APPIMAGE || process.execPath}`)
|
||||
@@ -0,0 +1,8 @@
|
||||
import { app } from 'electron'
|
||||
import { appLog } from './appLogging'
|
||||
|
||||
// Load native dependencies only after the startup logger is ready.
|
||||
void import('./index').catch((error: unknown) => {
|
||||
appLog.write('startup-error', error instanceof Error ? error.stack ?? error.message : String(error))
|
||||
app.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,51 @@
|
||||
import { appendFileSync, closeSync, mkdirSync, openSync, readdirSync, unlinkSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
|
||||
export function redactLog(value: string): string {
|
||||
return value
|
||||
.replace(/(https?:\/\/)[^\s/@]+@/gi, '$1[REDACTED]@')
|
||||
.replace(/\bBearer\s+[^\s"',;]+/gi, 'Bearer [REDACTED]')
|
||||
.replace(/((?:[?&]|--)(?:[^\s=&]*token|password|secret|code|key)=)[^\s&#"']+/gi, '$1[REDACTED]')
|
||||
.replace(/(["']?(?:[\w-]*token|password|secret|authorization|api[_-]?key)["']?\s*[:=]\s*)(?:"[^"]*"|'[^']*'|[^\s,;}]+)/gi, '$1[REDACTED]')
|
||||
}
|
||||
|
||||
function localDate(now: Date): string {
|
||||
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
export function createDailyLog(directories: string[], now: () => Date = () => new Date()) {
|
||||
let activePath = ''
|
||||
let activeDay = ''
|
||||
|
||||
function selectFile(): string {
|
||||
const day = localDate(now())
|
||||
if (activePath && activeDay === day) return activePath
|
||||
for (const directory of [...new Set(directories)]) {
|
||||
try {
|
||||
mkdirSync(directory, { recursive: true })
|
||||
const path = join(directory, `lms-${day}.log`)
|
||||
closeSync(openSync(path, 'a', 0o600))
|
||||
activePath = path
|
||||
activeDay = day
|
||||
for (const name of readdirSync(directory)) {
|
||||
if (/^lms-\d{4}-\d{2}-\d{2}\.log$/.test(name) && name < `lms-${day}.log`) {
|
||||
try { unlinkSync(join(directory, name)) } catch { /* Retention must not block startup. */ }
|
||||
}
|
||||
}
|
||||
return path
|
||||
} catch { /* Try the user-writable fallback directory. */ }
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
return {
|
||||
path: selectFile,
|
||||
write(kind: string, message: string): void {
|
||||
try {
|
||||
const path = selectFile()
|
||||
if (!path) return
|
||||
appendFileSync(path, `[${now().toISOString()}] [${redactLog(kind)}] ${redactLog(message)}\n`, { mode: 0o600 })
|
||||
} catch { /* Diagnostics must not interrupt the app. */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-13
@@ -1,4 +1,5 @@
|
||||
import { app, BrowserWindow, dialog, Menu, globalShortcut, ipcMain, screen, shell } from 'electron'
|
||||
import { appLog } from './appLogging'
|
||||
import { checkDockerStatus } from './dockerStatus'
|
||||
import { cameraRuntimeArtifacts } from './cameraRuntime'
|
||||
import { remoteModuleFilesScript } from './remoteModuleFiles'
|
||||
@@ -12,7 +13,7 @@ import type { WebContents } from 'electron'
|
||||
import { spawn } from 'child_process'
|
||||
import * as pty from 'node-pty'
|
||||
import type { IPty } from 'node-pty'
|
||||
import { appendFileSync, createReadStream, createWriteStream, existsSync, mkdirSync } from 'fs'
|
||||
import { createReadStream, createWriteStream, existsSync } 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'
|
||||
@@ -51,21 +52,12 @@ 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.
|
||||
}
|
||||
appLog.write(kind, message)
|
||||
}
|
||||
|
||||
async function readAppDiagnostics(): Promise<{ path: string; content: string }> {
|
||||
const logPath = join(app.getPath('logs'), 'renderer.log')
|
||||
const logPath = appLog.path()
|
||||
if (!logPath) return { path: '', content: '' }
|
||||
|
||||
try {
|
||||
const content = await readFile(logPath, 'utf8')
|
||||
|
||||
Reference in New Issue
Block a user