UNPKG

@clickup/pg-microsharding

Version:
151 lines (135 loc) 4.73 kB
import { execSync } from "child_process"; import { writeFileSync } from "fs"; import compact from "lodash/compact"; import { fileSync, setGracefulCleanup } from "tmp"; import { TMP_DIR, print } from "./logging"; import { shellQuote } from "./quote"; const TMUX_SESSION = "pg-microsharding-tmux"; const ACTIVE_BG = "#000033"; // We do not want to pass the following psql envs through tmux session, because // the data in them is already included in the DSNs in the command line. const EXCLUDE_PG_ENV = [ "PGUSER", "PGPASSWORD", "PGHOST", "PGPORT", "PGDATABASE", "PGDSNS", ]; /** * Returns true if this script runs inside a TMUX session. */ export function isInTmuxSession(): boolean { return !!process.env["TMUX"]; } /** * If there is already an active TMUX session, reattaches to it. We need to keep * the strong notion of singleton for all long-running microsharding actions, so * that e.g. if we run `pg-microsharding rebalance` in one terminal, and then * run it again in another terminal, the second run will attach to the first * one. */ export async function tryReattachToTmuxSession(): Promise<void> { if (isInTmuxSession()) { return; } try { execSync("which tmux", { stdio: "ignore" }); } catch { throw "TMUX is required to run this action; please install it"; } // Try to reattach to an existing session instead of starting a new one (in // case we e.g. got disconnected). try { execSync(`tmux attach-session -d -t ${TMUX_SESSION} 2>/dev/null`, { stdio: "inherit", }); process.exit(0); } catch { // No such session yet. } } /** * Opens M TMUX panes, in each run N commands sequentially (panes[M][N]). If a * single command fails, then fails the whole pane. * * In case we already run it from a TMUX session, just runs all the commands * sequentially. */ export async function runInTmux( panes: Array<Array<{ key: string; title: string; command: string[] }>>, ): Promise<void> { if (isInTmuxSession()) { for (const { title, command } of panes.flat()) { print.section(`\n##\n## ${title}\n##\n`); execSync(command.map((v) => shellQuote(v)).join(" "), { stdio: "inherit", }); } return; } setGracefulCleanup(); const tmuxCommands: string[] = ["set -e"]; for (const [i, sequence] of panes.entries()) { const commands: string[] = [ "set -e", String.raw`trap 'test $? -eq 0 && printf "\033[1;32mDONE. Press ^C to close this pane..." || printf "\033[0;31mFAILURE. Press ^C to close this pane..."; sleep 100000' EXIT`, ]; for (const [i, { title, command }] of sequence.entries()) { const progress = `${i + 1} of ${sequence.length}`; commands.push( `printf '\\033]2;%s\\033\\' ' ${progress} | ${title} '`, // Pass env vars like PGOPTIONS, PGSSLCERT etc. through tmux. ...compact( Object.entries(process.env).map( ([k, v]) => v && k.match(/^PG[A-Z][A-Z_]*$/) && !EXCLUDE_PG_ENV.includes(k) && `export ${k}=${shellQuote(v)}`, ), ), command.map(shellQuote).join(" "), "echo", ); } // TMUX has a limit on the command total length: // https://github.com/tmux/tmux/issues/254. So instead of passing the big // script, we put this script in a temporary file and then run it using the // `bash this-temp-file` plain command via TMUX. Notice that the temporary // files are auto-removed, but if the script is terminated cruelly with e.g. // -9 signal, they may linger; it's not a big deal since they are cleaned up // by linux tmpwatch anyway, we just don't have a better option. const tmpFile = fileSync({ tmpdir: TMP_DIR, prefix: "tmux-" + sequence .map(({ key }) => key) .slice(0, 10) .join("-"), }).name; writeFileSync(tmpFile, commands.join("\n")); if (i === 0) { tmuxCommands.push( `tmux new-session -s ${TMUX_SESSION} -d bash ${shellQuote(tmpFile)}`, ); } else { tmuxCommands.push( `tmux split-window -f bash ${shellQuote(tmpFile)}`, "tmux select-layout tiled", ); } } tmuxCommands.push( 'tmux bind -n "C-c" confirm-before -p "Confirm SIGINT? (y/n)" "send-keys C-c"', `tmux set -g window-active-style "bg=${ACTIVE_BG}"`, `tmux set -g pane-active-border-style "bg=${ACTIVE_BG}"`, "tmux set mouse on", "tmux set pane-border-status top", 'tmux set pane-border-format "#T"', "tmux select-layout tiled", `tmux attach-session -d -t ${TMUX_SESSION}`, ); execSync(tmuxCommands.join("\n"), { stdio: "inherit" }); }