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