From d30005acbd6e0474a97d61aac4203b75a7e7f2a8 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 21 Feb 2026 04:33:43 +0100 Subject: [PATCH] fix: implement atomic writes and hardened VDF provisioning for robust Steam integration on fresh installs --- frontend/electron/services/steam-client.ts | 132 +++++++++++++-------- 1 file changed, 82 insertions(+), 50 deletions(-) diff --git a/frontend/electron/services/steam-client.ts b/frontend/electron/services/steam-client.ts index cfdc94e..79487a7 100644 --- a/frontend/electron/services/steam-client.ts +++ b/frontend/electron/services/steam-client.ts @@ -26,14 +26,16 @@ class SteamClientService { if (platform === 'win32') { const possiblePaths = [ 'C:\\Program Files (x86)\\Steam', - 'C:\\Program Files\\Steam' + 'C:\\Program Files\\Steam', + path.join(process.env.APPDATA || '', 'Steam'), ]; this.steamPath = possiblePaths.find(p => fs.existsSync(p)) || null; } else if (platform === 'linux') { const possiblePaths = [ path.join(home, '.steam/steam'), path.join(home, '.local/share/Steam'), - path.join(home, '.var/app/com.valvesoftware.Steam/.steam/steam') + path.join(home, '.var/app/com.valvesoftware.Steam/.steam/steam'), // Flatpak + path.join(home, 'snap/steam/common/.steam/steam'), // Snap ]; this.steamPath = possiblePaths.find(p => fs.existsSync(p)) || null; } @@ -53,13 +55,36 @@ class SteamClientService { return path.join(this.steamPath, 'config', 'config.vdf'); } + /** + * Safe Atomic Write: Writes to a temp file and renames it. + * This prevents file corruption if the app crashes during write. + */ + private safeWriteVdf(filePath: string, data: any) { + const tempPath = `${filePath}.tmp_${Date.now()}`; + const dir = path.dirname(filePath); + + try { + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + const vdfContent = stringify(data); + fs.writeFileSync(tempPath, vdfContent, 'utf-8'); + + // Atomic rename + fs.renameSync(tempPath, filePath); + } catch (e: any) { + console.error(`[SteamClient] Atomic write failed for ${filePath}: ${e.message}`); + if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath); + throw e; + } + } + public startWatching(callback: (accounts: LocalSteamAccount[]) => void) { this.onAccountsChanged = callback; const loginUsersPath = this.getLoginUsersPath(); if (loginUsersPath && fs.existsSync(loginUsersPath)) { this.readLocalAccounts(); - chokidar.watch(loginUsersPath, { persistent: true }).on('change', () => { + chokidar.watch(loginUsersPath, { persistent: true, ignoreInitial: true }).on('change', () => { + console.log(`[SteamClient] loginusers.vdf changed, re-scanning...`); this.readLocalAccounts(); }); } @@ -71,16 +96,20 @@ class SteamClientService { try { const content = fs.readFileSync(filePath, 'utf-8'); + if (!content.trim()) return; // Empty file + const data = parse(content) as any; if (!data || !data.users) return; const accounts: LocalSteamAccount[] = []; for (const [steamId64, userData] of Object.entries(data.users)) { const user = userData as any; + if (!user || !user.AccountName) continue; + accounts.push({ steamId: steamId64, accountName: user.AccountName, - personaName: user.PersonaName, + personaName: user.PersonaName || user.AccountName, timestamp: parseInt(user.Timestamp) || 0 }); } @@ -98,35 +127,39 @@ class SteamClientService { try { const content = fs.readFileSync(configPath, 'utf-8'); const data = parse(content) as any; - const accounts = data?.InstallConfigStore?.Software?.Valve?.Steam?.Accounts; - if (accounts && accounts[accountName]) { - return accounts[accountName]; - } + return (accounts && accounts[accountName]) ? accounts[accountName] : null; } catch (e) { console.error('[SteamClient] Failed to extract config.vdf data'); + return null; } - return null; } public injectAccountConfig(accountName: string, accountData: any) { const configPath = this.getConfigVdfPath(); if (!configPath) return; - // Create directory if it doesn't exist - const configDir = path.dirname(configPath); - if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true }); - - let data: any = { InstallConfigStore: { Software: { Valve: { Steam: { Accounts: {} } } } } }; + let data: any = { + InstallConfigStore: { + Software: { + Valve: { + Steam: { + Accounts: {} + } + } + } + } + }; if (fs.existsSync(configPath)) { try { const content = fs.readFileSync(configPath, 'utf-8'); - data = parse(content) as any; + const parsed = parse(content) as any; + if (parsed && typeof parsed === 'object') data = parsed; } catch (e) { } } - // Ensure structure exists + // Ensure safe nesting if (!data.InstallConfigStore) data.InstallConfigStore = {}; if (!data.InstallConfigStore.Software) data.InstallConfigStore.Software = {}; if (!data.InstallConfigStore.Software.Valve) data.InstallConfigStore.Software.Valve = {}; @@ -136,11 +169,9 @@ class SteamClientService { data.InstallConfigStore.Software.Valve.Steam.Accounts[accountName] = accountData; try { - fs.writeFileSync(configPath, stringify(data)); - console.log(`[SteamClient] Injected login config for ${accountName} into config.vdf`); - } catch (e) { - console.error('[SteamClient] Failed to write config.vdf'); - } + this.safeWriteVdf(configPath, data); + console.log(`[SteamClient] Safely injected session for ${accountName}`); + } catch (e) { } } public async setAutoLoginUser(accountName: string, accountConfig?: any, steamId?: string): Promise { @@ -148,14 +179,12 @@ class SteamClientService { const loginUsersPath = this.getLoginUsersPath(); if (loginUsersPath) { - const configDir = path.dirname(loginUsersPath); - if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true }); - let data: any = { users: {} }; if (fs.existsSync(loginUsersPath)) { try { const content = fs.readFileSync(loginUsersPath, 'utf-8'); - data = parse(content) as any; + const parsed = parse(content) as any; + if (parsed && parsed.users) data = parsed; } catch (e) { } } @@ -164,7 +193,7 @@ class SteamClientService { let found = false; for (const [id, user] of Object.entries(data.users)) { const u = user as any; - if (u.AccountName.toLowerCase() === accountName.toLowerCase()) { + if (u.AccountName?.toLowerCase() === accountName.toLowerCase()) { u.mostrecent = "1"; u.RememberPassword = "1"; u.AllowAutoLogin = "1"; @@ -177,8 +206,8 @@ class SteamClientService { } } - if (!found && steamId) { - console.log(`[SteamClient] Provisioning user ${accountName} into loginusers.vdf`); + if (!found && steamId && accountName) { + console.log(`[SteamClient] Provisioning new user profile for ${accountName}`); data.users[steamId] = { AccountName: accountName, PersonaName: accountName, @@ -193,16 +222,15 @@ class SteamClientService { } try { - fs.writeFileSync(loginUsersPath, stringify(data)); - } catch (e) { - console.error('[SteamClient] Failed to write loginusers.vdf'); - } + this.safeWriteVdf(loginUsersPath, data); + } catch (e) { } } - if (accountConfig) { + if (accountConfig && accountName) { this.injectAccountConfig(accountName, accountConfig); } + // --- Linux Registry / Registry.vdf Hardening --- if (platform === 'linux') { const regLocations = [ path.join(os.homedir(), '.steam', 'registry.vdf'), @@ -210,36 +238,40 @@ class SteamClientService { ]; for (const regPath of regLocations) { - let regData: any = { Registry: { HKCU: { Software: { Valve: { Steam: {} } } } } }; + if (!fs.existsSync(path.dirname(regPath))) continue; + + let regData: any = { Registry: { HKCU: { Software: { Valve: { Steam: { + AutoLoginUser: "", + RememberPassword: "1", + AlreadyLoggedIn: "1" + } } } } } }; if (fs.existsSync(regPath)) { try { const content = fs.readFileSync(regPath, 'utf-8'); - regData = parse(content) as any; + const parsed = parse(content) as any; + if (parsed && typeof parsed === 'object') regData = parsed; } catch (e) { } - } else { - const regDir = path.dirname(regPath); - if (!fs.existsSync(regDir)) fs.mkdirSync(regDir, { recursive: true }); } - const setPath = (obj: any, keys: string[], val: string) => { + // Deep merge helper + const ensurePath = (obj: any, keys: string[]) => { let curr = obj; - for (let i = 0; i < keys.length - 1; i++) { - if (!curr[keys[i]!]) curr[keys[i]!] = {}; - curr = curr[keys[i]!]; + for (const key of keys) { + if (!curr[key] || typeof curr[key] !== 'object') curr[key] = {}; + curr = curr[key]; } - curr[keys[keys.length - 1]!] = val; + return curr; }; - const steamReg = ['Registry', 'HKCU', 'Software', 'Valve', 'Steam']; - setPath(regData, [...steamReg, 'AutoLoginUser'], accountName); - setPath(regData, [...steamReg, 'RememberPassword'], "1"); - setPath(regData, [...steamReg, 'AlreadyLoggedIn'], "1"); - setPath(regData, [...steamReg, 'WantsOfflineMode'], "0"); + const steamKey = ensurePath(regData, ['Registry', 'HKCU', 'Software', 'Valve', 'Steam']); + steamKey.AutoLoginUser = accountName; + steamKey.RememberPassword = "1"; + steamKey.AlreadyLoggedIn = "1"; + steamKey.WantsOfflineMode = "0"; try { - fs.writeFileSync(regPath, stringify(regData)); - console.log(`[SteamClient] Registry updated: ${regPath}`); + this.safeWriteVdf(regPath, regData); } catch (e) { } } }