fix(startup): create config directory before singleton lock (#242)

This commit is contained in:
2026-09-09 12:04:08 -07:00
committed by GitHub
parent 9eecb7358b
commit 614a8ca912
9 changed files with 80 additions and 26 deletions
+5 -2
View File
@@ -60,8 +60,11 @@ jobs:
- name: Install Lua
run: |
sudo apt-get update
sudo apt-get install -y lua5.4
# Lua needs only Ubuntu sources; unrelated runner repositories can be unavailable.
test -f /etc/apt/sources.list.d/ubuntu.sources
apt_sources=(-o Dir::Etc::sourcelist=sources.list.d/ubuntu.sources -o Dir::Etc::sourceparts=-)
sudo apt-get "${apt_sources[@]}" update
sudo apt-get "${apt_sources[@]}" install -y lua5.4
sudo ln -sf /usr/bin/lua5.4 /usr/local/bin/lua
lua -v
+4 -4
View File
@@ -34,14 +34,14 @@
"@discordjs/rest@2.6.1": "patches/@discordjs%2Frest@2.6.1.patch",
},
"overrides": {
"@xmldom/xmldom": "0.8.13",
"@xmldom/xmldom": "0.8.15",
"app-builder-lib": "26.15.3",
"brace-expansion": "5.0.9",
"electron-builder-squirrel-windows": "26.15.3",
"fast-uri": "3.1.6",
"form-data": "4.0.6",
"ip-address": "10.2.0",
"js-yaml": "4.3.1",
"js-yaml": "4.3.2",
"lodash": "4.18.0",
"minimatch": "10.2.5",
"picomatch": "4.0.4",
@@ -226,7 +226,7 @@
"@xhayper/discord-rpc": ["@xhayper/discord-rpc@1.3.4", "", { "dependencies": { "@discordjs/rest": "^2.6.1", "@vladfrangu/async_event_emitter": "^2.4.7", "discord-api-types": "^0.38.47", "ws": "^8.20.0" } }, "sha512-ff0uEXuibh9wi+l4vOj7xInLUjtlTaQBje/SCyQkeXZ0j2V0y+Zge5PQIQFRHH9TjjGaYJkTofEcQhncM2q7/w=="],
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.13", "", {}, "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw=="],
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.15", "", {}, "sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA=="],
"abbrev": ["abbrev@4.0.0", "", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="],
@@ -498,7 +498,7 @@
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="],
"js-yaml": ["js-yaml@4.3.2", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA=="],
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
@@ -0,0 +1,4 @@
type: fixed
area: startup
- Fixed first launch exiting on macOS when the SubMiner config directory did not yet exist by creating it before acquiring the startup lock.
+4
View File
@@ -40,3 +40,7 @@ The desktop app keeps `src/main.ts` as composition root and pushes behavior into
- Composition over monoliths
- Pure helpers where possible
- Stable user behavior while internals evolve
Startup resolves and creates the user-data directory in `src/main-entry-runtime.ts`
before the entry process requests Electron's single-instance lock. Main-process
config bootstrap then writes the default config only when no config file exists.
+2
View File
@@ -23,6 +23,8 @@ Read when: selecting the right verification lane for a change
there instead of copying them into caller workflows.
- The reusable gate installs Lua and runs `bun run test:env`, so the shipped mpv
plugin tests run for every pull request and tagged release.
Lua installation uses only the runner's Ubuntu package sources so unrelated
third-party repository failures do not block the gate.
## Default Handoff Gate
+2 -2
View File
@@ -83,14 +83,14 @@
"build:win:unsigned": "bun run build && node scripts/build-win-unsigned.mjs"
},
"overrides": {
"@xmldom/xmldom": "0.8.13",
"@xmldom/xmldom": "0.8.15",
"app-builder-lib": "26.15.3",
"brace-expansion": "5.0.9",
"electron-builder-squirrel-windows": "26.15.3",
"fast-uri": "3.1.6",
"form-data": "4.0.6",
"ip-address": "10.2.0",
"js-yaml": "4.3.1",
"js-yaml": "4.3.2",
"lodash": "4.18.0",
"minimatch": "10.2.5",
"picomatch": "4.0.4",
+51 -17
View File
@@ -585,24 +585,58 @@ test('shouldDetachBackgroundLaunch only for first background invocation', () =>
test('configureEarlyAppPaths pins userData to canonical SubMiner config dir', () => {
const calls: string[] = [];
const userDataPath = configureEarlyAppPaths(
{
setName: (name) => {
calls.push(`name:${name}`);
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-entry-paths-'));
const configDir = path.posix.join(tempDir, 'SubMiner');
try {
const userDataPath = configureEarlyAppPaths(
{
setName: (name) => {
calls.push(`name:${name}`);
},
setPath: (key, value) => {
calls.push(`path:${key}:${value}`);
},
},
setPath: (key, value) => {
calls.push(`path:${key}:${value}`);
{
platform: 'linux',
homeDir: tempDir,
xdgConfigHome: tempDir,
existsSync: (candidate) =>
candidate === path.posix.join(tempDir, 'subminer', 'config.jsonc'),
},
},
{
platform: 'linux',
homeDir: '/home/tester',
xdgConfigHome: '/tmp/xdg',
existsSync: (candidate) => candidate === '/tmp/xdg/subminer/config.jsonc',
},
);
);
assert.equal(userDataPath, '/tmp/xdg/SubMiner');
assert.deepEqual(calls, ['name:SubMiner', 'path:userData:/tmp/xdg/SubMiner']);
assert.equal(userDataPath, configDir);
assert.deepEqual(calls, ['name:SubMiner', `path:userData:${configDir}`]);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
test('configureEarlyAppPaths creates a fresh macOS config directory before Electron uses it', () => {
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-entry-first-launch-'));
const configDir = path.posix.join(homeDir, '.config', 'SubMiner');
try {
const app = {
setName: () => {},
setPath: (_key: 'userData', value: string) => {
assert.equal(value, configDir);
assert.equal(fs.statSync(value).isDirectory(), true);
},
};
const options = { platform: 'darwin', homeDir, xdgConfigHome: '' } satisfies Parameters<
typeof configureEarlyAppPaths
>[1];
assert.equal(fs.existsSync(path.join(homeDir, '.config')), false);
configureEarlyAppPaths(app, options);
const configPath = path.join(configDir, 'config.jsonc');
const existingConfig = '{"logging":{"level":"debug"}}\n';
fs.writeFileSync(configPath, existingConfig);
configureEarlyAppPaths(app, options);
assert.equal(fs.readFileSync(configPath, 'utf8'), existingConfig);
} finally {
fs.rmSync(homeDir, { recursive: true, force: true });
}
});
+2
View File
@@ -260,6 +260,8 @@ export function configureEarlyAppPaths(app: EarlyAppLike, options?: EarlyAppPath
existsSync: options?.existsSync ?? fs.existsSync,
});
// The entry process requests its singleton lock before main-process config bootstrap.
fs.mkdirSync(userDataPath, { recursive: true });
app.setName(APP_NAME);
app.setPath('userData', userDataPath);
+6 -1
View File
@@ -24,7 +24,12 @@ test('quality gate checkout does not persist GitHub credentials', () => {
test('quality gate installs Lua and runs the environment suite before coverage', () => {
assert.match(qualityGateWorkflow, /name: Install Lua/);
assert.match(qualityGateWorkflow, /apt-get install -y lua5\.4/);
assert.match(
qualityGateWorkflow,
/apt_sources=\(-o Dir::Etc::sourcelist=sources\.list\.d\/ubuntu\.sources -o Dir::Etc::sourceparts=-\)/,
);
assert.match(qualityGateWorkflow, /apt-get\s+"\$\{apt_sources\[@\]\}"\s+update/);
assert.match(qualityGateWorkflow, /apt-get\s+"\$\{apt_sources\[@\]\}"\s+install\s+-y\s+lua5\.4/);
assert.match(
qualityGateWorkflow,
/Test suite \(source\)\n\s*run: bun run test:fast\n\s*\n\s*- name: Environment suite\n\s*run: bun run test:env\n\s*\n\s*- name: Coverage suite \(maintained source lane\)/,