addons/isl-server/src/serverPlatform.tsblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import type {
b69ab319 AbsolutePath,
b69ab3110 PlatformSpecificClientToServerMessages,
b69ab3111 RepoRelativePath,
b69ab3112 ServerToClientMessage,
b69ab3113} from 'isl/src/types';
b69ab3114import type {Repository} from './Repository';
b69ab3115import type {RepositoryContext} from './serverTypes';
b69ab3116
b69ab3117import {spawn} from 'node:child_process';
b69ab3118import pathModule from 'node:path';
b69ab3119import {nullthrows} from 'shared/utils';
b69ab3120
b69ab3121/**
b69ab3122 * Platform-specific server-side API for each target: vscode extension host, electron standalone, browser, ...
b69ab3123 * See also platform.ts
b69ab3124 */
b69ab3125export interface ServerPlatform {
b69ab3126 platformName: string;
b69ab3127 /** Override the analytics Session ID. Should be globally unique. */
b69ab3128 sessionId?: string;
b69ab3129 handleMessageFromClient(
b69ab3130 this: ServerPlatform,
b69ab3131 repo: Repository | undefined,
b69ab3132 ctx: RepositoryContext,
b69ab3133 message: PlatformSpecificClientToServerMessages,
b69ab3134 postMessage: (message: ServerToClientMessage) => void,
b69ab3135 onDispose: (disapose: () => unknown) => void,
b69ab3136 ): void | Promise<void>;
b69ab3137}
b69ab3138
b69ab3139export const browserServerPlatform: ServerPlatform = {
b69ab3140 platformName: 'browser',
b69ab3141 handleMessageFromClient(
b69ab3142 this: ServerPlatform,
b69ab3143 repo: Repository | undefined,
b69ab3144 ctx: RepositoryContext,
b69ab3145 message: PlatformSpecificClientToServerMessages,
b69ab3146 ) {
b69ab3147 switch (message.type) {
b69ab3148 case 'platform/openContainingFolder': {
b69ab3149 const absPath: AbsolutePath = pathModule.join(
b69ab3150 nullthrows(repo?.info.repoRoot),
b69ab3151 message.path,
b69ab3152 );
b69ab3153 let args: Array<string> = [];
b69ab3154 // use OS-builtin open command to open parent directory
b69ab3155 // (which may open different file extensions with different programs)
b69ab3156 switch (process.platform) {
b69ab3157 case 'darwin':
b69ab3158 args = ['/usr/bin/open', pathModule.dirname(absPath)];
b69ab3159 break;
b69ab3160 case 'win32':
b69ab3161 // On windows, we can select the file in the newly opened explorer window by giving the full path
b69ab3162 args = ['explorer.exe', '/select,', absPath];
b69ab3163 break;
b69ab3164 case 'linux':
b69ab3165 args = ['xdg-open', pathModule.dirname(absPath)];
b69ab3166 break;
b69ab3167 }
b69ab3168 repo?.initialConnectionContext.logger.log('open file', absPath);
b69ab3169 if (args.length > 0) {
b69ab3170 spawnInBackground(repo, args);
b69ab3171 }
b69ab3172 break;
b69ab3173 }
b69ab3174 case 'platform/openFile': {
b69ab3175 openFile(repo, ctx, message.path);
b69ab3176 break;
b69ab3177 }
b69ab3178 case 'platform/openFiles': {
b69ab3179 for (const path of message.paths) {
b69ab3180 openFile(repo, ctx, path);
b69ab3181 }
b69ab3182 break;
b69ab3183 }
b69ab3184 }
b69ab3185 },
b69ab3186};
b69ab3187
b69ab3188async function openFile(
b69ab3189 repo: Repository | undefined,
b69ab3190 ctx: RepositoryContext | undefined,
b69ab3191 path: RepoRelativePath,
b69ab3192) {
b69ab3193 if (repo == null || ctx == null) {
b69ab3194 return;
b69ab3195 }
b69ab3196 const opener = await repo.getConfig(ctx, 'isl.open-file-cmd');
b69ab3197 const absPath: AbsolutePath = pathModule.join(repo.info.repoRoot, path);
b69ab3198 let args: Array<string> = [];
b69ab3199 if (opener) {
b69ab31100 // opener should be either a JSON string (wrapped in quotes) or a JSON array of strings,
b69ab31101 // to include arguments
b69ab31102 try {
b69ab31103 const jsonOpenerArgs = JSON.parse(opener);
b69ab31104 args = Array.isArray(jsonOpenerArgs)
b69ab31105 ? [...jsonOpenerArgs, absPath]
b69ab31106 : [jsonOpenerArgs, absPath];
b69ab31107 } catch {
b69ab31108 // if it's not JSON, it should be a regular string
b69ab31109 args = [opener, absPath];
b69ab31110 }
b69ab31111 } else {
b69ab31112 // by default, use OS-builtin open command to open files
b69ab31113 // (which may open different file extensions with different programs)
b69ab31114 switch (process.platform) {
b69ab31115 case 'darwin':
b69ab31116 args = ['/usr/bin/open', absPath];
b69ab31117 break;
b69ab31118 case 'win32':
b69ab31119 args = ['notepad.exe', absPath];
b69ab31120 break;
b69ab31121 case 'linux':
b69ab31122 args = ['xdg-open', absPath];
b69ab31123 break;
b69ab31124 }
b69ab31125 }
b69ab31126 repo.initialConnectionContext.logger.log('open file', absPath);
b69ab31127 if (args.length > 0) {
b69ab31128 spawnInBackground(repo, args);
b69ab31129 }
b69ab31130}
b69ab31131
b69ab31132/**
b69ab31133 * Because the ISL server is likely running in the background and is
b69ab31134 * no longer attached to a terminal, this is designed for the case
b69ab31135 * where the user opens the file in a windowed editor (hence
b69ab31136 * `windowsHide: false`, which is the default for
b69ab31137 * `child_process.spawn()`, but not for `execa()`):
b69ab31138 *
b69ab31139 * - For users using a simple one-window-per-file graphical text
b69ab31140 * editor, like notepad.exe, this is relatively straightforward.
b69ab31141 * - For users who prefer a terminal-based editor, like Emacs,
b69ab31142 * a conduit like EmacsClient would be required.
b69ab31143 *
b69ab31144 * Further, killing ISL should not kill the editor, so this follows
b69ab31145 * the pattern for spawning an independent, long-running process in
b69ab31146 * Node.js as described here:
b69ab31147 *
b69ab31148 * https://nodejs.org/docs/latest-v10.x/api/child_process.html#child_process_options_detached
b69ab31149 */
b69ab31150function spawnInBackground(repo: Repository | undefined, args: Array<string>) {
b69ab31151 // TODO: Report error if spawn() fails?
b69ab31152 // TODO: support passing the column/line number to programs that support it? e.g. vscode: `code /path/to/file:10:20`
b69ab31153 const proc = spawn(args[0], args.slice(1), {
b69ab31154 detached: true,
b69ab31155 stdio: 'ignore',
b69ab31156 windowsHide: false,
b69ab31157 windowsVerbatimArguments: true,
b69ab31158 });
b69ab31159 // Silent error. Don't crash the server process.
b69ab31160 proc.on('error', err => {
b69ab31161 repo?.initialConnectionContext.logger.log('failed to open', args, err);
b69ab31162 });
b69ab31163 proc.unref();
b69ab31164}