From 8d78e76a3ccd6317461e7521fd13f5b007b0fa2b Mon Sep 17 00:00:00 2001 From: Ossama Jouini Date: Wed, 28 Jan 2026 10:01:15 +0100 Subject: [PATCH 1/3] fix: properly cleanup resources on macOS quit to prevent hanging --- assets/electron/template/app/src/index.js | 103 ++++++++++++++++++++-- 1 file changed, 98 insertions(+), 5 deletions(-) diff --git a/assets/electron/template/app/src/index.js b/assets/electron/template/app/src/index.js index fa164ae..0bb36e9 100644 --- a/assets/electron/template/app/src/index.js +++ b/assets/electron/template/app/src/index.js @@ -286,6 +286,21 @@ app.setPath('userData', sessionDataPath) */ const clients = new Set() +/** + * @type {import('http').Server | null} + */ +let httpServer = null + +/** + * @type {import('ws').WebSocketServer | null} + */ +let wss = null + +/** + * @type {boolean} + */ +let isQuitting = false + /** * @param {string} message */ @@ -307,7 +322,8 @@ const dir = app.isPackaged ? join(metaDirname, './app') : './src/app' const createAppServer = (mainWindow, serveStatic = true) => { // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { - const server = createServer() + httpServer = createServer() + const server = httpServer if (serveStatic) { server.on('request', (req, res) => { @@ -347,7 +363,7 @@ const createAppServer = (mainWindow, serveStatic = true) => { } try { - const wss = new WebSocketServer({ server }) + wss = new WebSocketServer({ server }) wss.on('connection', function connection(ws) { clients.add(ws) @@ -657,9 +673,53 @@ const createWindow = async () => { return mainWindow } +/** + * Cleanup all resources before quitting + */ +const cleanup = () => { + return new Promise((resolve) => { + console.log('Cleaning up resources...') + + // Close all WebSocket clients + for (const client of clients) { + try { + client.close() + } catch (e) { + console.error('Error closing WebSocket client:', e) + } + } + clients.clear() + + // Close WebSocket server + if (wss) { + try { + wss.close() + } catch (e) { + console.error('Error closing WebSocket server:', e) + } + wss = null + } + + // Close HTTP server + if (httpServer) { + httpServer.close(() => { + console.log('HTTP server closed') + httpServer = null + resolve() + }) + // Force resolve after timeout in case server doesn't close cleanly + setTimeout(resolve, 500) + } else { + resolve() + } + }) +} + const registerHandlers = async () => { - ipcMain.on('exit', (event, code) => { + ipcMain.on('exit', async (event, code) => { console.log('exit', code) + isQuitting = true + await cleanup() app.exit(code) }) } @@ -708,6 +768,39 @@ app.whenReady().then(async () => { }) }) -app.on('window-all-closed', async () => { - app.quit() +app.on('before-quit', (event) => { + if (!isQuitting) { + event.preventDefault() + isQuitting = true + cleanup().then(() => { + app.quit() + }) + } +}) + +app.on('will-quit', () => { + // Final cleanup - synchronous operations only + // Close any remaining WebSocket clients + for (const client of clients) { + try { + client.terminate() // Force close + } catch (e) { + // Ignore errors during final cleanup + } + } + clients.clear() +}) + +app.on('window-all-closed', () => { + // On macOS, apps typically stay open until explicitly quit + // But for games, we usually want to quit when the window is closed + if (process.platform !== 'darwin' || isQuitting) { + app.quit() + } else { + // On macOS, trigger the quit process which will run cleanup + isQuitting = true + cleanup().then(() => { + app.quit() + }) + } }) From b28979e81f6f72dec51ead8416b03c6bd5d62945 Mon Sep 17 00:00:00 2001 From: Ossama Jouini Date: Fri, 30 Jan 2026 00:02:05 +0100 Subject: [PATCH 2/3] added save screenshot handler --- .../app/src/handlers/steam/saveScreenshot.js | 65 +++++++++++++++++++ assets/electron/template/app/src/index.js | 4 ++ 2 files changed, 69 insertions(+) create mode 100644 assets/electron/template/app/src/handlers/steam/saveScreenshot.js diff --git a/assets/electron/template/app/src/handlers/steam/saveScreenshot.js b/assets/electron/template/app/src/handlers/steam/saveScreenshot.js new file mode 100644 index 0000000..8858101 --- /dev/null +++ b/assets/electron/template/app/src/handlers/steam/saveScreenshot.js @@ -0,0 +1,65 @@ +import { handleSteamRequest } from './utils.js' +import { writeFileSync, unlinkSync, mkdirSync } from 'node:fs' +import { join } from 'node:path' +import { tmpdir } from 'node:os' +import { randomUUID } from 'node:crypto' + +/** + * Save screenshot from base64 data URL to Steam library + * @param {Omit} client + * @param {Object} json + * @returns {Promise} The screenshot handle + */ +const saveScreenshotHandler = async (client, json) => { + const { body } = json + const { dataUrl, width, height } = body + + // Validate input + if (!dataUrl || typeof dataUrl !== 'string') { + throw new Error('dataUrl is required and must be a string') + } + + if (!dataUrl.startsWith('data:image/')) { + throw new Error('dataUrl must be a base64 data URL (data:image/...)') + } + + // Extract base64 data + const matches = dataUrl.match(/^data:image\/(\w+);base64,(.+)$/) + if (!matches) { + throw new Error('Invalid base64 data URL format') + } + + const [, format, base64Data] = matches + const buffer = Buffer.from(base64Data, 'base64') + + // Create temp file + const tempDir = join(tmpdir(), 'pipelab-screenshots') + mkdirSync(tempDir, { recursive: true }) + const tempPath = join(tempDir, `screenshot-${randomUUID()}.${format}`) + + try { + // Write to temp file + writeFileSync(tempPath, buffer) + + // Add to Steam library + const handle = client.screenshots.addScreenshotToLibrary(tempPath, null, width, height) + + return handle + } finally { + // Clean up temp file + try { + unlinkSync(tempPath) + } catch { + // Ignore cleanup errors + } + } +} + +/** + * @param {Object} json + * @param {import('ws').WebSocket} ws + * @param {Omit} client + */ +export default async (json, ws, client) => { + await handleSteamRequest(client, json, ws, saveScreenshotHandler) +} diff --git a/assets/electron/template/app/src/index.js b/assets/electron/template/app/src/index.js index 0bb36e9..e27f970 100644 --- a/assets/electron/template/app/src/index.js +++ b/assets/electron/template/app/src/index.js @@ -72,6 +72,7 @@ import steamInstallInfo from './handlers/steam/installInfo.js' import steamDownloadInfo from './handlers/steam/downloadInfo.js' import steamDownload from './handlers/steam/download.js' import steamDeleteItem from './handlers/steam/deleteItem.js' +import steamSaveScreenshot from './handlers/steam/saveScreenshot.js' // discord set activity import discordSetActivity from './handlers/discord/set-activity.js' @@ -528,6 +529,9 @@ const createAppServer = (mainWindow, serveStatic = true) => { case '/steam/workshop/delete-item': await steamDeleteItem(json, ws, client) break + case '/steam/screenshots/save': + await steamSaveScreenshot(json, ws, client) + break case '/discord/set-activity': await discordSetActivity(json, ws, mainWindow, rpc) break From 3965c0126a5e38b8d82693160c4b51318a41f266 Mon Sep 17 00:00:00 2001 From: Ossama Jouini Date: Fri, 30 Jan 2026 02:33:35 +0100 Subject: [PATCH 3/3] fixed screenshot code, there needs to be a little delay before deleting the file otherwise steam fails to copy it --- .../app/src/handlers/steam/saveScreenshot.js | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/assets/electron/template/app/src/handlers/steam/saveScreenshot.js b/assets/electron/template/app/src/handlers/steam/saveScreenshot.js index 8858101..193a9d7 100644 --- a/assets/electron/template/app/src/handlers/steam/saveScreenshot.js +++ b/assets/electron/template/app/src/handlers/steam/saveScreenshot.js @@ -1,5 +1,5 @@ import { handleSteamRequest } from './utils.js' -import { writeFileSync, unlinkSync, mkdirSync } from 'node:fs' +import { writeFileSync, unlinkSync, mkdirSync, rmdirSync } from 'node:fs' import { join } from 'node:path' import { tmpdir } from 'node:os' import { randomUUID } from 'node:crypto' @@ -37,22 +37,23 @@ const saveScreenshotHandler = async (client, json) => { mkdirSync(tempDir, { recursive: true }) const tempPath = join(tempDir, `screenshot-${randomUUID()}.${format}`) - try { - // Write to temp file - writeFileSync(tempPath, buffer) + // Write to temp file + writeFileSync(tempPath, buffer) - // Add to Steam library - const handle = client.screenshots.addScreenshotToLibrary(tempPath, null, width, height) + // Add to Steam library + const handle = client.screenshots.addScreenshotToLibrary(tempPath, null, width, height) - return handle - } finally { - // Clean up temp file + // Delete temp file after a delay to allow Steam to process it + setTimeout(() => { try { unlinkSync(tempPath) + rmdirSync(tempDir) } catch { // Ignore cleanup errors } - } + }, 5000) + + return handle } /**