fix: align Linux background lifecycle

This commit is contained in:
2026-08-01 21:16:05 +09:00
parent 845dde50ad
commit 82f29f0bd2
7 changed files with 106 additions and 2 deletions
+1
View File
@@ -2,6 +2,7 @@ node_modules/
nani-app/ nani-app/
build/ build/
dist/ dist/
result
.cache/ .cache/
*.dmg *.dmg
*.part *.part
+29
View File
@@ -0,0 +1,29 @@
import { definePatch, PHASE_MAIN_BUNDLE } from "./descriptor.mjs";
import { markerStatus, replaceExactly } from "./lib.mjs";
export const DEEPLINK_MARKER = "/* nani-linux:deeplink-readiness */";
export const DEEPLINK_ANCHOR =
'if(t&&e){o.info(`Handling initial deeplink: ${t}`);const n=t;e.webContents.once("did-finish-load",()=>{He(n)})}';
const DEEPLINK_REPLACEMENT =
'if(t&&e){o.info(`Handling initial deeplink: ${t}`);const n=t;e.webContents.isLoading()?e.webContents.once("did-finish-load",()=>{He(n)}):He(n)}' +
DEEPLINK_MARKER;
export default definePatch({
id: "deeplink-readiness",
phase: PHASE_MAIN_BUNDLE,
order: 12,
required: true,
apply({ source }) {
const status = markerStatus(source, DEEPLINK_MARKER);
if (status) return { source, status };
return {
source: replaceExactly(
source,
DEEPLINK_ANCHOR,
DEEPLINK_REPLACEMENT,
"initial deep link readiness",
),
status: "applied",
};
},
});
+4
View File
@@ -1,4 +1,6 @@
import deeplinkReadiness from "./deeplink-readiness.mjs";
import disableUpdater from "./disable-updater.mjs"; import disableUpdater from "./disable-updater.mjs";
import linuxCloseToTray from "./linux-close-to-tray.mjs";
import linuxOnboarding from "./linux-onboarding.mjs"; import linuxOnboarding from "./linux-onboarding.mjs";
import linuxTray from "./linux-tray.mjs"; import linuxTray from "./linux-tray.mjs";
import ocrStub from "./ocr-stub.mjs"; import ocrStub from "./ocr-stub.mjs";
@@ -10,6 +12,8 @@ export const corePatchDescriptors = [
ocrStub, ocrStub,
linuxOnboarding, linuxOnboarding,
linuxTray, linuxTray,
linuxCloseToTray,
deeplinkReadiness,
disableUpdater, disableUpdater,
xdgAutostart, xdgAutostart,
]; ];
+28
View File
@@ -0,0 +1,28 @@
import { definePatch, PHASE_MAIN_BUNDLE } from "./descriptor.mjs";
import { markerStatus, replaceExactly } from "./lib.mjs";
export const CLOSE_TO_TRAY_MARKER = "/* nani-linux:linux-close-to-tray */";
export const CLOSE_TO_TRAY_ANCHOR = 'process.platform==="darwin"&&Fs(d)';
const CLOSE_TO_TRAY_REPLACEMENT =
'(process.platform==="darwin"||process.platform==="linux")&&Fs(d)' +
CLOSE_TO_TRAY_MARKER;
export default definePatch({
id: "linux-close-to-tray",
phase: PHASE_MAIN_BUNDLE,
order: 11,
required: true,
apply({ source }) {
const status = markerStatus(source, CLOSE_TO_TRAY_MARKER);
if (status) return { source, status };
return {
source: replaceExactly(
source,
CLOSE_TO_TRAY_ANCHOR,
CLOSE_TO_TRAY_REPLACEMENT,
"Linux close-to-tray",
),
status: "applied",
};
},
});
+8 -1
View File
@@ -4,8 +4,9 @@ import { markerStatus, replaceExactly } from "./lib.mjs";
export const AUTOSTART_MARKER = "/* nani-linux:xdg-autostart */"; export const AUTOSTART_MARKER = "/* nani-linux:xdg-autostart */";
export const AUTOSTART_GET_ANCHOR = ',bi=(async()=>({autoLaunchEnabled:c.app.getLoginItemSettings().openAtLogin})),Ai="ms-settings:startupapps";'; export const AUTOSTART_GET_ANCHOR = ',bi=(async()=>({autoLaunchEnabled:c.app.getLoginItemSettings().openAtLogin})),Ai="ms-settings:startupapps";';
export const AUTOSTART_SET_ANCHOR = 'const Mi=(async(e,t)=>{if(process.platform==="win32")return await vi();try{return c.app.setLoginItemSettings({openAtLogin:t,openAsHidden:!0}),{ok:!0}}catch(n){return{ok:!1,message:n instanceof Error?n.message:"Unknown error"}}})'; export const AUTOSTART_SET_ANCHOR = 'const Mi=(async(e,t)=>{if(process.platform==="win32")return await vi();try{return c.app.setLoginItemSettings({openAtLogin:t,openAsHidden:!0}),{ok:!0}}catch(n){return{ok:!1,message:n instanceof Error?n.message:"Unknown error"}}})';
export const AUTOSTART_SYNC_ANCHOR = 'Fi(),Zs(),c.app.on("browser-window-created"';
const AUTOSTART_GET_REPLACEMENT = String.raw`,$naniAutostartPath=()=>S.join(process.env.XDG_CONFIG_HOME||S.join(c.app.getPath("home"),".config"),"autostart","nani.desktop"),$naniDesktopQuote=e=>'"'+e.replace(/([\\"\u0060$])/g,"\\$1")+'"',$naniGetAutostart=async()=>te.access($naniAutostartPath()).then(()=>!0,()=>!1),$naniSetAutostart=async e=>{const t=$naniAutostartPath();if(!e)return await te.rm(t,{force:!0}),{ok:!0};const n=process.env.NANI_LAUNCHER_PATH;if(!n||!S.isAbsolute(n)||/[\r\n]/.test(n))return{ok:!1,message:"NANI_LAUNCHER_PATH must be an absolute path"};await te.mkdir(S.dirname(t),{recursive:!0});const r=t+".tmp-"+process.pid+"-"+Date.now(),a="[Desktop Entry]\nType=Application\nName=Nani Translate\nExec="+$naniDesktopQuote(n)+" --autostart\nTerminal=false\nX-GNOME-Autostart-enabled=true\n";try{return await te.writeFile(r,a,{mode:420}),await te.rename(r,t),{ok:!0}}finally{await te.rm(r,{force:!0}).catch(()=>{})}},bi=(async()=>process.platform==="linux"?({autoLaunchEnabled:await $naniGetAutostart()}):({autoLaunchEnabled:c.app.getLoginItemSettings().openAtLogin}))` + AUTOSTART_MARKER + ',Ai="ms-settings:startupapps";'; const AUTOSTART_GET_REPLACEMENT = String.raw`,$naniAutostartPath=()=>S.join(process.env.XDG_CONFIG_HOME||S.join(c.app.getPath("home"),".config"),"autostart","nani.desktop"),$naniDesktopQuote=e=>'"'+e.replace(/([\\"\u0060$])/g,"\\$1")+'"',$naniGetAutostart=async()=>te.access($naniAutostartPath()).then(()=>!0,()=>!1),$naniSetAutostart=async e=>{const t=$naniAutostartPath();if(!e)return await te.rm(t,{force:!0}),{ok:!0};const n=process.env.NANI_LAUNCHER_PATH;if(!n||!S.isAbsolute(n)||/[\r\n]/.test(n))return{ok:!1,message:"NANI_LAUNCHER_PATH must be an absolute path"};await te.mkdir(S.dirname(t),{recursive:!0});const r=t+".tmp-"+process.pid+"-"+Date.now(),a="[Desktop Entry]\nType=Application\nName=Nani Translate\nExec="+$naniDesktopQuote(n)+" --autostart\nTerminal=false\nX-GNOME-Autostart-enabled=true\n";try{return await te.writeFile(r,a,{mode:420}),await te.rename(r,t),{ok:!0}}finally{await te.rm(r,{force:!0}).catch(()=>{})}},$naniSyncAutostart=async()=>{await $naniGetAutostart()&&await $naniSetAutostart(!0)},bi=(async()=>process.platform==="linux"?({autoLaunchEnabled:await $naniGetAutostart()}):({autoLaunchEnabled:c.app.getLoginItemSettings().openAtLogin}))` + AUTOSTART_MARKER + ',Ai="ms-settings:startupapps";';
const AUTOSTART_SET_REPLACEMENT = 'const Mi=(async(e,t)=>{if(process.platform==="win32")return await vi();try{return process.platform==="linux"?await $naniSetAutostart(t):(c.app.setLoginItemSettings({openAtLogin:t,openAsHidden:!0}),{ok:!0})}catch(n){return{ok:!1,message:n instanceof Error?n.message:"Unknown error"}}})'; const AUTOSTART_SET_REPLACEMENT = 'const Mi=(async(e,t)=>{if(process.platform==="win32")return await vi();try{return process.platform==="linux"?await $naniSetAutostart(t):(c.app.setLoginItemSettings({openAtLogin:t,openAsHidden:!0}),{ok:!0})}catch(n){return{ok:!1,message:n instanceof Error?n.message:"Unknown error"}}})';
@@ -29,6 +30,12 @@ export default definePatch({
AUTOSTART_SET_REPLACEMENT, AUTOSTART_SET_REPLACEMENT,
"XDG autostart setter", "XDG autostart setter",
); );
patched = replaceExactly(
patched,
AUTOSTART_SYNC_ANCHOR,
'Fi(),Zs(),process.platform==="linux"&&await $naniSyncAutostart(),c.app.on("browser-window-created"',
"XDG autostart launcher sync",
);
return { source: patched, status: "applied" }; return { source: patched, status: "applied" };
}, },
}); });
+28 -1
View File
@@ -10,6 +10,10 @@ import {
XSEL_RELATIVE_PATH, XSEL_RELATIVE_PATH,
} from "../scripts/patch-asar.mjs"; } from "../scripts/patch-asar.mjs";
import deeplinkReadiness, {
DEEPLINK_ANCHOR,
DEEPLINK_MARKER,
} from "../scripts/patches/deeplink-readiness.mjs";
import { normalizeDescriptors, PHASE_MAIN_BUNDLE } from "../scripts/patches/descriptor.mjs"; import { normalizeDescriptors, PHASE_MAIN_BUNDLE } from "../scripts/patches/descriptor.mjs";
import disableUpdater, { import disableUpdater, {
UPDATER_ENABLE_ANCHOR, UPDATER_ENABLE_ANCHOR,
@@ -17,6 +21,10 @@ import disableUpdater, {
UPDATER_MARKER, UPDATER_MARKER,
} from "../scripts/patches/disable-updater.mjs"; } from "../scripts/patches/disable-updater.mjs";
import { runPatchDescriptors } from "../scripts/patches/engine.mjs"; import { runPatchDescriptors } from "../scripts/patches/engine.mjs";
import linuxCloseToTray, {
CLOSE_TO_TRAY_ANCHOR,
CLOSE_TO_TRAY_MARKER,
} from "../scripts/patches/linux-close-to-tray.mjs";
import linuxOnboarding, { import linuxOnboarding, {
ONBOARDING_ANCHOR, ONBOARDING_ANCHOR,
verifyLinuxOnboarding, verifyLinuxOnboarding,
@@ -32,6 +40,7 @@ import xdgAutostart, {
AUTOSTART_GET_ANCHOR, AUTOSTART_GET_ANCHOR,
AUTOSTART_MARKER, AUTOSTART_MARKER,
AUTOSTART_SET_ANCHOR, AUTOSTART_SET_ANCHOR,
AUTOSTART_SYNC_ANCHOR,
} from "../scripts/patches/xdg-autostart.mjs"; } from "../scripts/patches/xdg-autostart.mjs";
function temporaryDirectory(t) { function temporaryDirectory(t) {
@@ -45,6 +54,8 @@ test("descriptors have deterministic phase/order and reject duplicate ids", () =
xdgAutostart, xdgAutostart,
disableUpdater, disableUpdater,
linuxTray, linuxTray,
linuxCloseToTray,
deeplinkReadiness,
ocrStub, ocrStub,
pruneNonLinux, pruneNonLinux,
linuxOnboarding, linuxOnboarding,
@@ -54,6 +65,8 @@ test("descriptors have deterministic phase/order and reject duplicate ids", () =
"ocr-stub", "ocr-stub",
"linux-onboarding", "linux-onboarding",
"linux-tray", "linux-tray",
"linux-close-to-tray",
"deeplink-readiness",
"disable-updater", "disable-updater",
"xdg-autostart", "xdg-autostart",
]); ]);
@@ -64,12 +77,21 @@ test("Nani 1.1.0 semantic anchors apply once and are idempotent", () => {
let source = [ let source = [
UPDATER_ENABLE_ANCHOR, UPDATER_ENABLE_ANCHOR,
TRAY_ANCHOR, TRAY_ANCHOR,
CLOSE_TO_TRAY_ANCHOR,
DEEPLINK_ANCHOR,
AUTOSTART_GET_ANCHOR, AUTOSTART_GET_ANCHOR,
AUTOSTART_SET_ANCHOR, AUTOSTART_SET_ANCHOR,
AUTOSTART_SYNC_ANCHOR,
UPDATER_HANDLERS_ANCHOR, UPDATER_HANDLERS_ANCHOR,
].join(";"); ].join(";");
for (const descriptor of [linuxTray, disableUpdater, xdgAutostart]) { for (const descriptor of [
linuxTray,
linuxCloseToTray,
deeplinkReadiness,
disableUpdater,
xdgAutostart,
]) {
const result = descriptor.apply({ source }); const result = descriptor.apply({ source });
assert.equal(result.status, "applied"); assert.equal(result.status, "applied");
source = result.source; source = result.source;
@@ -78,11 +100,16 @@ test("Nani 1.1.0 semantic anchors apply once and are idempotent", () => {
assert.equal(again.source, source); assert.equal(again.source, source);
} }
assert.match(source, new RegExp(TRAY_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); assert.match(source, new RegExp(TRAY_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
assert.match(source, new RegExp(CLOSE_TO_TRAY_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
assert.match(source, new RegExp(DEEPLINK_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
assert.match(source, new RegExp(UPDATER_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); assert.match(source, new RegExp(UPDATER_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
assert.match(source, new RegExp(AUTOSTART_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); assert.match(source, new RegExp(AUTOSTART_MARKER.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
assert.match(source, /process\.platform==="linux"\?async\(\)=>!1:xa/); assert.match(source, /process\.platform==="linux"\?async\(\)=>!1:xa/);
assert.match(source, /nativeImage\.createFromPath/); assert.match(source, /nativeImage\.createFromPath/);
assert.match(source, /NANI_LAUNCHER_PATH/); assert.match(source, /NANI_LAUNCHER_PATH/);
assert.match(source, /await \$naniSyncAutostart\(\)/);
assert.match(source, /webContents\.isLoading\(\)/);
assert.match(source, /process\.platform==="darwin"\|\|process\.platform==="linux"/);
}); });
test("required anchor drift is reported and main bundle remains unchanged", () => { test("required anchor drift is reported and main bundle remains unchanged", () => {
+8
View File
@@ -53,6 +53,14 @@ test("explicit ozone CLI argument wins and URL arguments pass through", () => {
]); ]);
}); });
test("hidden autostart argument passes through to Electron", () => {
const setup = fixture();
assert.deepEqual(runLauncher(setup, ["--autostart"]), [
"--ozone-platform=x11",
"--autostart",
]);
});
test("launcher limits optional CDP to localhost", () => { test("launcher limits optional CDP to localhost", () => {
const setup = fixture(); const setup = fixture();
assert.deepEqual(runLauncher(setup, [], { NANI_CDP_PORT: "9222" }), [ assert.deepEqual(runLauncher(setup, [], { NANI_CDP_PORT: "9222" }), [