diff --git a/src/backend/clients/database/SqliteDatabaseClient.ts b/src/backend/clients/database/SqliteDatabaseClient.ts index 6881b0aa4..825ca37e9 100644 --- a/src/backend/clients/database/SqliteDatabaseClient.ts +++ b/src/backend/clients/database/SqliteDatabaseClient.ts @@ -78,6 +78,7 @@ const AVAILABLE_MIGRATIONS: [number, string[]][] = [ [43, ['0047_app-url-updates.sql']], [44, ['0048_old-app-names-unique-tuple.sql']], [45, ['0049_music-player-pdf-player-updates.sql']], + [46, ['0050_add_preamble_version.sql']], ]; export class SqliteDatabaseClient extends AbstractDatabaseClient { diff --git a/src/backend/clients/database/migrations/mysql/mysql_mig_7.sql b/src/backend/clients/database/migrations/mysql/mysql_mig_7.sql new file mode 100644 index 000000000..672922b90 --- /dev/null +++ b/src/backend/clients/database/migrations/mysql/mysql_mig_7.sql @@ -0,0 +1,18 @@ +-- Copyright (C) 2024-present Puter Technologies Inc. +-- +-- This file is part of Puter. +-- +-- Puter is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Affero General Public License as published +-- by the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU Affero General Public License for more details. +-- +-- You should have received a copy of the GNU Affero General Public License +-- along with this program. If not, see . + +ALTER TABLE `subdomains` ADD COLUMN `preamble_version` varchar(64) DEFAULT NULL; diff --git a/src/backend/clients/database/migrations/sqlite/0050_add_preamble_version.sql b/src/backend/clients/database/migrations/sqlite/0050_add_preamble_version.sql new file mode 100644 index 000000000..672922b90 --- /dev/null +++ b/src/backend/clients/database/migrations/sqlite/0050_add_preamble_version.sql @@ -0,0 +1,18 @@ +-- Copyright (C) 2024-present Puter Technologies Inc. +-- +-- This file is part of Puter. +-- +-- Puter is free software: you can redistribute it and/or modify +-- it under the terms of the GNU Affero General Public License as published +-- by the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU Affero General Public License for more details. +-- +-- You should have received a copy of the GNU Affero General Public License +-- along with this program. If not, see . + +ALTER TABLE `subdomains` ADD COLUMN `preamble_version` varchar(64) DEFAULT NULL; diff --git a/src/backend/drivers/workers/WorkerDriver.ts b/src/backend/drivers/workers/WorkerDriver.ts index d183c6d5c..e40cba134 100644 --- a/src/backend/drivers/workers/WorkerDriver.ts +++ b/src/backend/drivers/workers/WorkerDriver.ts @@ -42,6 +42,7 @@ const WORKER_SUBDOMAIN_PREFIX = 'workers.puter.'; let preamble = ''; let preambleError = false; let preambleLineCount = 0; +let preambleVersion: string | null = null; try { const preamblePath = path.join( __dirname, @@ -50,6 +51,13 @@ try { console.log('reading: ' + preamblePath); preamble = readFileSync(preamblePath, 'utf-8'); preambleLineCount = preamble.split('\n').length - 1; + + const versionMatch = /^var __PUTER_PREAMBLE_VERSION__\s*=\s*"([^"]+)"/.exec( + preamble, + ); + if (versionMatch) { + preambleVersion = versionMatch[1]; + } } catch { console.warn( '[workers] preamble not built — workers will not have puter.js injected.', @@ -75,6 +83,10 @@ export class WorkerDriver extends PuterDriver { #cfBaseUrl = ''; + static currentPreambleVersion(): string | null { + return preambleVersion; + } + override onServerStart(): void { const cfg = this.#workerConfig(); if (cfg.ACCOUNTID) { @@ -199,6 +211,7 @@ export class WorkerDriver extends PuterDriver { String(existingSub.uuid), { root_dir_id: loaded.fsEntry?.sqlId ?? null, + preamble_version: preambleVersion, }, { userId: actor.user.id }, ); @@ -217,6 +230,7 @@ export class WorkerDriver extends PuterDriver { subdomain: subdomainName, rootDirId: loaded.fsEntry?.sqlId, appOwner: appOwnerId, + preambleVersion, }); } @@ -604,6 +618,14 @@ export class WorkerDriver extends PuterDriver { preamble + sourceCode, )) as { success?: boolean; errors?: unknown[]; url?: string }; + if (cfResult.success && row.uuid) { + await this.stores.subdomain.update( + String(row.uuid), + { preamble_version: preambleVersion }, + { userId }, + ); + } + // Notify the user await this.#notifyUser(userId, workerName, cfResult); } catch (err) { diff --git a/src/backend/stores/subdomain/SubdomainStore.js b/src/backend/stores/subdomain/SubdomainStore.js index 1c7445fe6..393198a8d 100644 --- a/src/backend/stores/subdomain/SubdomainStore.js +++ b/src/backend/stores/subdomain/SubdomainStore.js @@ -182,13 +182,14 @@ export class SubdomainStore extends PuterStore { // ── Writes ─────────────────────────────────────────────────────── - /** @param {{ userId: number, subdomain: string, rootDirId?: number|null, associatedAppId?: number|null, appOwner?: number|null }} opts */ + /** @param {{ userId: number, subdomain: string, rootDirId?: number|null, associatedAppId?: number|null, appOwner?: number|null, preambleVersion?: string|null }} opts */ async create({ userId, subdomain, rootDirId = null, associatedAppId = null, appOwner = null, + preambleVersion = null, }) { if (!userId || !subdomain) { throw new Error('create: userId and subdomain are required'); @@ -196,8 +197,8 @@ export class SubdomainStore extends PuterStore { const uuid = uuidv4(); await this.clients.db.write( `INSERT INTO \`subdomains\` - (\`uuid\`, \`subdomain\`, \`user_id\`, \`root_dir_id\`, \`associated_app_id\`, \`app_owner\`) - VALUES (?, ?, ?, ?, ?, ?)`, + (\`uuid\`, \`subdomain\`, \`user_id\`, \`root_dir_id\`, \`associated_app_id\`, \`app_owner\`, \`preamble_version\`) + VALUES (?, ?, ?, ?, ?, ?, ?)`, [ uuid, subdomain, @@ -205,6 +206,7 @@ export class SubdomainStore extends PuterStore { rootDirId ?? null, associatedAppId, appOwner, + preambleVersion, ], ); @@ -215,6 +217,7 @@ export class SubdomainStore extends PuterStore { root_dir_id: rootDirId ?? null, associated_app_id: associatedAppId, app_owner: appOwner, + preamble_version: preambleVersion, }; await this.#refreshCache(row); await this.#invalidatePrefixListsForUser(userId); diff --git a/src/worker/scripts/buildPreamble.mjs b/src/worker/scripts/buildPreamble.mjs index addf5d9e1..94057dd98 100644 --- a/src/worker/scripts/buildPreamble.mjs +++ b/src/worker/scripts/buildPreamble.mjs @@ -1,4 +1,5 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { execSync } from 'node:child_process'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -8,6 +9,16 @@ const templatePath = path.join(workerDir, 'template', 'puter-portable.template') const outputDir = path.join(workerDir, 'dist'); const outputPath = path.join(outputDir, 'workerPreamble.js'); +// Build a version stamp: puter-js version + short git SHA +const puterJsPkg = JSON.parse( + await readFile(path.resolve(workerDir, '../puter-js/package.json'), 'utf-8'), +); +let gitSha = 'unknown'; +try { + gitSha = execSync('git rev-parse --short HEAD', { encoding: 'utf-8' }).trim(); +} catch { /* not in a git repo — keep "unknown" */ } +const preambleVersion = `${puterJsPkg.version}+${gitSha}`; + const inlineIncludes = async (filePath) => { const fileContents = await readFile(filePath, 'utf-8'); const lines = fileContents.split('\n'); @@ -36,5 +47,6 @@ const inlineIncludes = async (filePath) => { }; await mkdir(outputDir, { recursive: true }); +const versionBanner = `var __PUTER_PREAMBLE_VERSION__ = ${JSON.stringify(preambleVersion)};\n`; const preambleSource = await inlineIncludes(templatePath); -await writeFile(outputPath, preambleSource); +await writeFile(outputPath, versionBanner + preambleSource);