Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Require Replay browser to exit before starting a new recording #496

Merged
merged 3 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/nice-seahorses-study.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"replayio": minor
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is debatable I guess.

---

Require Replay browser to exit before creating a new recording.
19 changes: 19 additions & 0 deletions packages/replayio/src/commands/record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { writeFileSync } from "fs-extra";
import { v4 as uuid } from "uuid";
import { ProcessError } from "../utils/ProcessError";
import { logAsyncOperation } from "../utils/async/logAsyncOperation";
import { getRunningProcess } from "../utils/browser/getRunningProcess";
import { launchBrowser } from "../utils/browser/launchBrowser";
import { registerCommand } from "../utils/commander/registerCommand";
import { confirm } from "../utils/confirm";
import { exitProcess } from "../utils/exitProcess";
import { getReplayPath } from "../utils/getReplayPath";
import { killProcess } from "../utils/killProcess";
import { trackEvent } from "../utils/mixpanel/trackEvent";
import { canUpload } from "../utils/recordings/canUpload";
import { getRecordings } from "../utils/recordings/getRecordings";
Expand All @@ -34,6 +36,23 @@ async function record(url: string = "about:blank") {
const processGroupId = uuid();

try {
const process = await getRunningProcess();
if (process) {
const confirmed = await confirm(
"The replay browser is already running. You'll need to close it before starting a new recording.\n\nWould you like to close it now?",
true
);
Comment on lines +41 to +44
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little bit conflicted on this proposed behavior but when thinking about it logically... yee, why wouldn't we want to help the user and do it for them if we can? It definitely sounds reasonable. So you have my 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I felt the same. I didn't intend to do it, but then I thought I probably should

if (confirmed) {
const killResult = await killProcess(process.pid);
if (!killResult) {
console.log("Something went wrong trying to close the replay browser. Please try again.");
await exitProcess(1);
}
} else {
await exitProcess(0);
}
}

await launchBrowser(url, { processGroupId, verbose });
} catch (error) {
if (error instanceof ProcessError) {
Expand Down
19 changes: 19 additions & 0 deletions packages/replayio/src/utils/browser/getRunningProcess.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import findProcess from "find-process";
import { highlight } from "../theme";
import { debug } from "./debug";
import { getBrowserPath } from "./getBrowserPath";

export async function getRunningProcess() {
const browserExecutablePath = getBrowserPath();

const processes = await findProcess("name", browserExecutablePath);
if (processes.length > 0) {
const match = processes[0];

debug(`Browser process already running at ${highlight(match.pid)}`);

return match;
}

return null;
}
89 changes: 33 additions & 56 deletions packages/replayio/src/utils/browser/launchBrowser.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import findProcess from "find-process";
import { ensureDirSync, existsSync } from "fs-extra";
import { join } from "path";
import { timeoutAfter } from "../async/timeoutAfter";
import { getReplayPath } from "../getReplayPath";
import { runtimeMetadata, runtimePath } from "../installation/config";
import { prompt } from "../prompt/prompt";
import { spawnProcess } from "../spawnProcess";
import { dim, highlight, stderrPrefix, stdoutPrefix } from "../theme";
import { dim, stderrPrefix, stdoutPrefix } from "../theme";
import { debug } from "./debug";
import { getBrowserPath } from "./getBrowserPath";

Expand Down Expand Up @@ -44,63 +42,42 @@ export async function launchBrowser(
throw new Error(`Replay browser not found at: ${browserExecutablePath}`);
}

const processes = await findProcess("name", browserExecutablePath);
if (processes.length > 0) {
const match = processes[0];
debug(
`Launching browser: ${browserExecutablePath} with args:\n`,
args.join("\n"),
"\n",
processOptions
);

debug(`Browser process already running at ${highlight(match.pid)}`);
// Wait until the user quits the browser process OR
// until the user presses a key to continue (in which case, we will kill the process)
const abortControllerForPrompt = new AbortController();

console.log(`Recording... ${dim("(quit the Replay Browser to stop recording)")}`);
const spawnDeferred = spawnProcess(browserExecutablePath, args, processOptions, {
onSpawn: () => {
if (process.stdin.isTTY) {
console.log(`Recording... ${dim("(press any key to stop recording)")}`);

// Ask the browser to open a new tab
spawnProcess(browserExecutablePath, args, processOptions);

// The best we can do in this case is to regularly poll to see when the process exits
while (true) {
await timeoutAfter(1_000);
const processes = await findProcess("name", browserExecutablePath);
if (processes.length === 0) {
break;
prompt({
signal: abortControllerForPrompt.signal,
}).then(() => {
spawnDeferred.data.kill();
});
} else {
console.log(`Recording... ${dim("(quit the Replay Browser to stop recording)")}`);
}
}
} else {
debug(
`Launching browser: ${browserExecutablePath} with args:\n`,
args.join("\n"),
"\n",
processOptions
);

// Wait until the user quits the browser process OR
// until the user presses a key to continue (in which case, we will kill the process)
const abortControllerForPrompt = new AbortController();

const spawnDeferred = spawnProcess(browserExecutablePath, args, processOptions, {
onSpawn: () => {
if (process.stdin.isTTY) {
console.log(`Recording... ${dim("(press any key to stop recording)")}`);

prompt({
signal: abortControllerForPrompt.signal,
}).then(() => {
spawnDeferred.data.kill();
});
} else {
console.log(`Recording... ${dim("(quit the Replay Browser to stop recording)")}`);
}
},
printStderr: (text: string) => {
debug(stderrPrefix("stderr"), text);
},
printStdout: (text: string) => {
debug(stdoutPrefix("stdout"), text);
},
});
},
printStderr: (text: string) => {
debug(stderrPrefix("stderr"), text);
},
printStdout: (text: string) => {
debug(stdoutPrefix("stdout"), text);
},
});

try {
await spawnDeferred.promise;
} finally {
abortControllerForPrompt.abort();
}
try {
await spawnDeferred.promise;
} finally {
abortControllerForPrompt.abort();
}
}
40 changes: 40 additions & 0 deletions packages/replayio/src/utils/killProcess.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import findProcess from "find-process";
import { kill } from "process";
import { createDeferred } from "./async/createDeferred";
import { timeoutAfter } from "./async/timeoutAfter";

export async function killProcess(
pid: number,
signal?: string | number | undefined,
options: { retryIntervalMs?: number; timeoutMs?: number } = {}
): Promise<boolean> {
const { retryIntervalMs = 100, timeoutMs = 1_000 } = options;

const deferred = createDeferred<boolean>();

let timeout: NodeJS.Timeout | undefined;

const tryToKill = async () => {
timeout = undefined;

const process = await findProcess("pid", pid);
if (process.length === 0) {
deferred.resolve(true);
} else {
kill(pid, signal);

timeout = setTimeout(tryToKill, retryIntervalMs);
}
};

tryToKill();

return Promise.race([
deferred.promise.then(() => {
clearTimeout(timeout);

return true;
}),
timeoutAfter(timeoutMs).then(() => false),
]);
}