Skip to content

Commit 5b12224

Browse files
committed
add godot process logs to reports and fix success logic
- Capture stdout/stderr from Godot process and include in reports - Add godotLogs field to ProcessReport type - Fix success logic: treat "No assets completed successfully" as success when there are 0 assets to process (failed: 0 should mean success: true) - Return report from processScene/processWearableOrEmote instead of void - Store report in main godotOptimizer after collecting Godot logs - Increase log buffer limit to 5000 lines
1 parent a560353 commit 5b12224

File tree

2 files changed

+155
-22
lines changed

2 files changed

+155
-22
lines changed

src/adapters/asset-server.ts

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export interface IAssetServerComponent {
1414
getBatchStatus(batchId: string): Promise<BatchStatus>
1515
waitForCompletion(batchId: string, timeoutMs?: number): Promise<BatchStatus>
1616
restartGodot(): Promise<boolean>
17+
getGodotLogs(): string[]
18+
clearGodotLogs(): void
1719
}
1820

1921
export type ProcessSceneParams = {
@@ -87,6 +89,28 @@ export function createAssetServerComponent(
8789
const { baseUrl } = config
8890
const logger = logs.getLogger('asset-server')
8991

92+
// Buffer to store Godot process output
93+
let godotLogs: string[] = []
94+
const MAX_LOG_LINES = 5000 // Limit to prevent memory issues
95+
96+
function addGodotLog(line: string) {
97+
if (line.trim()) {
98+
godotLogs.push(line.trim())
99+
// Keep only the last MAX_LOG_LINES
100+
if (godotLogs.length > MAX_LOG_LINES) {
101+
godotLogs = godotLogs.slice(-MAX_LOG_LINES)
102+
}
103+
}
104+
}
105+
106+
function getGodotLogs(): string[] {
107+
return [...godotLogs]
108+
}
109+
110+
function clearGodotLogs(): void {
111+
godotLogs = []
112+
}
113+
90114
async function isReady(): Promise<boolean> {
91115
try {
92116
const response = await fetch.fetch(`${baseUrl}/health`)
@@ -209,10 +233,39 @@ export function createAssetServerComponent(
209233
// Wait for process to fully terminate
210234
await new Promise((resolve) => setTimeout(resolve, 2000))
211235

236+
// Clear logs from previous run
237+
clearGodotLogs()
238+
addGodotLog(`[${new Date().toISOString()}] Starting Godot asset-server...`)
239+
212240
// Start new Godot process in background
213241
const godotCmd = `/app/decentraland.godot.client.x86_64 --headless --asset-server --asset-server-port ${port}`
214242
const child = exec(godotCmd)
215243

244+
// Capture stdout
245+
if (child.stdout) {
246+
child.stdout.on('data', (data: Buffer | string) => {
247+
const lines = data.toString().split('\n')
248+
for (const line of lines) {
249+
addGodotLog(line)
250+
}
251+
})
252+
}
253+
254+
// Capture stderr
255+
if (child.stderr) {
256+
child.stderr.on('data', (data: Buffer | string) => {
257+
const lines = data.toString().split('\n')
258+
for (const line of lines) {
259+
addGodotLog(`[stderr] ${line}`)
260+
}
261+
})
262+
}
263+
264+
// Log when process exits
265+
child.on('exit', (code, signal) => {
266+
addGodotLog(`[${new Date().toISOString()}] Godot process exited with code ${code}, signal ${signal}`)
267+
})
268+
216269
// Detach from child process - we don't need to track it
217270
child.unref()
218271

@@ -221,14 +274,18 @@ export function createAssetServerComponent(
221274
await new Promise((resolve) => setTimeout(resolve, 1000))
222275
if (await isReady()) {
223276
logger.info('Godot asset-server is ready after restart')
277+
addGodotLog(`[${new Date().toISOString()}] Godot asset-server is ready`)
224278
return true
225279
}
226280
}
227281

228282
logger.error('Godot asset-server failed to become ready after restart')
283+
addGodotLog(`[${new Date().toISOString()}] ERROR: Godot asset-server failed to become ready`)
229284
return false
230285
} catch (error) {
231-
logger.error('Failed to restart Godot', { error: error instanceof Error ? error.message : String(error) })
286+
const errorMsg = error instanceof Error ? error.message : String(error)
287+
logger.error('Failed to restart Godot', { error: errorMsg })
288+
addGodotLog(`[${new Date().toISOString()}] ERROR: Failed to restart Godot: ${errorMsg}`)
232289
return false
233290
}
234291
}
@@ -239,6 +296,8 @@ export function createAssetServerComponent(
239296
processAssets,
240297
getBatchStatus,
241298
waitForCompletion,
242-
restartGodot
299+
restartGodot,
300+
getGodotLogs,
301+
clearGodotLogs
243302
}
244303
}

src/runners/godot-optimizer/index.ts

Lines changed: 94 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type ProcessReport = {
2828
startedAt: Date
2929
finishedAt: Date | null
3030
errors: string[]
31+
godotLogs: string[]
3132
individualAssets: {
3233
total: number
3334
successful: number
@@ -54,7 +55,7 @@ export async function godotOptimizer(
5455
_msg: TaskQueueMessage,
5556
components: Pick<AppComponents, 'logs' | 'config' | 'storage' | 'assetServer' | 'fetch'>
5657
): Promise<void> {
57-
const { logs, assetServer } = components
58+
const { logs, assetServer, storage } = components
5859
const logger = logs.getLogger('godot-optimizer')
5960

6061
// Cast to extended type to access entityType
@@ -66,20 +67,36 @@ export async function godotOptimizer(
6667
entityType
6768
})
6869

70+
const tempDir = path.join(process.cwd(), 'temp')
71+
let report: ProcessReport | null = null
72+
6973
try {
7074
switch (entityType) {
7175
case 'scene':
72-
await processScene(entity, components)
76+
report = await processScene(entity, components)
7377
break
7478
case 'wearable':
7579
case 'emote':
76-
await processWearableOrEmote(entity, entityType, components)
80+
report = await processWearableOrEmote(entity, entityType, components)
7781
break
7882
default:
7983
logger.warn('Unknown entity type, defaulting to scene processing', { entityType })
80-
await processScene(entity, components)
84+
report = await processScene(entity, components)
8185
}
8286
} finally {
87+
// Get Godot logs before restart (they will be cleared on restart)
88+
const godotProcessLogs = assetServer.getGodotLogs()
89+
if (godotProcessLogs.length > 0) {
90+
logger.info('Captured Godot process logs', { lineCount: godotProcessLogs.length })
91+
}
92+
93+
// Add Godot process logs to report
94+
if (report) {
95+
report.godotLogs = [...report.godotLogs, ...godotProcessLogs]
96+
// Store the report with Godot logs included
97+
await storeReport(report, tempDir, storage, logger)
98+
}
99+
83100
// Restart Godot after each entity to free memory
84101
logger.info('Processing complete, restarting Godot to free memory', {
85102
entityId: entity.entity.entityId
@@ -94,7 +111,7 @@ export async function godotOptimizer(
94111
async function processScene(
95112
entity: DeploymentToSqs,
96113
components: Pick<AppComponents, 'logs' | 'config' | 'storage' | 'assetServer' | 'fetch'>
97-
): Promise<void> {
114+
): Promise<ProcessReport> {
98115
const { logs, storage, assetServer, config } = components
99116
const logger = logs.getLogger('godot-optimizer:scene')
100117

@@ -113,6 +130,7 @@ async function processScene(
113130
startedAt: new Date(),
114131
finishedAt: null,
115132
errors: [],
133+
godotLogs: [],
116134
individualAssets: { total: 0, successful: 0, failed: 0 },
117135
result: null
118136
}
@@ -156,7 +174,7 @@ async function processScene(
156174
optimizedAssets: 0,
157175
individualZips: []
158176
}
159-
return
177+
return report
160178
}
161179
throw error
162180
}
@@ -170,7 +188,28 @@ async function processScene(
170188
// 3. Wait for metadata processing to complete
171189
const metadataResult = await assetServer.waitForCompletion(metadataResponse.batch_id, timeoutMs)
172190

191+
// Collect job logs from metadata result
192+
for (const job of metadataResult.jobs || []) {
193+
if (job.error) {
194+
report.godotLogs.push(`[${job.hash}] ${job.status}: ${job.error}`)
195+
} else {
196+
report.godotLogs.push(`[${job.hash}] ${job.status} (${job.elapsed_secs}s)`)
197+
}
198+
}
199+
173200
if (metadataResult.status === 'failed') {
201+
// Check if this is "No assets completed successfully" - treat as success with 0 assets
202+
const errorMsg = metadataResult.error || ''
203+
if (errorMsg.includes('No assets completed successfully') || errorMsg.includes('No processable assets')) {
204+
logger.info('Scene has no processable assets (from metadata result)', { entityId })
205+
report.finishedAt = new Date()
206+
report.result = {
207+
success: true,
208+
optimizedAssets: 0,
209+
individualZips: []
210+
}
211+
return report
212+
}
174213
throw new Error(metadataResult.error || 'Metadata processing failed')
175214
}
176215

@@ -202,7 +241,7 @@ async function processScene(
202241
optimizedAssets: 0,
203242
individualZips: []
204243
}
205-
return
244+
return report
206245
}
207246

208247
// Parse error is a real failure
@@ -236,7 +275,7 @@ async function processScene(
236275
metadataZipPath: metadataS3Key,
237276
individualZips: [metadataS3Key]
238277
}
239-
return
278+
return report
240279
}
241280

242281
logger.info('Found assets to bundle individually', {
@@ -279,23 +318,34 @@ async function processScene(
279318

280319
const result = await assetServer.waitForCompletion(response.batch_id, timeoutMs)
281320

321+
// Collect job logs
322+
for (const job of result.jobs || []) {
323+
if (job.error) {
324+
report.godotLogs.push(`[${assetHash}] ${job.status}: ${job.error}`)
325+
}
326+
}
327+
282328
if (result.status === 'completed' && result.zip_path) {
283329
const s3Key = `${assetHash}-mobile.zip`
284330
await storage.storeFile(s3Key, result.zip_path)
285331
// Clean up temp file from asset-server
286332
await fs.rm(result.zip_path, { force: true }).catch(() => {})
287333
report.individualAssets.successful++
288334
individualZips.push(s3Key)
335+
report.godotLogs.push(`[${assetHash}] completed successfully`)
289336
return { success: true, hash: assetHash, s3Key }
290337
} else {
291338
report.individualAssets.failed++
292-
report.errors.push(`Asset ${assetHash} failed: ${result.error || 'Unknown error'}`)
339+
const errorMsg = result.error || 'Unknown error'
340+
report.errors.push(`Asset ${assetHash} failed: ${errorMsg}`)
341+
report.godotLogs.push(`[${assetHash}] failed: ${errorMsg}`)
293342
return { success: false, hash: assetHash, error: result.error }
294343
}
295344
} catch (error) {
296345
report.individualAssets.failed++
297346
const errorMsg = error instanceof Error ? error.message : String(error)
298347
report.errors.push(`Asset ${assetHash} failed: ${errorMsg}`)
348+
report.godotLogs.push(`[${assetHash}] exception: ${errorMsg}`)
299349
return { success: false, hash: assetHash, error: errorMsg }
300350
}
301351
})
@@ -328,16 +378,14 @@ async function processScene(
328378
}
329379

330380
report.finishedAt = new Date()
381+
return report
331382
} catch (error) {
332383
logger.error(`Error processing scene ${entityId}`)
333384
logger.error(error as any)
334385
report.errors.push(error instanceof Error ? error.message : String(error))
335386
report.finishedAt = new Date()
336387
report.result = { success: false }
337-
338-
throw error
339-
} finally {
340-
await storeReport(report, tempDir, storage, logger)
388+
return report
341389
}
342390
}
343391

@@ -348,7 +396,7 @@ async function processWearableOrEmote(
348396
entity: DeploymentToSqs,
349397
entityType: 'wearable' | 'emote',
350398
components: Pick<AppComponents, 'logs' | 'config' | 'storage' | 'assetServer' | 'fetch'>
351-
): Promise<void> {
399+
): Promise<ProcessReport> {
352400
const { logs, storage, assetServer, config, fetch } = components
353401
const logger = logs.getLogger(`godot-optimizer:${entityType}`)
354402

@@ -371,6 +419,7 @@ async function processWearableOrEmote(
371419
startedAt: new Date(),
372420
finishedAt: null,
373421
errors: [],
422+
godotLogs: [],
374423
individualAssets: { total: 1, successful: 0, failed: 0 },
375424
result: null
376425
}
@@ -422,7 +471,7 @@ async function processWearableOrEmote(
422471
optimizedAssets: 0,
423472
individualZips: []
424473
}
425-
return
474+
return report
426475
}
427476

428477
// Use first GLTF (for standard mode, we process the whole entity)
@@ -470,6 +519,15 @@ async function processWearableOrEmote(
470519
// 4. Wait for completion
471520
const result = await assetServer.waitForCompletion(response.batch_id, timeoutMs)
472521

522+
// Collect job logs
523+
for (const job of result.jobs || []) {
524+
if (job.error) {
525+
report.godotLogs.push(`[${job.hash}] ${job.status}: ${job.error}`)
526+
} else {
527+
report.godotLogs.push(`[${job.hash}] ${job.status} (${job.elapsed_secs}s)`)
528+
}
529+
}
530+
473531
if (result.status === 'failed') {
474532
// Log detailed job status for debugging
475533
const jobErrors = result.jobs
@@ -482,6 +540,21 @@ async function processWearableOrEmote(
482540
error: result.error || 'unknown',
483541
jobErrors: jobErrors || 'none'
484542
})
543+
544+
// Check if this is "No assets completed successfully" - treat as success with 0 assets
545+
const errorMsg = result.error || ''
546+
if (errorMsg.includes('No assets completed successfully') || errorMsg.includes('No processable assets')) {
547+
logger.info(`${entityType} has no processable assets`, { entityId, gltfHash })
548+
report.finishedAt = new Date()
549+
report.individualAssets.total = 0
550+
report.result = {
551+
success: true,
552+
optimizedAssets: 0,
553+
individualZips: []
554+
}
555+
return report
556+
}
557+
485558
throw new Error(result.error || 'Processing failed')
486559
}
487560

@@ -497,6 +570,7 @@ async function processWearableOrEmote(
497570
logger.info('Uploaded ZIP', { gltfHash, entityType, s3Key })
498571

499572
report.individualAssets.successful = 1
573+
report.godotLogs.push(`[${gltfHash}] completed successfully`)
500574
report.result = {
501575
success: true,
502576
batchId: response.batch_id,
@@ -505,17 +579,17 @@ async function processWearableOrEmote(
505579
}
506580

507581
report.finishedAt = new Date()
582+
return report
508583
} catch (error) {
509584
logger.error(`Error processing ${entityType} ${entityId}`)
510585
logger.error(error as any)
511-
report.errors.push(error instanceof Error ? error.message : String(error))
586+
const errorMsg = error instanceof Error ? error.message : String(error)
587+
report.errors.push(errorMsg)
588+
report.godotLogs.push(`[${entityId}] exception: ${errorMsg}`)
512589
report.finishedAt = new Date()
513590
report.result = { success: false }
514591
report.individualAssets.failed = 1
515-
516-
throw error
517-
} finally {
518-
await storeReport(report, tempDir, storage, logger)
592+
return report
519593
}
520594
}
521595

0 commit comments

Comments
 (0)