[wip] crash log #4
|
|
@ -4,6 +4,12 @@ const app = new Hype
|
|||
|
||||
app.get('/', c => c.html(<h1>Hi there!</h1>))
|
||||
|
||||
// Test crash - remove after testing
|
||||
setTimeout(() => {
|
||||
console.log('About to crash...')
|
||||
throw new Error('Test crash!')
|
||||
}, 3000)
|
||||
|
||||
const apps = () => {
|
||||
}
|
||||
|
||||
|
|
|
|||
90
src/cli/commands/crashes.ts
Normal file
90
src/cli/commands/crashes.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import color from 'kleur'
|
||||
import { get, makeUrl } from '../http'
|
||||
import { resolveAppName } from '../name'
|
||||
|
||||
interface CrashSummary {
|
||||
date: string
|
||||
exitCode: string
|
||||
filename: string
|
||||
time: string
|
||||
uptime: string
|
||||
}
|
||||
|
||||
const parseCrashFilename = (filename: string): { date: string, time: string } => {
|
||||
// Format: YYYY-MM-DD-HHMMSS.txt
|
||||
const match = filename.match(/^(\d{4}-\d{2}-\d{2})-(\d{2})(\d{2})(\d{2})\.txt$/)
|
||||
if (!match) return { date: filename, time: '' }
|
||||
const [, date, h, m, s] = match
|
||||
return { date: date!, time: `${h}:${m}:${s}` }
|
||||
}
|
||||
|
||||
const parseCrashReport = (content: string): { exitCode: string, uptime: string } => {
|
||||
const lines = content.split('\n')
|
||||
let exitCode = 'unknown'
|
||||
let uptime = 'unknown'
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('Exit Code: ')) {
|
||||
exitCode = line.slice('Exit Code: '.length)
|
||||
} else if (line.startsWith('Uptime: ')) {
|
||||
uptime = line.slice('Uptime: '.length)
|
||||
}
|
||||
}
|
||||
|
||||
return { exitCode, uptime }
|
||||
}
|
||||
|
||||
export async function crashesApp(arg: string | undefined) {
|
||||
const name = resolveAppName(arg)
|
||||
if (!name) return
|
||||
|
||||
const files = await get<string[]>(`/api/apps/${name}/crashes`)
|
||||
if (!files) {
|
||||
console.error(`App not found: ${name}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (files.length === 0) {
|
||||
console.log('No crash reports found')
|
||||
return
|
||||
}
|
||||
|
||||
console.log(color.bold(`Crash history for ${name}:`))
|
||||
console.log()
|
||||
|
||||
// Fetch summary info for each crash
|
||||
const crashes: CrashSummary[] = []
|
||||
for (const filename of files.slice(0, 10)) { // Show last 10 crashes
|
||||
const content = await fetchCrashContent(name, filename)
|
||||
const { date, time } = parseCrashFilename(filename)
|
||||
const { exitCode, uptime } = content
|
||||
? parseCrashReport(content)
|
||||
: { exitCode: 'unknown', uptime: 'unknown' }
|
||||
|
||||
crashes.push({ filename, date, time, exitCode, uptime })
|
||||
}
|
||||
|
||||
for (const crash of crashes) {
|
||||
const exitColor = crash.exitCode === '0' ? color.green : color.red
|
||||
console.log(
|
||||
` ${color.gray(crash.date)} ${color.gray(crash.time)} ` +
|
||||
`exit ${exitColor(crash.exitCode)} ` +
|
||||
`uptime ${color.cyan(crash.uptime)}`
|
||||
)
|
||||
}
|
||||
|
||||
if (files.length > 10) {
|
||||
console.log()
|
||||
console.log(color.gray(` ... and ${files.length - 10} more`))
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCrashContent(appName: string, filename: string): Promise<string | null> {
|
||||
try {
|
||||
const res = await fetch(makeUrl(`/api/apps/${appName}/crashes/${filename}`))
|
||||
if (!res.ok) return null
|
||||
return await res.text()
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
export { crashesApp } from './crashes'
|
||||
export { logApp } from './logs'
|
||||
export {
|
||||
configShow,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import color from 'kleur'
|
|||
import {
|
||||
cleanApp,
|
||||
configShow,
|
||||
crashesApp,
|
||||
diffApp,
|
||||
getApp,
|
||||
infoApp,
|
||||
|
|
@ -106,6 +107,12 @@ program
|
|||
.option('-g, --grep <pattern>', 'filter logs by pattern')
|
||||
.action(logApp)
|
||||
|
||||
program
|
||||
.command('crashes')
|
||||
.description('Show crash history for an app')
|
||||
.argument('[name]', 'app name (uses current directory if omitted)')
|
||||
.action(crashesApp)
|
||||
|
||||
program
|
||||
.command('open')
|
||||
.description('Open an app in browser')
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { APPS_DIR, allApps, getLogDates, onChange, readLogs, registerApp, renameApp, startApp, stopApp, updateAppIcon } from '$apps'
|
||||
import { APPS_DIR, allApps, getCrashFiles, getLogDates, onChange, readCrashReport, readLogs, registerApp, renameApp, startApp, stopApp, updateAppIcon } from '$apps'
|
||||
import type { App as BackendApp } from '$apps'
|
||||
import type { App as SharedApp } from '@types'
|
||||
import { generateTemplates, type TemplateType } from '%templates'
|
||||
|
|
@ -86,6 +86,31 @@ router.get('/:app/logs/dates', c => {
|
|||
return c.json(getLogDates(appName))
|
||||
})
|
||||
|
||||
router.get('/:app/crashes', c => {
|
||||
const appName = c.req.param('app')
|
||||
if (!appName) return c.json({ error: 'App not found' }, 404)
|
||||
|
||||
const app = allApps().find(a => a.name === appName)
|
||||
if (!app) return c.json({ error: 'App not found' }, 404)
|
||||
|
||||
const files = getCrashFiles(appName)
|
||||
return c.json(files)
|
||||
})
|
||||
|
||||
router.get('/:app/crashes/:filename', c => {
|
||||
const appName = c.req.param('app')
|
||||
const filename = c.req.param('filename')
|
||||
if (!appName || !filename) return c.json({ error: 'App not found' }, 404)
|
||||
|
||||
const app = allApps().find(a => a.name === appName)
|
||||
if (!app) return c.json({ error: 'App not found' }, 404)
|
||||
|
||||
const content = readCrashReport(appName, filename)
|
||||
if (content === null) return c.json({ error: 'Crash report not found' }, 404)
|
||||
|
||||
return c.text(content)
|
||||
})
|
||||
|
||||
router.post('/', async c => {
|
||||
let body: { name?: string, template?: TemplateType, tool?: boolean }
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -55,6 +55,16 @@ export const runApps = () =>
|
|||
export const runningApps = (): App[] =>
|
||||
allApps().filter(a => a.state === 'running')
|
||||
|
||||
export function getCrashFiles(appName: string): string[] {
|
||||
const dir = crashDir(appName)
|
||||
if (!existsSync(dir)) return []
|
||||
|
||||
return readdirSync(dir)
|
||||
.filter(f => f.endsWith('.txt'))
|
||||
.sort()
|
||||
.reverse()
|
||||
}
|
||||
|
||||
export function getLogDates(appName: string): string[] {
|
||||
const dir = logDir(appName)
|
||||
if (!existsSync(dir)) return []
|
||||
|
|
@ -66,6 +76,12 @@ export function getLogDates(appName: string): string[] {
|
|||
.reverse()
|
||||
}
|
||||
|
||||
export function readCrashReport(appName: string, filename: string): string | null {
|
||||
const file = join(crashDir(appName), filename)
|
||||
if (!existsSync(file)) return null
|
||||
return readFileSync(file, 'utf-8')
|
||||
}
|
||||
|
||||
export function readLogs(appName: string, date?: string, tail?: number): string[] {
|
||||
const file = logFile(appName, date ?? formatLogDate())
|
||||
if (!existsSync(file)) return []
|
||||
|
|
@ -270,15 +286,34 @@ const info = (app: App, ...msg: string[]) => {
|
|||
app.logs?.push({ time: Date.now(), text: msg.join(' ') })
|
||||
}
|
||||
|
||||
const crashDir = (appName: string) =>
|
||||
join(APPS_DIR, appName, 'crashes')
|
||||
|
||||
const formatCrashTimestamp = (date: Date = new Date()) => {
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`
|
||||
}
|
||||
|
||||
const formatUptime = (ms: number): string => {
|
||||
const seconds = Math.floor(ms / 1000) % 60
|
||||
const minutes = Math.floor(ms / 60000) % 60
|
||||
const hours = Math.floor(ms / 3600000)
|
||||
const parts: string[] = []
|
||||
if (hours > 0) parts.push(`${hours}h`)
|
||||
if (minutes > 0) parts.push(`${minutes}m`)
|
||||
parts.push(`${seconds}s`)
|
||||
return parts.join(' ')
|
||||
}
|
||||
|
||||
const isApp = (dir: string): boolean =>
|
||||
!loadApp(dir).error
|
||||
|
||||
const logDir = (appName: string) =>
|
||||
join(APPS_DIR, appName, 'logs')
|
||||
|
||||
const logFile = (appName: string, date: string = formatLogDate()) =>
|
||||
join(logDir(appName), `${date}.log`)
|
||||
|
||||
const isApp = (dir: string): boolean =>
|
||||
!loadApp(dir).error
|
||||
|
||||
const update = () => {
|
||||
setApps(allApps())
|
||||
_listeners.forEach(cb => cb())
|
||||
|
|
@ -302,6 +337,14 @@ function discoverApps() {
|
|||
update()
|
||||
}
|
||||
|
||||
function ensureCrashDir(appName: string): string {
|
||||
const dir = crashDir(appName)
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true })
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
function ensureLogDir(appName: string): string {
|
||||
const dir = logDir(appName)
|
||||
if (!existsSync(dir)) {
|
||||
|
|
@ -471,6 +514,36 @@ function rotateLogs() {
|
|||
}
|
||||
}
|
||||
|
||||
function writeCrashReport(app: App, exitCode: number | null, signal: string | null) {
|
||||
const now = new Date()
|
||||
const uptime = app.started ? Date.now() - app.started : 0
|
||||
|
||||
// Get last 20 log lines
|
||||
const logs = app.logs ?? []
|
||||
const lastLogs = logs.slice(-20)
|
||||
|
||||
const lines: string[] = [
|
||||
`App: ${app.name}`,
|
||||
`Time: ${now.toISOString()}`,
|
||||
`Exit Code: ${exitCode}`,
|
||||
`Signal: ${signal}`,
|
||||
`Uptime: ${formatUptime(uptime)}`,
|
||||
`Restart Attempt: ${app.restartAttempts ?? 0}`,
|
||||
'',
|
||||
'--- Last 20 log lines ---',
|
||||
]
|
||||
|
||||
for (const log of lastLogs) {
|
||||
const timestamp = new Date(log.time).toISOString()
|
||||
lines.push(`[${timestamp}] ${log.text}`)
|
||||
}
|
||||
|
||||
ensureCrashDir(app.name)
|
||||
const filename = `${formatCrashTimestamp(now)}.txt`
|
||||
const filepath = join(crashDir(app.name), filename)
|
||||
writeFileSync(filepath, lines.join('\n') + '\n')
|
||||
}
|
||||
|
||||
function writeLogLine(appName: string, streamType: 'stdout' | 'stderr' | 'system', text: string) {
|
||||
ensureLogDir(appName)
|
||||
const timestamp = new Date().toISOString()
|
||||
|
|
@ -565,6 +638,11 @@ async function runApp(dir: string, port: number) {
|
|||
const msg = `Exited with code ${code}`
|
||||
app.logs?.push({ time: Date.now(), text: msg })
|
||||
writeLogLine(dir, 'system', msg)
|
||||
|
||||
// Write crash report only for actual crashes (non-zero exit, not manually stopped)
|
||||
if (!app.manuallyStopped) {
|
||||
writeCrashReport(app, code, null)
|
||||
}
|
||||
} else {
|
||||
app.logs?.push({ time: Date.now(), text: 'Stopped' })
|
||||
writeLogLine(dir, 'system', 'Stopped')
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user