| 1 | /** |
| 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. |
| 3 | * |
| 4 | * This source code is licensed under the MIT license found in the |
| 5 | * LICENSE file in the root directory of this source tree. |
| 6 | */ |
| 7 | |
| 8 | import fs from 'node:fs'; |
| 9 | import os from 'node:os'; |
| 10 | import path from 'node:path'; |
| 11 | import {nullthrows} from 'shared/utils'; |
| 12 | import rmtree from './rmtree'; |
| 13 | |
| 14 | export type ExistingServerInfo = { |
| 15 | sensitiveToken: string; |
| 16 | challengeToken: string; |
| 17 | logFileLocation: string; |
| 18 | /** Which command name was used to launch this server instance, |
| 19 | * so it can be propagated to run further sl commands by the server. |
| 20 | * Usually, "sl". */ |
| 21 | command: string; |
| 22 | /** |
| 23 | * `sl version` string. If the version of sl changes, we shouldn't reuse that server instance, |
| 24 | * due to potential incompatibilities between the old running server javascript and the new client javascript. |
| 25 | */ |
| 26 | slVersion: string; |
| 27 | }; |
| 28 | |
| 29 | const cacheDir = |
| 30 | process.platform == 'win32' |
| 31 | ? path.join(nullthrows(process.env.LOCALAPPDATA), 'cache') |
| 32 | : process.platform == 'darwin' |
| 33 | ? path.join(os.homedir(), 'Library/Caches') |
| 34 | : process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache'); |
| 35 | |
| 36 | /** |
| 37 | * Per-user cache dir with restrictive permissions. |
| 38 | * Inside this folder will be a number of files, one per port for an active ISL server. |
| 39 | */ |
| 40 | const savedActiveServerUrlsDirectory = path.join(cacheDir, 'sapling-isl'); |
| 41 | |
| 42 | function fileNameForPort(port: number): string { |
| 43 | return `reusable_server_${port}`; |
| 44 | } |
| 45 | |
| 46 | function isMode700(stat: fs.Stats): boolean { |
| 47 | // eslint-disable-next-line no-bitwise |
| 48 | return (stat.mode & 0o777) === 0o700; |
| 49 | } |
| 50 | |
| 51 | /** |
| 52 | * Make a temp directory with restrictive permissions where we can write existing server information. |
| 53 | * Ensures directory has proper restrictive mode if the directory already exists. |
| 54 | */ |
| 55 | export async function ensureExistingServerFolder(): Promise<void> { |
| 56 | await fs.promises.mkdir(savedActiveServerUrlsDirectory, { |
| 57 | // directory needs rwx |
| 58 | mode: 0o700, |
| 59 | recursive: true, |
| 60 | }); |
| 61 | |
| 62 | const stat = await fs.promises.stat(savedActiveServerUrlsDirectory); |
| 63 | if (process.platform !== 'win32' && !isMode700(stat)) { |
| 64 | throw new Error( |
| 65 | `active servers folder ${savedActiveServerUrlsDirectory} has the wrong permissions: ${stat.mode}`, |
| 66 | ); |
| 67 | } |
| 68 | if (stat.isSymbolicLink()) { |
| 69 | throw new Error(`active servers folder ${savedActiveServerUrlsDirectory} is a symlink`); |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | export function deleteExistingServerFile(port: number): Promise<void> { |
| 74 | const folder = path.join(savedActiveServerUrlsDirectory, fileNameForPort(port)); |
| 75 | if (typeof fs.promises.rm === 'function') { |
| 76 | return fs.promises.rm(folder, {force: true}); |
| 77 | } else { |
| 78 | return rmtree(folder); |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | export async function writeExistingServerFile( |
| 83 | port: number, |
| 84 | data: ExistingServerInfo, |
| 85 | ): Promise<void> { |
| 86 | await fs.promises.writeFile( |
| 87 | path.join(savedActiveServerUrlsDirectory, fileNameForPort(port)), |
| 88 | JSON.stringify(data), |
| 89 | {encoding: 'utf-8', flag: 'w', mode: 0o600}, |
| 90 | ); |
| 91 | } |
| 92 | |
| 93 | export async function readExistingServerFile(port: number): Promise<ExistingServerInfo> { |
| 94 | // TODO: do we need to verify the permissions of this file? |
| 95 | const data: string = await fs.promises.readFile( |
| 96 | path.join(savedActiveServerUrlsDirectory, fileNameForPort(port)), |
| 97 | {encoding: 'utf-8', flag: 'r'}, |
| 98 | ); |
| 99 | return JSON.parse(data) as ExistingServerInfo; |
| 100 | } |
| 101 | |