fix: Fixed a bunch of issues on linux

fix: Removed archive when unzipping with stream zip fallback
This commit is contained in:
Simeon Radivoev 2026-04-20 02:14:37 +03:00
parent 7065e64722
commit 6aacec2c0d
No known key found for this signature in database
GPG key ID: 6D1F209D0C277D60
22 changed files with 236 additions and 83 deletions

View file

@ -451,7 +451,7 @@ export default new Elysia()
}, {
params: z.object({ id: z.string(), source: z.string() }),
})
.post('/game/:source/:id/install', async ({ params: { id, source }, query: { downloadId } }) =>
.post('/game/:source/:id/install', async ({ params: { id, source }, body: { downloadId } }) =>
{
if (!taskQueue.findJob(InstallJob.query({ source, id }), InstallJob))
{
@ -462,7 +462,7 @@ export default new Elysia()
}
}, {
params: z.object({ id: z.string(), source: z.string() }),
query: z.object({ downloadId: z.string().optional() }),
body: z.object({ downloadId: z.string().optional() }),
response: z.any()
})
.delete('/game/:source/:id/install', async ({ params: { id, source } }) =>

View file

@ -4,11 +4,11 @@ import { getErrorMessage } from "@/bun/utils";
import { checkFiles, getLocalGameMatch, getSourceGameDetailed } from "./utils";
import fs from 'node:fs/promises';
import Elysia from "elysia";
import z from "zod";
import z, { string } from "zod";
import { InstallJob, InstallJobStates } from "../../jobs/install-job";
import { LaunchGameJob } from "../../jobs/launch-game-job";
import * as appSchema from "@schema/app";
import { RPC_URL } from "@/shared/constants";
import { DownloadSourceSchema, RPC_URL } from "@/shared/constants";
import { host } from "@/bun/utils/host";
export class CommandSearchError extends Error
@ -205,7 +205,7 @@ export default function buildStatusResponse ()
z.object({ status: z.literal('refresh'), localId: z.number().optional() }),
z.object({ status: z.literal(['queued']) }),
z.object({ status: z.literal('playing'), details: z.string() }),
z.object({ status: z.literal('install'), details: z.string() }),
z.object({ status: z.literal('install'), details: z.string(), sources: DownloadSourceSchema.array() }),
z.object({ status: z.literal('present'), details: z.string() }),
z.object({ status: z.literal(['download', 'extract']), progress: z.number() }),
]),
@ -261,6 +261,8 @@ export default function buildStatusResponse ()
} else if (!localGame && ws.data.params.source === 'store')
{
const downloads = await plugins.hooks.games.fetchDownloads.promise({ source: ws.data.params.source, id: ws.data.params.id });
const sources = downloads?.map(d => ({ id: d.id, name: d.id })) ?? [];
/*const storeGame = await getStoreGame(ws.data.params.id);
const fileResponse = await fetch(storeGame.file, { method: 'HEAD' });
const size = Number(fileResponse.headers.get('content-length'));
@ -274,19 +276,20 @@ export default function buildStatusResponse ()
ws.send({ status: 'install', details: 'Install' });
}*/
ws.send({ status: 'install', details: 'Install' });
ws.send({ status: 'install', details: 'Install', sources });
} else if (!localGame)
{
const files = await plugins.hooks.games.fetchDownloads.promise({
source: ws.data.params.source,
id: ws.data.params.id
});
const sources = files?.map(d => ({ id: d.id, name: d.id })) ?? [];
let filesChecked: LocalDownloadFileEntry[] | undefined;
if (files)
if (files && files.length)
{
filesChecked = await checkFiles(files.files, !!files.extract_path);
filesChecked = await checkFiles(files[0].files, !!files[0].extract_path);
}
if (filesChecked && !filesChecked.some(f => f.exists === false || f.matches === false))
@ -301,11 +304,11 @@ export default function buildStatusResponse ()
ws.send({ status: 'error', error: "Not Enough Free Space" });
} else if (filesChecked?.some(f => f.exists === true && f.matches === false))
{
ws.send({ status: 'install', details: 'Some Files Present, Install' });
ws.send({ status: 'install', details: 'Some Files Present, Install', sources });
}
else
{
ws.send({ status: 'install', details: 'Install' });
ws.send({ status: 'install', details: 'Install', sources });
}
}
} else

