mirror of
https://github.com/tutao/tutanota.git
synced 2025-10-19 16:03:43 +00:00
[desktop] Fix opening files & directories on Linux
Electron uses xdg-open to open files. On GNOME it will try to use gio open which reads user configs from a few files. To detect those config files it uses `XDG_CURRENT_DESKTOP` env variable. Electron overwrites `XDG_CURRENT_DESKTOP` to enable some compatibility functions in Chromium. This breaks gio because it can't locate the config files for default apps. It can be reproduced on Ubuntu with GNOME. There is a fix in Electron to restore the env for opening files, but it doesn't work. We worked around that by invoking xdg-open directly with correct env. Additionally, we fixed opening the directories having the same issue and getting stuck by using showItemInFolder instead. This also has an additional benefit of highlighting the file in the file manager. Close #9696 Co-authored-by: hrb-hub <hrb-hub@users.noreply.github.com> Co-authored-by: wrd <wrd@tutao.de> Co-authored-by: paw <paw-hub@users.noreply.github.com>
This commit is contained in:
parent
02a285f08c
commit
439260efbb
6 changed files with 100 additions and 40 deletions
3
make.js
3
make.js
|
@ -48,8 +48,7 @@ await program
|
|||
const buildDir = app === "calendar" ? "build-calendar-app" : "build"
|
||||
const env = Object.assign({}, process.env, { ELECTRON_ENABLE_SECURITY_WARNINGS: "TRUE", ELECTRON_START_WITH_DEV_TOOLS: devTools })
|
||||
// we don't want to quit here because we want to keep piping output to our stdout.
|
||||
spawn("npx", [`electron --inspect=5858 ./${buildDir}/`], {
|
||||
shell: true,
|
||||
spawn("node_modules/.bin/electron", ["--inspect=5858", `./${buildDir}/`], {
|
||||
stdio: "inherit",
|
||||
env: options.verbose ? Object.assign({}, env, { ELECTRON_ENABLE_LOGGING: 1 }) : env,
|
||||
})
|
||||
|
|
|
@ -27,6 +27,11 @@ export interface ProcessParams {
|
|||
* By default, the current working directory will be used.
|
||||
*/
|
||||
currentDirectory?: string
|
||||
|
||||
/**
|
||||
* Environment variables to set on subprocesses
|
||||
*/
|
||||
env?: { [key: string]: string | undefined }
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -134,6 +139,7 @@ export class CommandExecutor {
|
|||
stdoutEncoding = ProcessIOEncoding.Utf8,
|
||||
stderrEncoding = ProcessIOEncoding.Utf8,
|
||||
currentDirectory,
|
||||
env,
|
||||
} = params
|
||||
|
||||
if (isNaN(timeout) || timeout < 0) {
|
||||
|
@ -145,6 +151,7 @@ export class CommandExecutor {
|
|||
const process = this.childProcess.spawn(executable, args, {
|
||||
timeout: timeout === 0 ? undefined : timeout,
|
||||
cwd: currentDirectory,
|
||||
env,
|
||||
})
|
||||
|
||||
let stdout = initializeOutputBuffer(stdoutEncoding)
|
||||
|
|
|
@ -330,7 +330,7 @@ async function createComponents(): Promise<Components> {
|
|||
new DesktopDesktopSystemFacade(wm, window, sock),
|
||||
new DesktopExportFacade(tfs, electron, conf, window, dragIcons, mailboxExportPersistence, fs, dateProvider, desktopExportLock),
|
||||
new DesktopExternalCalendarFacade(electron.app.userAgentFallback),
|
||||
new DesktopFileFacade(window, conf, dateProvider, customFetch, electron, tfs, fs, path),
|
||||
new DesktopFileFacade(window, conf, dateProvider, customFetch, electron, tfs, fs, path, commandExecutor, process),
|
||||
new DesktopInterWindowEventFacade(window, wm),
|
||||
nativeCredentialsFacade,
|
||||
desktopCrypto,
|
||||
|
|
|
@ -206,7 +206,7 @@ export class DesktopExportFacade implements ExportFacade {
|
|||
if (exportState == null || exportState.type !== "finished") {
|
||||
throw new ProgrammingError("Export is not finished")
|
||||
}
|
||||
await this.electron.shell.openPath(exportState.exportDirectoryPath)
|
||||
this.electron.shell.showItemInFolder(exportState.exportDirectoryPath)
|
||||
}
|
||||
|
||||
private async getExportDirectoryPath(): Promise<string> {
|
||||
|
|
|
@ -11,6 +11,7 @@ import { sha256Hash } from "@tutao/tutanota-crypto"
|
|||
import { assertNotNull, splitUint8ArrayInChunks, stringToUtf8Uint8Array, uint8ArrayToBase64, uint8ArrayToHex } from "@tutao/tutanota-utils"
|
||||
import { looksExecutable, nonClobberingFilename } from "../PathUtils.js"
|
||||
import url from "node:url"
|
||||
import type { WriteStream } from "node:fs"
|
||||
import FsModule from "node:fs"
|
||||
import { Buffer } from "node:buffer"
|
||||
import { default as stream } from "node:stream"
|
||||
|
@ -26,9 +27,9 @@ import { TempFs } from "./TempFs.js"
|
|||
import { HttpMethod } from "../../api/common/EntityFunctions"
|
||||
import { FetchImpl } from "../net/NetAgent"
|
||||
import { OpenDialogOptions } from "electron"
|
||||
import type { WriteStream } from "node:fs"
|
||||
|
||||
import { newPromise } from "@tutao/tutanota-utils/dist/Utils"
|
||||
import { CommandExecutor } from "../CommandExecutor"
|
||||
|
||||
const TAG = "[DesktopFileFacade]"
|
||||
|
||||
|
@ -45,6 +46,8 @@ export class DesktopFileFacade implements FileFacade {
|
|||
private readonly tfs: TempFs,
|
||||
private readonly fs: FsExports,
|
||||
private readonly path: PathExports,
|
||||
private readonly commandExecutor: CommandExecutor,
|
||||
private readonly process: NodeJS.Process,
|
||||
) {
|
||||
this.lastOpenedFileManagerAt = null
|
||||
}
|
||||
|
@ -139,32 +142,49 @@ export class DesktopFileFacade implements FileFacade {
|
|||
return fileUri
|
||||
}
|
||||
|
||||
open(location: string /* , mimeType: string omitted */): Promise<void> {
|
||||
const tryOpen = () =>
|
||||
async open(location: string /* , mimeType: string omitted */): Promise<void> {
|
||||
const openWithElectronShell = () =>
|
||||
this.electron.shell
|
||||
.openPath(location) // may resolve with "" or an error message
|
||||
.catch(() => "failed to open path.")
|
||||
.then((errMsg) => (errMsg === "" ? Promise.resolve() : Promise.reject(new FileOpenError("Could not open " + location + ", " + errMsg))))
|
||||
.catch((e) => {
|
||||
const message = "failed to open path." + e
|
||||
throw new FileOpenError("Could not open " + location + ", " + message)
|
||||
})
|
||||
|
||||
// only windows will happily execute a just downloaded program
|
||||
if (process.platform === "win32" && looksExecutable(location)) {
|
||||
return this.electron.dialog
|
||||
.showMessageBox({
|
||||
type: "warning",
|
||||
buttons: [lang.get("yes_label"), lang.get("no_label")],
|
||||
title: lang.get("executableOpen_label"),
|
||||
message: lang.get("executableOpen_msg"),
|
||||
defaultId: 1, // default button
|
||||
if (this.process.platform === "win32" && looksExecutable(location)) {
|
||||
const { response } = await this.electron.dialog.showMessageBox({
|
||||
type: "warning",
|
||||
buttons: [lang.get("yes_label"), lang.get("no_label")],
|
||||
title: lang.get("executableOpen_label"),
|
||||
message: lang.get("executableOpen_msg"),
|
||||
defaultId: 1, // default button
|
||||
})
|
||||
if (response === 0) {
|
||||
await openWithElectronShell()
|
||||
}
|
||||
} else if (this.process.platform === "linux") {
|
||||
// temporary fix for the electron fix:
|
||||
// https://github.com/electron/electron/issues/45129#issuecomment-3334644846
|
||||
// https://github.com/tutao/tutanota/issues/9696
|
||||
await this.commandExecutor
|
||||
.run({
|
||||
executable: "xdg-open",
|
||||
args: [location],
|
||||
env:
|
||||
// electron replaces XDG_CURRENT_DESKTOP in some cases which breaks gio open which breaks xdg-open
|
||||
this.process.env.ORIGINAL_XDG_CURRENT_DESKTOP == null
|
||||
? undefined
|
||||
: {
|
||||
...this.process.env,
|
||||
XDG_CURRENT_DESKTOP: this.process.env.ORIGINAL_XDG_CURRENT_DESKTOP,
|
||||
},
|
||||
})
|
||||
.then(({ response }) => {
|
||||
if (response === 0) {
|
||||
return tryOpen()
|
||||
} else {
|
||||
return Promise.resolve()
|
||||
}
|
||||
.catch((e) => {
|
||||
throw new FileOpenError("Could not open " + location + ", " + e)
|
||||
})
|
||||
} else {
|
||||
return tryOpen()
|
||||
await openWithElectronShell()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -307,7 +327,7 @@ export class DesktopFileFacade implements FileFacade {
|
|||
|
||||
if (lastOpenedFileManagerAt == null || this.dateProvider.now() - lastOpenedFileManagerAt > fileManagerTimeout) {
|
||||
this.lastOpenedFileManagerAt = this.dateProvider.now()
|
||||
await this.electron.shell.openPath(path.dirname(savePath))
|
||||
this.electron.shell.showItemInFolder(savePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import { ElectronExports, FsExports, PathExports } from "../../../../src/common/
|
|||
import { NotFoundError, PreconditionFailedError, TooManyRequestsError } from "../../../../src/common/api/common/error/RestError.js"
|
||||
import type fs from "node:fs"
|
||||
import { assertThrows } from "@tutao/tutanota-test-utils"
|
||||
import n from "../../nodemocker.js"
|
||||
import { stringToUtf8Uint8Array } from "@tutao/tutanota-utils"
|
||||
import { DesktopConfig } from "../../../../src/common/desktop/config/DesktopConfig.js"
|
||||
import { DesktopUtils } from "../../../../src/common/desktop/DesktopUtils.js"
|
||||
|
@ -16,6 +15,7 @@ import { TempFs } from "../../../../src/common/desktop/files/TempFs.js"
|
|||
import { BuildConfigKey, DesktopConfigKey } from "../../../../src/common/desktop/config/ConfigKeys.js"
|
||||
import { HttpMethod } from "../../../../src/common/api/common/EntityFunctions"
|
||||
import { FetchImpl, FetchResult } from "../../../../src/common/desktop/net/NetAgent"
|
||||
import { CommandExecutor } from "../../../../src/common/desktop/CommandExecutor"
|
||||
|
||||
const DEFAULT_DOWNLOAD_PATH = "/a/download/path/"
|
||||
|
||||
|
@ -30,6 +30,8 @@ o.spec("DesktopFileFacade", function () {
|
|||
let tfs: TempFs
|
||||
let ff: DesktopFileFacade
|
||||
let path: PathExports
|
||||
let executor: CommandExecutor
|
||||
let process: Writeable<Partial<NodeJS.Process>>
|
||||
|
||||
o.beforeEach(function () {
|
||||
win = object()
|
||||
|
@ -38,18 +40,23 @@ o.spec("DesktopFileFacade", function () {
|
|||
tfs = object()
|
||||
path = object()
|
||||
fs.promises = object()
|
||||
process = {
|
||||
env: {},
|
||||
}
|
||||
when(fs.promises.stat(matchers.anything())).thenResolve({ size: 42 })
|
||||
electron = object()
|
||||
// @ts-ignore read-only prop
|
||||
electron["shell"] = object()
|
||||
// @ts-ignore read-only prop
|
||||
electron["dialog"] = object()
|
||||
process.platform = "linux"
|
||||
|
||||
conf = object()
|
||||
du = object()
|
||||
dp = object()
|
||||
executor = object()
|
||||
|
||||
ff = new DesktopFileFacade(win, conf, dp, fetch, electron, tfs, fs, path)
|
||||
ff = new DesktopFileFacade(win, conf, dp, fetch, electron, tfs, fs, path, executor, process as NodeJS.Process)
|
||||
})
|
||||
o.spec("saveDataFile", function () {
|
||||
o("when there's no existing file it will be simply written", async function () {
|
||||
|
@ -309,18 +316,45 @@ o.spec("DesktopFileFacade", function () {
|
|||
})
|
||||
|
||||
o.spec("open", function () {
|
||||
o("open valid", async function () {
|
||||
when(electron.shell.openPath("/some/folder/file")).thenResolve("")
|
||||
await ff.open("/some/folder/file")
|
||||
o.spec("open on linux", () => {
|
||||
o.test("open on ubuntu", async function () {
|
||||
when(electron.shell.openPath("/some/folder/file")).thenReject(new Error("wrong function"))
|
||||
when(executor.run(matchers.anything())).thenResolve({})
|
||||
process.env!.ORIGINAL_XDG_CURRENT_DESKTOP = "original ;)"
|
||||
process.env!.SOMETHING_ELSE = "something else!"
|
||||
await ff.open("/some/folder/file")
|
||||
|
||||
verify(
|
||||
executor.run({
|
||||
executable: "xdg-open",
|
||||
args: ["/some/folder/file"],
|
||||
env: {
|
||||
XDG_CURRENT_DESKTOP: "original ;)",
|
||||
ORIGINAL_XDG_CURRENT_DESKTOP: "original ;)",
|
||||
SOMETHING_ELSE: "something else!",
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
o.test("open on non ubuntu", async function () {
|
||||
when(electron.shell.openPath("/some/folder/file")).thenReject(new Error("wrong function"))
|
||||
when(executor.run(matchers.anything())).thenResolve({})
|
||||
process.env!.ORIGINAL_XDG_CURRENT_DESKTOP = undefined
|
||||
process.env!.SOMETHING_ELSE = "something else!"
|
||||
await ff.open("/some/folder/file")
|
||||
|
||||
verify(
|
||||
executor.run({
|
||||
executable: "xdg-open",
|
||||
args: ["/some/folder/file"],
|
||||
env: undefined,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
o("open invalid", async () => {
|
||||
await assertThrows(Error, () => ff.open("invalid"))
|
||||
verify(electron.shell.openPath("invalid"), { times: 1 })
|
||||
})
|
||||
|
||||
o("open on windows", async function () {
|
||||
n.setPlatform("win32")
|
||||
o.test("open on windows", async function () {
|
||||
process.platform = "win32"
|
||||
when(electron.dialog.showMessageBox(matchers.anything())).thenReturn(
|
||||
Promise.resolve({
|
||||
response: 1,
|
||||
|
@ -384,7 +418,7 @@ o.spec("DesktopFileFacade", function () {
|
|||
const dir = "/path/to"
|
||||
const p = dir + "/file.txt"
|
||||
await ff.showInFileExplorer(p)
|
||||
verify(electron.shell.openPath(dir), { times: 1 })
|
||||
verify(electron.shell.showItemInFolder(p), { times: 1 })
|
||||
})
|
||||
|
||||
o("two downloads, open two filemanagers after a pause", async function () {
|
||||
|
@ -394,10 +428,10 @@ o.spec("DesktopFileFacade", function () {
|
|||
await ff.showInFileExplorer(p)
|
||||
when(dp.now()).thenReturn(time)
|
||||
when(conf.getConst(BuildConfigKey.fileManagerTimeout)).thenResolve(2)
|
||||
verify(electron.shell.openPath(dir), { times: 1 })
|
||||
verify(electron.shell.showItemInFolder(p), { times: 1 })
|
||||
when(dp.now()).thenReturn(time + 10)
|
||||
await ff.showInFileExplorer(p)
|
||||
verify(electron.shell.openPath(dir), { times: 2 })
|
||||
verify(electron.shell.showItemInFolder(p), { times: 2 })
|
||||
})
|
||||
})
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue