Skip to content

Commit 6cd38fa

Browse files
committed
fix(desktop): refresh Linux MIME handler cache
1 parent c0e09f3 commit 6cd38fa

2 files changed

Lines changed: 82 additions & 4 deletions

File tree

‎apps/desktop/src/app/DesktopLinuxUrlHandler.test.ts‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ const makeHandlerLayer = (
4949
recorded: RecordedRegistration,
5050
input: {
5151
readonly environment?: Record<string, unknown>;
52+
readonly updateDesktopDatabaseExitCode?: number;
5253
readonly xdgMimeExitCode?: number;
5354
readonly writeError?: PlatformError.PlatformError;
5455
} = {},
@@ -80,7 +81,11 @@ const makeHandlerLayer = (
8081
command: childProcess.command,
8182
args: childProcess.args,
8283
});
83-
return Effect.succeed(mockProcess(input.xdgMimeExitCode ?? 0));
84+
const exitCode =
85+
childProcess.command === "update-desktop-database"
86+
? (input.updateDesktopDatabaseExitCode ?? 0)
87+
: (input.xdgMimeExitCode ?? 0);
88+
return Effect.succeed(mockProcess(exitCode));
8489
}),
8590
),
8691
),
@@ -169,6 +174,10 @@ describe("DesktopLinuxUrlHandler", () => {
169174
);
170175
assert.include(recorded.files[0]?.content, "MimeType=x-scheme-handler/t3code;");
171176
assert.deepEqual(recorded.commands, [
177+
{
178+
command: "update-desktop-database",
179+
args: ["/home/alice/.local/share/applications"],
180+
},
172181
{
173182
command: "xdg-mime",
174183
args: ["default", "t3code-url-handler.desktop", "x-scheme-handler/t3code"],
@@ -207,10 +216,12 @@ describe("DesktopLinuxUrlHandler", () => {
207216
});
208217

209218
it.effect("never fails startup when registration cannot complete", () => {
219+
const desktopDatabaseFailed = emptyRecording();
210220
const xdgMimeFailed = emptyRecording();
211221
const writeFailed = emptyRecording();
212222

213223
return Effect.gen(function* () {
224+
yield* runRegister(desktopDatabaseFailed, { updateDesktopDatabaseExitCode: 1 });
214225
yield* runRegister(xdgMimeFailed, { xdgMimeExitCode: 1 });
215226
yield* runRegister(writeFailed, {
216227
writeError: PlatformError.systemError({
@@ -222,6 +233,10 @@ describe("DesktopLinuxUrlHandler", () => {
222233
}),
223234
});
224235

236+
assert.deepEqual(
237+
desktopDatabaseFailed.commands.map(({ command }) => command),
238+
["update-desktop-database", "xdg-mime"],
239+
);
225240
assert.equal(xdgMimeFailed.files.length, 1);
226241
assert.deepEqual(writeFailed.commands, []);
227242
});

‎apps/desktop/src/app/DesktopLinuxUrlHandler.ts‎

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ import { makeComponentLogger } from "./DesktopObservability.ts";
1717
// Electron's app.setAsDefaultProtocolClient resolves the desktop id from
1818
// setDesktopName, which cannot match those files — so the browser keeps
1919
// prompting "Choose an application" for every OAuth callback. Instead, write
20-
// our own handler entry pointing at the current AppImage and claim the
21-
// scheme default via xdg-mime, exactly what the file manager's "set as
22-
// default" checkbox would record in mimeapps.list.
20+
// our own handler entry pointing at the current AppImage, refresh the desktop
21+
// MIME cache so desktop environments recognize that entry as a handler, and
22+
// use xdg-mime to record it as the scheme default in mimeapps.list.
2323
export const URL_HANDLER_DESKTOP_ENTRY_NAME = "t3code-url-handler.desktop";
2424

2525
const { logInfo, logWarning } = makeComponentLogger("desktop-linux-url-handler");
@@ -42,6 +42,23 @@ export class DesktopLinuxUrlHandlerRegistrationError extends Schema.TaggedErrorC
4242

4343
const isRegistrationError = Schema.is(DesktopLinuxUrlHandlerRegistrationError);
4444

45+
export class DesktopLinuxUrlHandlerCacheRefreshError extends Schema.TaggedErrorClass<DesktopLinuxUrlHandlerCacheRefreshError>()(
46+
"DesktopLinuxUrlHandlerCacheRefreshError",
47+
{
48+
applicationsDir: Schema.String,
49+
exitCode: Schema.optionalKey(Schema.Number),
50+
cause: Schema.optionalKey(Schema.Defect()),
51+
},
52+
) {
53+
override get message(): string {
54+
const exitCode =
55+
this.exitCode === undefined ? "" : `, update-desktop-database exit code ${this.exitCode}`;
56+
return `Failed to refresh the desktop MIME cache at ${this.applicationsDir}${exitCode}.`;
57+
}
58+
}
59+
60+
const isCacheRefreshError = Schema.is(DesktopLinuxUrlHandlerCacheRefreshError);
61+
4562
const escapeDesktopEntryString = (value: string): string =>
4663
value
4764
.replaceAll("\\", "\\\\")
@@ -128,6 +145,37 @@ export const make = Effect.gen(function* () {
128145
),
129146
);
130147

148+
const updateDesktopDatabase = Effect.scoped(
149+
Effect.gen(function* () {
150+
const command = ChildProcess.make(
151+
"update-desktop-database",
152+
[environment.linuxApplicationsDir],
153+
{
154+
stdin: "ignore",
155+
stdout: "ignore",
156+
stderr: "ignore",
157+
},
158+
);
159+
const handle = yield* spawner.spawn(command);
160+
const exitCode = yield* handle.exitCode;
161+
if (exitCode !== 0) {
162+
return yield* new DesktopLinuxUrlHandlerCacheRefreshError({
163+
applicationsDir: environment.linuxApplicationsDir,
164+
exitCode,
165+
});
166+
}
167+
}),
168+
).pipe(
169+
Effect.mapError((error) =>
170+
isCacheRefreshError(error)
171+
? error
172+
: new DesktopLinuxUrlHandlerCacheRefreshError({
173+
applicationsDir: environment.linuxApplicationsDir,
174+
cause: error,
175+
}),
176+
),
177+
);
178+
131179
const setDefaultHandler = Effect.scoped(
132180
Effect.gen(function* () {
133181
const command = ChildProcess.make(
@@ -166,6 +214,21 @@ export const make = Effect.gen(function* () {
166214
return;
167215
}
168216
yield* writeDesktopEntry;
217+
218+
yield* updateDesktopDatabase.pipe(
219+
// Some MIME implementations, including GIO, use mimeinfo.cache to verify
220+
// that a desktop entry is associated with a scheme. Cache refresh is
221+
// independently best-effort so a missing update-desktop-database executable
222+
// does not prevent xdg-mime from recording the requested default.
223+
Effect.catch((error) =>
224+
logWarning("desktop MIME cache refresh failed", {
225+
applicationsDir: environment.linuxApplicationsDir,
226+
message: error.message,
227+
...(error.exitCode === undefined ? {} : { exitCode: error.exitCode }),
228+
}),
229+
),
230+
);
231+
169232
yield* setDefaultHandler;
170233
yield* logInfo("registered URL scheme handler", { scheme });
171234
}).pipe(

0 commit comments

Comments
 (0)