1import { execFileSync, execSync, ExecSyncOptionsWithStringEncoding } from 'child_process';
2import * as path from 'path';
3
4const debug = require('debug')('expo:utils:getRunningProcess') as typeof console.log;
5
6const defaultOptions: ExecSyncOptionsWithStringEncoding = {
7  encoding: 'utf8',
8  stdio: ['pipe', 'pipe', 'ignore'],
9};
10
11/** Returns a pid value for a running port like `63828` or null if nothing is running on the given port. */
12export function getPID(port: number): number | null {
13  try {
14    const results = execFileSync('lsof', [`-i:${port}`, '-P', '-t', '-sTCP:LISTEN'], defaultOptions)
15      .split('\n')[0]
16      .trim();
17    const pid = Number(results);
18    debug(`pid: ${pid} for port: ${port}`);
19    return pid;
20  } catch (error: any) {
21    debug(`No pid found for port: ${port}. Error: ${error}`);
22    return null;
23  }
24}
25
26/** Get `package.json` `name` field for a given directory. Returns `null` if none exist. */
27function getPackageName(packageRoot: string): string | null {
28  const packageJson = path.join(packageRoot, 'package.json');
29  try {
30    return require(packageJson).name || null;
31  } catch {
32    return null;
33  }
34}
35
36/** Returns a command like `node /Users/evanbacon/.../bin/expo start` or the package.json name. */
37function getProcessCommand(pid: number, procDirectory: string): string {
38  const name = getPackageName(procDirectory);
39
40  if (name) {
41    return name;
42  }
43  return execSync(`ps -o command -p ${pid} | sed -n 2p`, defaultOptions).replace(/\n$/, '').trim();
44}
45
46/** Get directory for a given process ID. */
47export function getDirectoryOfProcessById(processId: number): string {
48  return execSync(
49    `lsof -p ${processId} | awk '$4=="cwd" {for (i=9; i<=NF; i++) printf "%s ", $i}'`,
50    defaultOptions
51  ).trim();
52}
53
54/** Get information about a running process given a port. Returns null if no process is running on the given port. */
55export function getRunningProcess(port: number): {
56  /** The PID value for the port. */
57  pid: number;
58  /** Get the directory for the running process. */
59  directory: string;
60  /** The command running the process like `node /Users/evanbacon/.../bin/expo start` or the `package.json` name like `my-app`. */
61  command: string;
62} | null {
63  // 63828
64  const pid = getPID(port);
65  if (!pid) {
66    return null;
67  }
68
69  try {
70    // /Users/evanbacon/Documents/GitHub/lab/myapp
71    const directory = getDirectoryOfProcessById(pid);
72    // /Users/evanbacon/Documents/GitHub/lab/myapp/package.json
73    const command = getProcessCommand(pid, directory);
74    // TODO: Have a better message for reusing another process.
75    return { pid, directory, command };
76  } catch {
77    return null;
78  }
79}
80