From eb6ce3ecf98a28e8ea9ef79fb392a290dbdb156b Mon Sep 17 00:00:00 2001 From: Cheng Liu Date: Mon, 15 Apr 2024 15:23:59 -0700 Subject: [PATCH] feat: add graceful shutdown to ensure consistent git configs --- apps/sparo-lib/src/cli/SparoCICommandLine.ts | 5 +++ apps/sparo-lib/src/cli/SparoCommandLine.ts | 5 +++ apps/sparo-lib/src/cli/commands/fetch.ts | 38 +++++++++++++----- .../src/services/GracefulShutdownService.ts | 40 +++++++++++++++++++ 4 files changed, 78 insertions(+), 10 deletions(-) create mode 100644 apps/sparo-lib/src/services/GracefulShutdownService.ts diff --git a/apps/sparo-lib/src/cli/SparoCICommandLine.ts b/apps/sparo-lib/src/cli/SparoCICommandLine.ts index 833d885..6079fcf 100644 --- a/apps/sparo-lib/src/cli/SparoCICommandLine.ts +++ b/apps/sparo-lib/src/cli/SparoCICommandLine.ts @@ -7,6 +7,7 @@ import { ArgvService } from '../services/ArgvService'; import { GitVersionCompatibility } from '../logic/GitVersionCompatibility'; import { TelemetryService } from '../services/TelemetryService'; import { GitSparseCheckoutService } from '../services/GitSparseCheckoutService'; +import { GracefulShutdownService } from '../services/GracefulShutdownService'; import { getCommandName } from './commands/util'; import { SparoStartupBanner } from './SparoStartupBanner'; import type { ILaunchOptions } from '../api/Sparo'; @@ -51,6 +52,10 @@ export class SparoCICommandLine { this._commandsMap.add(getCommandName(cmdInstance.cmd)); }) ); + + const gracefulShutdownService: GracefulShutdownService = + await getFromContainerAsync(GracefulShutdownService); + gracefulShutdownService.setup(); } public async runAsync(): Promise { diff --git a/apps/sparo-lib/src/cli/SparoCommandLine.ts b/apps/sparo-lib/src/cli/SparoCommandLine.ts index 1203bbc..530c78b 100644 --- a/apps/sparo-lib/src/cli/SparoCommandLine.ts +++ b/apps/sparo-lib/src/cli/SparoCommandLine.ts @@ -8,6 +8,7 @@ import { ICommand } from './commands/base'; import { GitVersionCompatibility } from '../logic/GitVersionCompatibility'; import { TelemetryService } from '../services/TelemetryService'; import { GitSparseCheckoutService } from '../services/GitSparseCheckoutService'; +import { GracefulShutdownService } from '../services/GracefulShutdownService'; import { getCommandName } from './commands/util'; import { SparoStartupBanner } from './SparoStartupBanner'; import type { ILaunchOptions } from '../api/Sparo'; @@ -52,6 +53,10 @@ export class SparoCommandLine { this._commandsMap.add(getCommandName(cmdInstance.cmd)); }) ); + + const gracefulShutdownService: GracefulShutdownService = + await getFromContainerAsync(GracefulShutdownService); + gracefulShutdownService.setup(); } public async runAsync(): Promise { diff --git a/apps/sparo-lib/src/cli/commands/fetch.ts b/apps/sparo-lib/src/cli/commands/fetch.ts index 8cf85bd..a4d3176 100644 --- a/apps/sparo-lib/src/cli/commands/fetch.ts +++ b/apps/sparo-lib/src/cli/commands/fetch.ts @@ -1,6 +1,7 @@ import { inject } from 'inversify'; import { Command } from '../../decorator'; import { GitService } from '../../services/GitService'; +import { GracefulShutdownService } from '../../services/GracefulShutdownService'; import type { Argv, ArgumentsCamelCase } from 'yargs'; import type { GitRepoInfo } from 'git-repo-info'; @@ -19,6 +20,7 @@ export class FetchCommand implements ICommand { public description: string = 'fetch remote branch to local'; @inject(GitService) private _gitService!: GitService; + @inject(GracefulShutdownService) private _gracefulShutdownService!: GracefulShutdownService; public builder(yargs: Argv<{}>): void { /** * sparo fetch [--all] @@ -44,14 +46,10 @@ export class FetchCommand implements ICommand { const { all, branch = defaultBranch, remote = this._gitService.getBranchRemote(branch) } = args; const fetchArgs: string[] = ['fetch']; - let remoteFetchGitConfig: string[] | undefined; + let restoreSingleBranchCallback: (() => void) | undefined; if (all) { - // Temporary revert single branch fetch - const currentRemoteFetchGitConfig: string[] | undefined = this._getRemoteFetchGitConfig(remote); - if (currentRemoteFetchGitConfig) { - this._setAllBranchFetch(remote); - remoteFetchGitConfig = currentRemoteFetchGitConfig; - } + // Temporary revert single branch fetch if necessary + restoreSingleBranchCallback = this._revertSingleBranchIfNecessary(remote); fetchArgs.push('--all'); } else { @@ -60,15 +58,35 @@ export class FetchCommand implements ICommand { gitService.executeGitCommand({ args: fetchArgs }); - if (remoteFetchGitConfig) { - this._restoreSingleBranchFetch(remote, remoteFetchGitConfig); - } + restoreSingleBranchCallback?.(); }; public getHelp(): string { return `fetch help`; } + private _revertSingleBranchIfNecessary = (remote: string): (() => void) | undefined => { + let remoteFetchGitConfig: string[] | undefined = this._getRemoteFetchGitConfig(remote); + let callback: (() => void) | undefined; + if (remoteFetchGitConfig) { + this._setAllBranchFetch(remote); + + callback = () => { + if (remoteFetchGitConfig) { + this._restoreSingleBranchFetch(remote, remoteFetchGitConfig); + + // Avoid memory leaking + remoteFetchGitConfig = undefined; + this._gracefulShutdownService.unregisterCallback(callback); + } + }; + + this._gracefulShutdownService.registerCallback(callback); + } + + return callback; + }; + private _getRemoteFetchGitConfig(remote: string): string[] | undefined { const result: string | undefined = this._gitService.getGitConfig(`remote.${remote}.fetch`, { array: true diff --git a/apps/sparo-lib/src/services/GracefulShutdownService.ts b/apps/sparo-lib/src/services/GracefulShutdownService.ts new file mode 100644 index 0000000..8eaa19e --- /dev/null +++ b/apps/sparo-lib/src/services/GracefulShutdownService.ts @@ -0,0 +1,40 @@ +import { Service } from '../decorator'; + +type ICallback = () => void; + +/** + * Helper class for managing graceful shutdown callbacks + * + * Example: + * When running "sparo fetch --all", the command will temporarily modify git configs. + * It's essential to register a restore callback via graceful shutdown service to + * prevent inconsistent git configs status if user presses CTRL + C to terminate the + * process in the middle of running. + */ +@Service() +export class GracefulShutdownService { + private _callbacks: Set = new Set(); + + public setup = (): void => { + process.on('SIGINT', () => this._handleSignal()); + }; + + public registerCallback = (cb: ICallback): void => { + this._callbacks.add(cb); + }; + + public unregisterCallback = (cb?: ICallback): void => { + if (!cb) { + return; + } + this._callbacks.delete(cb); + }; + + private _handleSignal = (): void => { + // Keep the implementation simple to run each callbacks synchronously. + for (const cb of Array.from(this._callbacks)) { + cb(); + this.unregisterCallback(cb); + } + }; +}