From c0da2ffe8a16f98457e4fa750298057ec7036b61 Mon Sep 17 00:00:00 2001 From: Brendan Forster Date: Wed, 14 Jul 2021 11:53:03 -0300 Subject: [PATCH] introduce abstractions to make Flatpak integration easier (#555) Co-Authored-By: nullrequest <30698906+advaithm@users.noreply.github.com> --- app/src/lib/editors/found-editor.ts | 6 ++ app/src/lib/editors/launch.ts | 22 ++++++- app/src/lib/editors/linux.ts | 8 ++- app/src/lib/helpers/linux.ts | 93 +++++++++++++++++++++++++++++ app/src/lib/shells/linux.ts | 26 ++++---- app/src/lib/shells/shared.ts | 20 ++++++- app/test/unit/helpers/linux-test.ts | 30 ++++++++++ 7 files changed, 190 insertions(+), 15 deletions(-) create mode 100644 app/src/lib/helpers/linux.ts create mode 100644 app/test/unit/helpers/linux-test.ts diff --git a/app/src/lib/editors/found-editor.ts b/app/src/lib/editors/found-editor.ts index a2c7f2bb5ad..c6a54597cc5 100644 --- a/app/src/lib/editors/found-editor.ts +++ b/app/src/lib/editors/found-editor.ts @@ -1,5 +1,11 @@ export interface IFoundEditor { readonly editor: T readonly path: string + /** + * Indicate to Desktop to launch the editor with the `shell: true` option included. + * + * This is available to all platforms, but is only currently used by some Windows + * editors as their launch programs end in `.cmd` + */ readonly usesShell?: boolean } diff --git a/app/src/lib/editors/launch.ts b/app/src/lib/editors/launch.ts index 1d098f3843f..d20be766e48 100644 --- a/app/src/lib/editors/launch.ts +++ b/app/src/lib/editors/launch.ts @@ -1,7 +1,25 @@ import { spawn, SpawnOptions } from 'child_process' -import { pathExists } from 'fs-extra' +import { pathExists as pathExistsDefault } from 'fs-extra' +import { pathExists as pathExistsLinux, spawnEditor } from '../helpers/linux' import { ExternalEditorError, FoundEditor } from './shared' +/** + * Use a platform-specific pathExists based on the platform, to simplify changes + * to the application logic + * + * @param path the location of some program on disk + * + * @returns `true` if the path exists on disk, or `false` otherwise + * + */ +function pathExists(path: string) { + if (__LINUX__) { + return pathExistsLinux(path) + } else { + return pathExistsDefault(path) + } +} + /** * Open a given file or folder in the desired external editor. * @@ -35,6 +53,8 @@ export async function launchExternalEditor( // In macOS we can use `open`, which will open the right executable file // for us, we only need the path to the editor .app folder. spawn('open', ['-a', editorPath, fullPath], opts) + } else if (__LINUX__) { + spawnEditor(editorPath, fullPath, opts) } else { spawn(editorPath, [fullPath], opts) } diff --git a/app/src/lib/editors/linux.ts b/app/src/lib/editors/linux.ts index 91fd4ecbe14..04c9fb4e45a 100644 --- a/app/src/lib/editors/linux.ts +++ b/app/src/lib/editors/linux.ts @@ -1,4 +1,4 @@ -import { pathExists } from 'fs-extra' +import { pathExists } from '../helpers/linux' import { IFoundEditor } from './found-editor' @@ -34,7 +34,11 @@ const editors: ILinuxExternalEditor[] = [ }, { name: 'VSCodium', - paths: ['/usr/bin/codium', '/var/lib/flatpak/app/com.vscodium.codium'], + paths: [ + '/usr/bin/codium', + '/var/lib/flatpak/app/com.vscodium.codium', + '/usr/share/vscodium-bin/bin/codium', + ], }, { name: 'Sublime Text', diff --git a/app/src/lib/helpers/linux.ts b/app/src/lib/helpers/linux.ts new file mode 100644 index 00000000000..73fe58887fe --- /dev/null +++ b/app/src/lib/helpers/linux.ts @@ -0,0 +1,93 @@ +import { join } from 'path' +import { pathExists as pathExistsInternal } from 'fs-extra' +import { + ChildProcess, + spawn, + SpawnOptionsWithoutStdio, + SpawnOptions, +} from 'child_process' + +export function isFlatpakBuild() { + return __LINUX__ && process.env.FLATPAK_HOST === '1' +} + +/** + * Convert an executable path to be relative to the flatpak host + * + * @param path a path to an executable relative to the root of the filesystem + * + */ +export function convertToFlatpakPath(path: string) { + if (!__LINUX__) { + return path + } + + if (path.startsWith('/opt/')) { + return path + } + + return join('/var/run/host', path) +} + +/** + * Checks the file path on disk exists before attempting to launch a specific shell + * + * @param path + * + * @returns `true` if the path can be resolved, or `false` otherwise + */ +export async function pathExists(path: string): Promise { + if (isFlatpakBuild()) { + path = convertToFlatpakPath(path) + } + + try { + return await pathExistsInternal(path) + } catch { + return false + } +} + +/** + * Spawn a particular shell in a way that works for Flatpak-based usage + * + * @param path path to shell, relative to the root of the filesystem + * @param args arguments to provide to the shell + * @param options additional options to provide to spawn function + * + * @returns a child process to observe and monitor + */ +export function spawnShell( + path: string, + args: string[], + options?: SpawnOptionsWithoutStdio +): ChildProcess { + if (isFlatpakBuild()) { + return spawn('flatpak-spawn', ['--host', path, ...args], options) + } + + return spawn(path, args, options) +} + +/** + * Spawn a given editor in a way that works for Flatpak-based usage + * + * @param path path to editor, relative to the root of the filesystem + * @param workingDirectory working directory to open initially in editor + * @param options additional options to provide to spawn function + */ +export function spawnEditor( + path: string, + workingDirectory: string, + options: SpawnOptions +): ChildProcess { + if (isFlatpakBuild()) { + return spawn( + 'flatpak-spawn', + ['--host', path, `"${workingDirectory}"`], + options + ) + } else { + return spawn(path, [workingDirectory], options) + } +} diff --git a/app/src/lib/shells/linux.ts b/app/src/lib/shells/linux.ts index 47a5c7f038c..393e0bb7e30 100644 --- a/app/src/lib/shells/linux.ts +++ b/app/src/lib/shells/linux.ts @@ -1,8 +1,8 @@ -import { spawn, ChildProcess } from 'child_process' -import { pathExists } from 'fs-extra' +import { ChildProcess } from 'child_process' import { assertNever } from '../fatal-error' import { IFoundShell } from './found-shell' import { parseEnumValue } from '../enum' +import { pathExists as pathExistsLinux, spawnShell } from '../helpers/linux' export enum Shell { Gnome = 'GNOME Terminal', @@ -27,7 +27,7 @@ export function parse(label: string): Shell { } async function getPathIfAvailable(path: string): Promise { - return (await pathExists(path)) ? path : null + return (await pathExistsLinux(path)) ? path : null } function getShellPath(shell: Shell): Promise { @@ -164,21 +164,25 @@ export function launch( case Shell.Terminator: case Shell.XFCE: case Shell.Alacritty: - return spawn(foundShell.path, ['--working-directory', path]) + return spawnShell(foundShell.path, ['--working-directory', path]) case Shell.Urxvt: - return spawn(foundShell.path, ['-cd', path]) + return spawnShell(foundShell.path, ['-cd', path]) case Shell.Konsole: - return spawn(foundShell.path, ['--workdir', path]) + return spawnShell(foundShell.path, ['--workdir', path]) case Shell.Xterm: - return spawn(foundShell.path, ['-e', '/bin/bash'], { cwd: path }) + return spawnShell(foundShell.path, ['-e', '/bin/bash'], { cwd: path }) case Shell.Terminology: - return spawn(foundShell.path, ['-d', path]) + return spawnShell(foundShell.path, ['-d', path]) case Shell.Deepin: - return spawn(foundShell.path, ['-w', path]) + return spawnShell(foundShell.path, ['-w', path]) case Shell.Elementary: - return spawn(foundShell.path, ['-w', path]) + return spawnShell(foundShell.path, ['-w', path]) case Shell.Kitty: - return spawn(foundShell.path, ['--single-instance', '--directory', path]) + return spawnShell(foundShell.path, [ + '--single-instance', + '--directory', + path, + ]) default: return assertNever(shell, `Unknown shell: ${shell}`) } diff --git a/app/src/lib/shells/shared.ts b/app/src/lib/shells/shared.ts index f09bfdfa774..4ec05019ac8 100644 --- a/app/src/lib/shells/shared.ts +++ b/app/src/lib/shells/shared.ts @@ -1,9 +1,10 @@ import { ChildProcess } from 'child_process' -import { pathExists } from 'fs-extra' +import { pathExists as pathExistsDefault } from 'fs-extra' import * as Darwin from './darwin' import * as Win32 from './win32' import * as Linux from './linux' +import { pathExists as pathExistsLinux } from '../helpers/linux' import { IFoundShell } from './found-shell' import { ShellError } from './error' @@ -72,6 +73,23 @@ export async function findShellOrDefault(shell: Shell): Promise { } } +/** + * Use a platform-specific pathExists based on the platform, to simplify changes + * to the application logic + * + * @param path the location of some program on disk + * + * @returns `true` if the path exists on disk, or `false` otherwise + * + */ +function pathExists(path: string) { + if (__LINUX__) { + return pathExistsLinux(path) + } else { + return pathExistsDefault(path) + } +} + /** Launch the given shell at the path. */ export async function launchShell( shell: FoundShell, diff --git a/app/test/unit/helpers/linux-test.ts b/app/test/unit/helpers/linux-test.ts new file mode 100644 index 00000000000..df18564e441 --- /dev/null +++ b/app/test/unit/helpers/linux-test.ts @@ -0,0 +1,30 @@ +import { convertToFlatpakPath } from '../../../src/lib/helpers/linux' + +describe('convertToFlatpakPath()', () => { + if (__LINUX__) { + it('converts /usr paths', () => { + const path = '/usr/bin/subl' + const expectedPath = '/var/run/host/usr/bin/subl' + expect(convertToFlatpakPath(path)).toEqual(expectedPath) + }) + + it('preserves /opt paths', () => { + const path = '/opt/slickedit-pro2018/bin/vs' + expect(convertToFlatpakPath(path)).toEqual(path) + }) + } + + if (__WIN32__) { + it('returns same path', () => { + const path = 'C:\\Windows\\System32\\Notepad.exe' + expect(convertToFlatpakPath(path)).toEqual(path) + }) + } + + if (__DARWIN__) { + it('returns same path', () => { + const path = '/usr/local/bin/code' + expect(convertToFlatpakPath(path)).toEqual(path) + }) + } +})