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 - name: Install Lua
run: | run: |
sudo apt-get update # Lua needs only Ubuntu sources; unrelated runner repositories can be unavailable.
sudo apt-get install -y lua5.4 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 sudo ln -sf /usr/bin/lua5.4 /usr/local/bin/lua
lua -v lua -v
+4 -4
View File
@@ -34,14 +34,14 @@
"@discordjs/rest@2.6.1": "patches/@discordjs%2Frest@2.6.1.patch", "@discordjs/rest@2.6.1": "patches/@discordjs%2Frest@2.6.1.patch",
}, },
"overrides": { "overrides": {
"@xmldom/xmldom": "0.8.13", "@xmldom/xmldom": "0.8.15",
"app-builder-lib": "26.15.3", "app-builder-lib": "26.15.3",
"brace-expansion": "5.0.9", "brace-expansion": "5.0.9",
"electron-builder-squirrel-windows": "26.15.3", "electron-builder-squirrel-windows": "26.15.3",
"fast-uri": "3.1.6", "fast-uri": "3.1.6",
"form-data": "4.0.6", "form-data": "4.0.6",
"ip-address": "10.2.0", "ip-address": "10.2.0",
"js-yaml": "4.3.1", "js-yaml": "4.3.2",
"lodash": "4.18.0", "lodash": "4.18.0",
"minimatch": "10.2.5", "minimatch": "10.2.5",
"picomatch": "4.0.4", "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=="], "@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=="], "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=="], "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=="], "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 - Composition over monoliths
- Pure helpers where possible - Pure helpers where possible
- Stable user behavior while internals evolve - 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. there instead of copying them into caller workflows.
- The reusable gate installs Lua and runs `bun run test:env`, so the shipped mpv - 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. 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 ## Default Handoff Gate
+2 -2
View File
@@ -83,14 +83,14 @@
"build:win:unsigned": "bun run build && node scripts/build-win-unsigned.mjs" "build:win:unsigned": "bun run build && node scripts/build-win-unsigned.mjs"
}, },
"overrides": { "overrides": {
"@xmldom/xmldom": "0.8.13", "@xmldom/xmldom": "0.8.15",
"app-builder-lib": "26.15.3", "app-builder-lib": "26.15.3",
"brace-expansion": "5.0.9", "brace-expansion": "5.0.9",
"electron-builder-squirrel-windows": "26.15.3", "electron-builder-squirrel-windows": "26.15.3",
"fast-uri": "3.1.6", "fast-uri": "3.1.6",
"form-data": "4.0.6", "form-data": "4.0.6",
"ip-address": "10.2.0", "ip-address": "10.2.0",
"js-yaml": "4.3.1", "js-yaml": "4.3.2",
"lodash": "4.18.0", "lodash": "4.18.0",
"minimatch": "10.2.5", "minimatch": "10.2.5",
"picomatch": "4.0.4", "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', () => { test('configureEarlyAppPaths pins userData to canonical SubMiner config dir', () => {
const calls: string[] = []; const calls: string[] = [];
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-entry-paths-'));
const userDataPath = configureEarlyAppPaths( const configDir = path.posix.join(tempDir, 'SubMiner');
{ try {
setName: (name) => { const userDataPath = configureEarlyAppPaths(
calls.push(`name:${name}`); {
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.equal(userDataPath, configDir);
assert.deepEqual(calls, ['name:SubMiner', 'path:userData:/tmp/xdg/SubMiner']); 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, 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.setName(APP_NAME);
app.setPath('userData', userDataPath); 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', () => { test('quality gate installs Lua and runs the environment suite before coverage', () => {
assert.match(qualityGateWorkflow, /name: Install Lua/); 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( assert.match(
qualityGateWorkflow, 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\)/, /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\)/,