View file

@ -67,7 +67,7 @@ export class GameHooks
source: string;
id: string;
downloadId?: string;
}], DownloadInfo | undefined>(['ctx']);
}], DownloadInfo[] | undefined>(['ctx']);
fetchRomFiles = new AsyncSeriesBailHook<[ctx: {
source: string;
id: string;

View file

@ -53,7 +53,8 @@ export class InstallJob implements IJob<never, InstallJobStates>
const downloadPath = config.get('downloadPath');
let info: DownloadInfo | undefined;
info = await plugins.hooks.games.fetchDownloads.promise({ source: this.source, id: this.gameId, downloadId: this.config?.downloadId });
const allDownloads = await plugins.hooks.games.fetchDownloads.promise({ source: this.source, id: this.gameId, downloadId: this.config?.downloadId });
info = allDownloads?.[0];
if (!info) throw new Error(`Could not find downloader for source ${this.source}`);
@ -137,12 +138,21 @@ export class InstallJob implements IJob<never, InstallJobStates>
{
if (filePath.endsWith('.zip'))
{
cx.setProgress(0, "extract");
console.warn("Could not extract", filePath, "with 7zip trying zip extractor");
await ensureDir(extractPath);
const zip = new StreamZip.async({ file: filePath });
let entryCount = await zip.entriesCount;
let entryCounter = entryCount;
zip.on('extract', (entry, outPath) =>
{
entryCounter--;
cx.setProgress(progress + (1 - (entryCounter / entryCount)) * 100 * progressDelta, "extract");
});
const count = await zip.extract(null, extractPath);
console.log(`Extracted ${count} entries`);
await zip.close();
await fs.rm(filePath);
} else
{
throw e;

View file

@ -120,7 +120,7 @@ export class LaunchGameJob implements IJob<z.infer<typeof LaunchGameJob.dataSche
// ES-DE commands require shell execution. Some emulators fail otherwise.
const spawnGame = spawn(this.validCommand.command, {
shell: true,
shell: this.validCommand.shell ?? true,
cwd: this.validCommand.startDir,
signal: context.abortSignal,
env: {
@ -151,6 +151,7 @@ export class LaunchGameJob implements IJob<z.infer<typeof LaunchGameJob.dataSche
await this.prePlay(context.setProgress.bind(context), { platformSlug: gameInfo?.platformSlug });
// We have full control over launching integrated emulators better to use bun spawn
await fs.chmod(this.validCommand.metadata.emulatorBin, 0o755);
const bunGame = Bun.spawn([this.validCommand.metadata.emulatorBin, ...commandArgs.args], {
cwd: this.validCommand.startDir,
signal: context.abortSignal,

View file

@ -3,16 +3,13 @@ import desc from './package.json';
import { config, events } from "@/bun/api/app";
import path, { dirname } from 'node:path';
import unzip from 'unzip-stream';
import { ensureDir } from "fs-extra";
import { chmodSync, ensureDir } from "fs-extra";
import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";
import fs from 'node:fs/promises';
import { randomUUIDv7, sleep } from "bun";
import z from "zod";
import { createInterface } from "node:readline";
import { redirect } from "elysia";
import { getErrorMessage } from "@/bun/utils";
import { id } from "zod/v4/locales";
const SettingsSchema = z.object({
runWebGui: z.boolean()
@ -75,7 +72,13 @@ export default class RcloneIntegration implements PluginType<SettingsType>
ctx.zodRegistry.add(SettingsSchema.shape.globalConfig, { requiresRestart: true });
const toolsPath = path.join(config.get('downloadPath'), "tools");
const existingRclones = await Array.fromAsync(fs.glob('**/rclone.exe', { cwd: toolsPath }));
await ensureDir(toolsPath);
const binaryMap: Record<string, string> = {
win32: '**/rclone.exe',
linux: '**/rclone',
darwin: '**/rclone'
};
const existingRclones = await Array.fromAsync(fs.glob(binaryMap[process.platform], { cwd: toolsPath }));
if (existingRclones[0])
{
this.rclonePath = path.join(toolsPath, existingRclones[0]);
@ -83,13 +86,19 @@ export default class RcloneIntegration implements PluginType<SettingsType>
return;
}
if (await fs.exists(path.join(toolsPath, 'rclone-current-windows-amd64')))
{
return;
}
ctx.setProgress(0.5, "Downloading RClone");
const rcCloseZip = await fetch(`https://downloads.rclone.org/rclone-current-windows-amd64.zip`);
const platformMap: Record<string, string> = {
linux: "linux",
win32: "windows",
darwin: "osx"
};
const archMap: Record<string, string> = {
x64: "amd64",
arm64: "arm64"
};
const downloadUrl = `https://downloads.rclone.org/rclone-current-${platformMap[process.platform]}-${archMap[process.arch]}.zip`;
console.log("Starting Download", downloadUrl);
const rcCloseZip = await fetch(downloadUrl);
await ensureDir(toolsPath);
await pipeline(Readable.fromWeb(rcCloseZip.body as any), unzip.Extract({ path: toolsPath }));
@ -97,6 +106,7 @@ export default class RcloneIntegration implements PluginType<SettingsType>
if (dests[0])
{
this.rclonePath = path.join(toolsPath, dests[0]);
await fs.chmod(this.rclonePath, 0o755);
await this.startServer(ctx);
return;
}
@ -139,7 +149,12 @@ export default class RcloneIntegration implements PluginType<SettingsType>
if (data.level === 'error')
{
console.error(data.msg);
} else
} else if (data.level === 'critical')
{
console.error(data.msg);
}
else
{
console.log(e);
if (loginTokenUrlRegex.test(data.msg))
@ -150,7 +165,7 @@ export default class RcloneIntegration implements PluginType<SettingsType>
});
await new Promise((resolve) =>
await new Promise((resolve, reject) =>
{
const handleResolve = (line: string) =>
{
@ -160,6 +175,7 @@ export default class RcloneIntegration implements PluginType<SettingsType>
resolve(data);
};
rl.on('line', handleResolve);
setTimeout(() => { reject("Timeout"); }, 5000);
});
await this.refresh();

View file

@ -31,6 +31,12 @@ export default class RommIntegration implements PluginType<SettingsType>
release: "metadatum.first_release_date"
};
async checkRemote ()
{
if (!config.has('rommAddress')) return false;
return true;
}
async updateClient ()
{
client.setConfig({
@ -141,6 +147,7 @@ export default class RommIntegration implements PluginType<SettingsType>
ctx.hooks.games.fetchGames.tapPromise(desc.name, async ({ query, games }) =>
{
if (!await this.checkRemote()) return;
if (((!query.platform_source || query.platform_source === 'romm') || !!query.collection_id) && (!query.source || query.source === 'romm'))
{
@ -173,6 +180,7 @@ export default class RommIntegration implements PluginType<SettingsType>
ctx.hooks.games.fetchFilters.tapPromise(desc.name, async ({ filters, source }) =>
{
if (!await this.checkRemote()) return;
if (source && source !== 'romm') return;
const rommFilters = await getRomFiltersApiRomsFiltersGet({ throwOnError: true });
@ -185,12 +193,14 @@ export default class RommIntegration implements PluginType<SettingsType>
ctx.hooks.auth.loginComplete.tapPromise(desc.name, async ({ service }) =>
{
if (!await this.checkRemote()) return;
if (service !== 'romm') return;
await this.updateClient();
});
ctx.hooks.games.fetchGame.tapPromise(desc.name, async ({ source, id }) =>
{
if (!await this.checkRemote()) return;
if (source !== 'romm') return;
const rom = await getRomApiRomsIdGet({ path: { id: Number(id) } });
@ -205,6 +215,7 @@ export default class RommIntegration implements PluginType<SettingsType>
ctx.hooks.games.fetchDownloads.tapPromise(desc.name, async ({ source, id }) =>
{
if (!await this.checkRemote()) return;
if (source !== 'romm') return;
const rom = (await getRomApiRomsIdGet({ path: { id: Number(id) }, throwOnError: true })).data;
@ -260,12 +271,13 @@ export default class RommIntegration implements PluginType<SettingsType>
extract_path
};
return info;
return [info];
});
ctx.hooks.emulators.fetchBiosDownload.tapPromise(desc.name, async ({ systems, biosFolder }) =>
{
if (!await this.checkRemote()) return;
const files: DownloadFileEntry[] = [];
const allRommPlatforms = await this.getAllRommPlatforms();
@ -296,6 +308,7 @@ export default class RommIntegration implements PluginType<SettingsType>
ctx.hooks.games.fetchRecommendedGamesForGame.tapPromise(desc.name, async ({ game, games }) =>
{
if (!await this.checkRemote()) return;
const rommPlatforms = await this.getAllRommPlatforms();
if (rommPlatforms)
{
@ -313,7 +326,7 @@ export default class RommIntegration implements PluginType<SettingsType>
ctx.hooks.games.fetchRecommendedGamesForEmulator.tapPromise(desc.name, async ({ emulator, games, systems }) =>
{
if (!await this.checkRemote()) return;
const rommPlatforms = await this.getAllRommPlatforms();
const systemsRommSlugSet = new Set(systems.filter(s => s.romm_slug).map(s => s.romm_slug!));
if (rommPlatforms)
@ -343,6 +356,7 @@ export default class RommIntegration implements PluginType<SettingsType>
ctx.hooks.games.fetchPlatform.tapPromise(desc.name, async ({ source, id }) =>
{
if (!await this.checkRemote()) return;
if (source !== 'romm') return;
const { data: rommPlatform } = await getPlatformApiPlatformsIdGet({ path: { id: Number(id) } });
if (rommPlatform)
@ -365,7 +379,13 @@ export default class RommIntegration implements PluginType<SettingsType>
ctx.hooks.games.fetchPlatforms.tapPromise(desc.name, async ({ platforms }) =>
{
const rommPlatforms = await this.getAllRommPlatforms();
if (!await this.checkRemote()) return;
const rommPlatforms = await this.getAllRommPlatforms().catch(e =>
{
console.error(e);
return undefined;
});
if (rommPlatforms)
{
const frontEndPlatforms = await Promise.all(rommPlatforms.map(async p =>
@ -401,6 +421,7 @@ export default class RommIntegration implements PluginType<SettingsType>
ctx.hooks.games.prePlay.tapPromise(desc.name, async ({ source, id, saveFolderSlots, setProgress }) =>
{
if (!await this.checkRemote()) return;
if (source !== 'romm' || !ctx.config.get('savesSync')) return;
if (!saveFolderSlots) return;
@ -445,6 +466,7 @@ export default class RommIntegration implements PluginType<SettingsType>
// Should run after emulators decide on saves
ctx.hooks.games.postPlay.tapPromise({ name: desc.name, stage: 10 }, async ({ source, id, validChangedSaveFiles, command }) =>
{
if (!await this.checkRemote()) return;
if (source !== 'romm' || !ctx.config.get('savesSync')) return;
const sourceValidation = await validateGameSource(source, id);
@ -529,6 +551,7 @@ export default class RommIntegration implements PluginType<SettingsType>
ctx.hooks.games.fetchCollections.tapPromise(desc.name, async ({ collections }) =>
{
if (!await this.checkRemote()) return;
const rommCollections = await getCollectionsApiCollectionsGet();
if (rommCollections.response.ok && rommCollections.data)
{
@ -549,6 +572,7 @@ export default class RommIntegration implements PluginType<SettingsType>
ctx.hooks.games.fetchCollection.tapPromise(desc.name, async ({ source, id }) =>
{
if (!await this.checkRemote()) return;
if (source !== 'romm') return;
const collection = await getCollectionApiCollectionsIdGet({ path: { id: Number(id) } });
if (collection.data)
@ -567,6 +591,7 @@ export default class RommIntegration implements PluginType<SettingsType>
ctx.hooks.games.platformLookup.tapPromise(desc.name, async ({ source, id, slug }) =>
{
if (!await this.checkRemote()) return;
let platform: PlatformSchema | undefined = undefined;
if (id && source)
@ -587,6 +612,7 @@ export default class RommIntegration implements PluginType<SettingsType>
ctx.hooks.games.searchGame.tapPromise(desc.name, async ({ source, igdb_id, ra_id }) =>
{
if (!await this.checkRemote()) return;
if (source !== 'romm') return;
const roms = await getRomByMetadataProviderApiRomsByMetadataProviderGet({ query: { igdb_id, ra_id } });
if (roms.error) throw roms.error;

View file

@ -50,16 +50,16 @@ function convertStoreMediaToPath (c: string)
export async function convertStoreToFrontend (id: string, storeGame: StoreGameType): Promise<FrontEndGameType>
{
const validDownload = getValidDownload(storeGame);
const validDownloads = getValidDownloads(storeGame);
let platform_slug: string | null = null;
let platform_id: number | null = null;
let platform_display_name: string | null = null;
let path_platform_cover: string | null = null;
if (validDownload?.system)
if (validDownloads.length > 0 && validDownloads[0].system)
{
let system = validDownload.system.split(':')[0];
let system = validDownloads[0].system.split(':')[0];
if (system === 'win32') system = 'win';
const localPlatform = await db.query.platforms.findFirst({ where: eq(appSchema.platforms.slug, system), columns: { id: true, slug: true, name: true } });
@ -130,13 +130,13 @@ export async function convertStoreToFrontend (id: string, storeGame: StoreGameTy
export async function convertStoreToFrontendDetailed (id: string, storeGame: StoreGameType): Promise<FrontEndGameTypeDetailed>
{
const validDownload = getValidDownload(storeGame);
const validDownloads = getValidDownloads(storeGame);
let size: number | null = null;
if (validDownload?.url)
if (validDownloads.length > 0 && validDownloads[0].url)
{
try
{
const fileResponse = await fetch(validDownload?.url, { method: 'HEAD' });
const fileResponse = await fetch(validDownloads[0]?.url, { method: 'HEAD' });
size = Number(fileResponse.headers.get('content-length'));
} catch (error)
{
@ -167,25 +167,32 @@ export async function convertStoreToFrontendDetailed (id: string, storeGame: Sto
return detailed;
}
export function getValidDownload (game: StoreGameType, downloadId?: string)
export function getValidDownloads (game: StoreGameType, downloadId?: string)
{
const downloads = Object.entries(game.downloads).map(([k, d]) => ({ id: k, ...d }));
const supportedDownloads = downloads.filter(d => d.type === 'direct');
if (downloadId)
{
return supportedDownloads.find(d => d.id === downloadId);
return supportedDownloads.filter(d => d.id === downloadId);
} else
{
return supportedDownloads.find(d => d.system === `${process.platform}:${process.arch}`)
?? supportedDownloads.find(d =>
{
// Linux supports proton, can run windows games
if (process.platform === 'linux') return d.system === `win32:${process.arch}`;
return false;
})
// Fallback to emulator platforms
?? supportedDownloads.find(d => !d.system.includes(':'));
return supportedDownloads.filter(d =>
{
if (d.system === `${process.platform}:${process.arch}`) return true;
// TODO: Add linux proton support
//if (process.platform === 'linux' && d.system === `win32:${process.arch}`) return true;
// emulator fallback
return !d.system.includes(':');
}).toSorted((a, b) =>
{
const bScore = b.system.includes(':') ? 0 : 1;
const aScore = a.system.includes(':') ? 0 : 1;
return bScore - aScore;
});
}
}
@ -283,7 +290,8 @@ export async function convertStoreEmulatorToFrontend (emulator: EmulatorPackageT
systems,
gameCount: 0,
validSources: execPaths,
integrations: []
integrations: [],
source: "store"
};
return em;

View file

@ -1,22 +1,17 @@
import { PluginLoadingContextType, PluginType } from "@/bun/types/typesc.schema";
import desc from './package.json';
import path, { basename, dirname } from 'node:path';
import { StoreDownloadType, StoreGameSchema, StoreGameType } from "@/shared/constants";
import { buildStoreFrontendEmulatorSystems, getAllStoreEmulatorPackages, getStoreEmulatorPackage, getStoreFolder } from "@/bun/api/store/services/gamesService";
import { Glob, pathToFileURL } from "bun";
import { getOrCached } from "@/bun/api/cache";
import { shuffleInPlace } from "@/bun/utils";
import { and, eq } from "drizzle-orm";
import * as emulatorSchema from '@schema/emulators';
import { config, db, emulatorsDb, plugins, taskQueue } from "@/bun/api/app";
import { config, emulatorsDb, taskQueue } from "@/bun/api/app";
import fs from "node:fs/promises";
import { getSourceGameDetailed } from "@/bun/api/games/services/utils";
import mustache from "mustache";
import os from 'node:os';
import UpdateStoreJob from "@/bun/api/jobs/update-store";
import { getEmulatorDownload } from "@/bun/api/store/services/emulatorsService";
import { buildFilters, buildSaves, convertStoreEmulatorToFrontend, convertStoreToFrontend, convertStoreToFrontendDetailed, getExistingStoreEmulatorDownload, getShuffledStoreGames, getStoreGame, getValidDownload } from "./services";
import { buildFilters, buildSaves, convertStoreEmulatorToFrontend, convertStoreToFrontend, convertStoreToFrontendDetailed, getExistingStoreEmulatorDownload, getShuffledStoreGames, getStoreGame, getValidDownloads } from "./services";
export default class RommIntegration implements PluginType
{
@ -29,11 +24,13 @@ export default class RommIntegration implements PluginType
async load (ctx: PluginLoadingContextType)
{
await this.setup(ctx);
ctx.hooks.store.fetchDownload.tapPromise(desc.name, async ({ id }) =>
{
const emulatorPackage = await getStoreEmulatorPackage(id);
const downloadInfo = await getExistingStoreEmulatorDownload(emulatorPackage!);
if (!emulatorPackage) return;
const downloadInfo = await getExistingStoreEmulatorDownload(emulatorPackage);
return downloadInfo;
});
@ -131,7 +128,7 @@ export default class RommIntegration implements PluginType
ctx.hooks.games.buildLaunchCommands.tapPromise({ name: desc.name, before: 'com.simeonradivoev.gameflow.es' }, async ({ gamePath, source, sourceId, systemSlug, mainGlob }) =>
{
if (source !== 'store' || !gamePath || systemSlug !== 'win') return;
if (source !== 'store' || !gamePath) return;
const downloadPath = config.get('downloadPath');
const gamePathAbsolute = path.join(downloadPath, gamePath);
if (!(await fs.exists(gamePathAbsolute))) return;
@ -139,13 +136,15 @@ export default class RommIntegration implements PluginType
if (gamePathStat.isDirectory())
{
if (!mainGlob && systemSlug !== 'win') return;
const fileGlob = new Glob(mainGlob ?? '**/*.exe');
for await (const file of fileGlob.scan({ cwd: path.join(downloadPath, gamePath) }))
{
return [{
startDir: path.join(downloadPath, gamePath, dirname(file)),
command: basename(file),
id: 'store-win',
command: `./${basename(file)}`,
id: `store-${process.platform}`,
shell: false,
valid: true,
env: {
XDG_DATA_HOME: path.join(config.get('downloadPath'), 'save', source, sourceId ?? '')
@ -160,12 +159,13 @@ export default class RommIntegration implements PluginType
{
return [{
startDir: path.join(downloadPath, dirname(gamePath)),
command: basename(gamePath),
command: `./${basename(gamePath)}`,
env: {
XDG_DATA_HOME: path.join(config.get('downloadPath'), 'save', source, sourceId ?? '')
},
id: 'store-win',
id: `store-${process.platform}`,
valid: true,
shell: false,
metadata: {
romPath: path.join(downloadPath, gamePath)
}
@ -272,14 +272,15 @@ export default class RommIntegration implements PluginType
const game = await getStoreGame(id);
if (!game) throw new Error("Missing Store Game");
const validDownload = getValidDownload(game, downloadId);
const validDownloads = getValidDownloads(game, downloadId);
if (validDownload)
return validDownloads.map(validDownload =>
{
let system = validDownload.system.split(":")[0];
if (system === 'win32') system = 'win';
const info: DownloadInfo = {
id: validDownload.id,
coverUrl: game.covers?.[0] ? game.covers[0].startsWith('http') ? game.covers[0] : pathToFileURL(path.join(getStoreFolder(), game.covers[0])).href : "",
screenshotUrls: game.screenshots ?? [],
files: [{
@ -306,7 +307,7 @@ export default class RommIntegration implements PluginType
};
return info;
}
});
});
}
}

View file

@ -126,7 +126,14 @@ export const store = new Elysia({ prefix: '/api/store' })
})
.get('/emulator/:id', async ({ params: { id } }) =>
{
return plugins.hooks.store.fetchEmulator.promise({ id });
const emulator = await plugins.hooks.store.fetchEmulator.promise({ id });
if (!emulator) return status("Not Found");
const sources: EmulatorSourceEntryType[] = [];
await plugins.hooks.emulators.findEmulatorSource.promise({ emulator: emulator.name, sources });
const integrations = findEmulatorPluginIntegration(emulator.name, sources);
emulator.validSources = sources;
emulator.integrations = integrations;
return emulator;
}, { params: z.object({ id: z.string() }) })
.post('/install/emulator/:id/:source', async ({ params: { source, id }, body: { isUpdate } }) =>
{

View file

@ -1,4 +1,4 @@
import { spawnSync } from "bun";
import { Glob, spawnSync } from "bun";
import { platform } from "node:os";
import { RunBrowserType } from "./browser-spawner";
import path from 'node:path';
@ -48,12 +48,17 @@ const ARCH_MAP: Record<string, Record<string, string>> = {
};
/** The expected binary path per platform after extraction */
function getBundledBinaryPath (outDir: string, version: string, platform: string, arch: string): string
async function getBundledBinaryPath (outDir: string, version: string, platform: string, arch: string): Promise<string | undefined>
{
const subFolder = `ungoogled-chromium_${version}_${PLATFORM_MAP[platform]}_${ARCH_MAP[platform][arch]}`;
if (platform === "linux") return path.join(outDir, subFolder, "chrome");
if (platform === "darwin") return path.join(outDir, "Chromium.app");
return path.join(outDir, subFolder, "chrome.exe");
let glob: Glob | undefined = undefined;
if (platform === "linux") glob = new Glob(`**/chrome`);
else if (platform === "darwin") glob = new Glob(`**/Chromium.app`);
else glob = new Glob(`**/chrome.exe`);
for await (const bin of glob.scan({ cwd: outDir }))
{
return path.join(outDir, bin);
}
}
/**
@ -101,10 +106,14 @@ export async function getBrowserPath (config?: BrowserPriorityConfig): Promise<B
if (await versionFile.exists())
{
const getVerstion = await versionFile.text();
const binPath = getBundledBinaryPath("./bin/chromium", getVerstion, process.platform, process.arch);
if (await Bun.file(binPath).exists())
const binPath = await getBundledBinaryPath("./bin/chromium", getVerstion, process.platform, process.arch);
if (binPath)
{
return { path: binPath, type: "chromium", source: "bundled" };
console.log("Found Bundled Chromium Version", binPath);
if (await Bun.file(binPath).exists())
{
return { path: binPath, type: "chromium", source: "bundled" };
}
}
}