Skip to content
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
- Fixes an issue where the suggested redeploy command for Firebase Functions was incorrect for names with dashes.
- Adds a the `--export-on-exit` flag to `emulators:start` and `emulators:exec` to automatically export emulator data on command exit (#2224)
2 changes: 2 additions & 0 deletions src/commands/emulators-exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { Command } from "../command";
import * as commandUtils from "../emulator/commandUtils";

module.exports = new Command("emulators:exec <script>")
.before(commandUtils.setExportOnExitOptions)
.before(commandUtils.beforeEmulatorCommand)
.description(
"start the local Firebase emulators, " + "run a test script, then shut down the emulators"
)
.option(commandUtils.FLAG_ONLY, commandUtils.DESC_ONLY)
.option(commandUtils.FLAG_INSPECT_FUNCTIONS, commandUtils.DESC_INSPECT_FUNCTIONS)
.option(commandUtils.FLAG_IMPORT, commandUtils.DESC_IMPORT)
.option(commandUtils.FLAG_EXPORT_ON_EXIT, commandUtils.DESC_EXPORT_ON_EXIT)
.action(commandUtils.emulatorExec);
93 changes: 2 additions & 91 deletions src/commands/emulators-export.ts
Original file line number Diff line number Diff line change
@@ -1,98 +1,9 @@
import * as clc from "cli-color";
import * as fs from "fs";
import * as path from "path";
import * as rimraf from "rimraf";

import { Command } from "../command";
import * as controller from "../emulator/controller";
import * as commandUtils from "../emulator/commandUtils";
import * as utils from "../utils";
import { EmulatorHub } from "../emulator/hub";
import { FirebaseError } from "../error";
import { HubExport } from "../emulator/hubExport";
import { promptOnce } from "../prompt";
import { EmulatorHubClient } from "../emulator/hubClient";

module.exports = new Command("emulators:export <path>")
.description("export data from running emulators")
.option(commandUtils.FLAG_ONLY, commandUtils.DESC_ONLY)
.option("--force", "Overwrite any export data in the target directory.")
.action(async (exportPath: string, options: any) => {
const projectId = options.project;
if (!projectId) {
throw new FirebaseError(
"Could not determine project ID, make sure you're running in a Firebase project directory or add the --project flag.",
{ exit: 1 }
);
}

const hubClient = new EmulatorHubClient(projectId);
if (!hubClient.foundHub()) {
throw new FirebaseError(
`Did not find any running emulators for project ${clc.bold(projectId)}.`,
{ exit: 1 }
);
}

try {
await hubClient.getStatus();
} catch (e) {
const filePath = EmulatorHub.getLocatorFilePath(projectId);
throw new FirebaseError(
`The emulator hub for ${projectId} did not respond to a status check. If this error continues try shutting down all running emulators and deleting the file ${filePath}`,
{ exit: 1 }
);
}

utils.logBullet(
`Found running emulator hub for project ${clc.bold(projectId)} at ${hubClient.origin}`
);

// If the export target directory does not exist, we should attempt to create it
const exportAbsPath = path.resolve(exportPath);
if (!fs.existsSync(exportAbsPath)) {
utils.logBullet(`Creating export directory ${exportAbsPath}`);
fs.mkdirSync(exportAbsPath);
}

// Check if there is already an export there and prompt the user about deleting it
const existingMetadata = HubExport.readMetadata(exportAbsPath);
if (existingMetadata && !options.force) {
if (options.noninteractive) {
throw new FirebaseError(
"Export already exists in the target directory, re-run with --force to overwrite.",
{ exit: 1 }
);
}

const prompt = await promptOnce({
type: "confirm",
message: `The directory ${exportAbsPath} already contains export data. Exporting again to the same directory will overwrite all data. Do you want to continue?`,
default: false,
});

if (!prompt) {
throw new FirebaseError("Command aborted", { exit: 1 });
}
}

// Remove all existing data (metadata.json will be overwritten automatically)
if (existingMetadata) {
if (existingMetadata.firestore) {
const firestorePath = path.join(exportAbsPath, existingMetadata.firestore.path);
utils.logBullet(`Deleting directory ${firestorePath}`);
rimraf.sync(firestorePath);
}
}

utils.logBullet(`Exporting data to: ${exportAbsPath}`);
try {
await hubClient.postExport(exportAbsPath);
} catch (e) {
throw new FirebaseError("Export request failed, see emulator logs for more information.", {
exit: 1,
original: e,
});
}

utils.logSuccess("Export complete");
});
.action(controller.exportEmulatorData);
4 changes: 3 additions & 1 deletion src/commands/emulators-start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ function stylizeLink(url: String) {
}

module.exports = new Command("emulators:start")
.before(commandUtils.setExportOnExitOptions)
.before(commandUtils.beforeEmulatorCommand)
.description("start the local Firebase emulators")
.option(commandUtils.FLAG_ONLY, commandUtils.DESC_ONLY)
.option(commandUtils.FLAG_INSPECT_FUNCTIONS, commandUtils.DESC_INSPECT_FUNCTIONS)
.option(commandUtils.FLAG_IMPORT, commandUtils.DESC_IMPORT)
.option(commandUtils.FLAG_EXPORT_ON_EXIT, commandUtils.DESC_EXPORT_ON_EXIT)
.action(async (options: any) => {
const killSignalPromise = commandUtils.shutdownWhenKilled();
const killSignalPromise = commandUtils.shutdownWhenKilled(options);

try {
await controller.startAll(options);
Expand Down
2 changes: 2 additions & 0 deletions src/commands/ext-dev-emulators-exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import * as optionsHelper from "../extensions/emulator/optionsHelper";

module.exports = new Command("ext:dev:emulators:exec <script>")
.description("emulate an extension, run a test script, then shut down the emulators")
.before(commandUtils.setExportOnExitOptions)
.option(commandUtils.FLAG_INSPECT_FUNCTIONS, commandUtils.DESC_INSPECT_FUNCTIONS)
.option(commandUtils.FLAG_TEST_CONFIG, commandUtils.DESC_TEST_CONFIG)
.option(commandUtils.FLAG_TEST_PARAMS, commandUtils.DESC_TEST_PARAMS)
.option(commandUtils.FLAG_IMPORT, commandUtils.DESC_IMPORT)
.option(commandUtils.FLAG_EXPORT_ON_EXIT, commandUtils.DESC_EXPORT_ON_EXIT)
.action(async (script: string, options: any) => {
const emulatorOptions = await optionsHelper.buildOptions(options);
commandUtils.beforeEmulatorCommand(emulatorOptions);
Expand Down
5 changes: 3 additions & 2 deletions src/commands/ext-dev-emulators-start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import { FirebaseError } from "../error";

module.exports = new Command("ext:dev:emulators:start")
.description("start the local Firebase extension emulator")
.before(commandUtils.setExportOnExitOptions)
.option(commandUtils.FLAG_INSPECT_FUNCTIONS, commandUtils.DESC_INSPECT_FUNCTIONS)
.option(commandUtils.FLAG_TEST_CONFIG, commandUtils.DESC_TEST_CONFIG)
.option(commandUtils.FLAG_TEST_PARAMS, commandUtils.DESC_TEST_PARAMS)
.option(commandUtils.FLAG_IMPORT, commandUtils.DESC_IMPORT)
.option(commandUtils.FLAG_EXPORT_ON_EXIT, commandUtils.DESC_EXPORT_ON_EXIT)
.action(async (options: any) => {
const killSignalPromise = commandUtils.shutdownWhenKilled();

const killSignalPromise = commandUtils.shutdownWhenKilled(options);
const emulatorOptions = await optionsHelper.buildOptions(options);
try {
commandUtils.beforeEmulatorCommand(emulatorOptions);
Expand Down
140 changes: 130 additions & 10 deletions src/emulator/commandUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,22 @@ import * as controller from "../emulator/controller";
import * as Config from "../config";
import * as utils from "../utils";
import * as logger from "../logger";
import * as path from "path";
import { Constants } from "./constants";
import { requireAuth } from "../requireAuth";
import requireConfig = require("../requireConfig");
import { Emulators, ALL_SERVICE_EMULATORS } from "../emulator/types";
import { Emulators, ALL_SERVICE_EMULATORS } from "./types";
import { FirebaseError } from "../error";
import { EmulatorRegistry } from "../emulator/registry";
import { FirestoreEmulator } from "../emulator/firestoreEmulator";
import { EmulatorRegistry } from "./registry";
import { FirestoreEmulator } from "./firestoreEmulator";
import * as getProjectId from "../getProjectId";
import { prompt } from "../prompt";
import { EmulatorHub } from "./hub";
import { onExit } from "./controller";
import * as fsutils from "../fsutils";
import Signals = NodeJS.Signals;
import SignalsListener = NodeJS.SignalsListener;
import Table = require("cli-table");

export const FLAG_ONLY = "--only <emulators>";
export const DESC_ONLY =
Expand All @@ -30,6 +36,16 @@ export const DESC_INSPECT_FUNCTIONS =
export const FLAG_IMPORT = "--import [dir]";
export const DESC_IMPORT = "import emulator data from a previous export (see emulators:export)";

export const FLAG_EXPORT_ON_EXIT_NAME = "--export-on-exit";
export const FLAG_EXPORT_ON_EXIT = `${FLAG_EXPORT_ON_EXIT_NAME} [dir]`;
export const DESC_EXPORT_ON_EXIT =
"automatically export emulator data (emulators:export) " +
"when the emulators make a clean exit (SIGINT), " +
`when no dir is provided the location of ${FLAG_IMPORT} is used`;
export const EXPORT_ON_EXIT_USAGE_ERROR =
`"${FLAG_EXPORT_ON_EXIT_NAME}" must be used with "${FLAG_IMPORT}"` +
` or provide a dir directly to "${FLAG_EXPORT_ON_EXIT}"`;

// Flags for the ext:dev:emulators:* commands
export const FLAG_TEST_CONFIG = "--test-config <firebase.json file>";
export const DESC_TEST_CONFIG =
Expand Down Expand Up @@ -156,13 +172,116 @@ export function parseInspectionPort(options: any): number {
return parsed;
}

export function shutdownWhenKilled(): Promise<void> {
/**
* Sets the correct export options based on --import and --export-on-exit. Mutates the options object.
* Also validates if we have a correct setting we need to export the data on exit.
* When used as: `--import ./data --export-on-exit` or `--import ./data --export-on-exit ./data`
* we do allow an non-existing --import [dir] and we just export-on-exit. This because else one would always need to
* export data the first time they start developing on a clean project.
* @param options
*/
export function setExportOnExitOptions(options: any) {
if (options.exportOnExit || typeof options.exportOnExit === "string") {
// note that options.exportOnExit may be a bool when used as a flag without a [dir] argument:
// --import ./data --export-on-exit
if (options.import) {
options.exportOnExit =
typeof options.exportOnExit === "string" ? options.exportOnExit : options.import;

const importPath = path.resolve(options.import);
if (!fsutils.dirExistsSync(importPath) && options.import === options.exportOnExit) {
// --import path does not exist and is the same as --export-on-exit, let's not import and only --export-on-exit
options.exportOnExit = options.import;
delete options.import;
}
}

if (options.exportOnExit === true || !options.exportOnExit) {
// might be true when only used as a flag without --import [dir]
// options.exportOnExit might be an empty string when used as:
// firebase emulators:start --debug --import '' --export-on-exit ''
throw new FirebaseError(EXPORT_ON_EXIT_USAGE_ERROR);
}
}
return;
}

function processKillSignal(
signal: Signals,
res: (value?: unknown) => void,
rej: (value?: unknown) => void,
options: any
): SignalsListener {
let signalCount = 0;
return async () => {
try {
signalCount = signalCount + 1;
const signalDisplay = signal === "SIGINT" ? `SIGINT (Ctrl-C)` : signal;
logger.debug(`Received signal ${signalDisplay} ${signalCount}`);
logger.info(" "); // to not indent the log with the possible Ctrl-C char
if (signalCount === 1) {
utils.logLabeledBullet(
"emulators",
`Received ${signalDisplay} for the first time. Starting a clean shutdown.`
);
utils.logLabeledBullet(
"emulators",
`Please wait for a clean shutdown or send the ${signalDisplay} signal again to stop right now.`
);
// in case of a double 'Ctrl-C' we do not want to cleanly exit with onExit/cleanShutdown
await onExit(options);
await controller.cleanShutdown();
} else {
logger.debug(`Skipping clean onExit() and cleanShutdown()`);
const runningEmulatorsInfosWithPid = EmulatorRegistry.listRunningWithInfo().filter((i) =>
Boolean(i.pid)
);

utils.logLabeledWarning(
"emulators",
`Received ${signalDisplay} ${signalCount} times. You have forced the Emulator Suite to exit without waiting for ${
runningEmulatorsInfosWithPid.length
} subprocess${
runningEmulatorsInfosWithPid.length > 1 ? "es" : ""
} to finish. These processes ${clc.bold("may")} still be running on your machine: `
);

const pids: number[] = [];

const emulatorsTable = new Table({
head: ["Emulator", "Host:Port", "PID"],
style: {
head: ["yellow"],
},
});

for (const emulatorInfo of runningEmulatorsInfosWithPid) {
pids.push(emulatorInfo.pid as number);
emulatorsTable.push([
Constants.description(emulatorInfo.name),
`${emulatorInfo.host}:${emulatorInfo.port}`,
emulatorInfo.pid,
]);
}
logger.info(`\n${emulatorsTable}\n\nTo force them to exit run:\n`);
if (process.platform === "win32") {
logger.info(clc.bold(`TASKKILL ${pids.map((pid) => "/PID " + pid).join(" ")} /T\n`));
} else {
logger.info(clc.bold(`kill ${pids.join(" ")}\n`));
}
}
res();
} catch (e) {
logger.debug(e);
rej();
}
};
}

export function shutdownWhenKilled(options: any): Promise<void> {
return new Promise((res, rej) => {
process.on("SIGINT", () => {
controller
.cleanShutdown()
.then(res)
.catch(rej);
["SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT"].forEach((signal: string) => {
process.on(signal as Signals, processKillSignal(signal as Signals, res, rej, options));
});
})
.then(() => {
Expand Down Expand Up @@ -251,7 +370,7 @@ async function runScript(script: string, extraEnv: Record<string, string>): Prom
/** The action function for emulators:exec and ext:dev:emulators:exec.
* Starts the appropriate emulators, executes the provided script,
* and then exits.
* @param script: A script to run after starting the emualtors.
* @param script: A script to run after starting the emulators.
* @param options: A Commander options object.
*/
export async function emulatorExec(script: string, options: any) {
Expand All @@ -264,6 +383,7 @@ export async function emulatorExec(script: string, options: any) {
try {
await controller.startAll(options, /* noUi = */ true);
exitCode = await runScript(script, extraEnv);
await onExit(options);
} finally {
await controller.cleanShutdown();
}
Expand Down
Loading