55 ERROR_NO_VALID_SUBCOMMAND_FORMAT ,
66} from "../constants" ;
77import { EOL } from "os" ;
8+ import { AsyncLocalStorage } from "node:async_hooks" ;
89import * as _ from "lodash" ;
910import { IOptions , IOptionsTracker } from "../../declarations" ;
1011import { IErrors , IHooksService , IAnalyticsService } from "../declarations" ;
@@ -22,6 +23,10 @@ import {
2223 ISimilarCommand ,
2324} from "../definitions/commands" ;
2425
26+ interface IInProcessDispatch {
27+ commandName : string ;
28+ }
29+
2530class CommandArgumentsValidationHelper {
2631 constructor (
2732 public isValid : boolean ,
@@ -42,10 +47,19 @@ export class CommandsService
4247 }
4348
4449 private commands : ICommandData [ ] = [ ] ;
45- private inProcessDepth : number = 0 ;
50+
51+ /**
52+ * The in-process dispatches in flight, innermost last, and the one whose
53+ * async context the running code belongs to. Each dispatch primes the
54+ * options service and puts it back when it ends, which only restores the
55+ * right table when dispatches nest; a dispatch may therefore start only from
56+ * inside the innermost one in flight.
57+ */
58+ private inProcessDispatches : IInProcessDispatch [ ] = [ ] ;
59+ private dispatchContext = new AsyncLocalStorage < IInProcessDispatch > ( ) ;
4660
4761 public get isExecutingInProcess ( ) : boolean {
48- return this . inProcessDepth > 0 ;
62+ return this . inProcessDispatches . length > 0 ;
4963 }
5064
5165 constructor (
@@ -299,50 +313,49 @@ export class CommandsService
299313 // Known before the lookup, so a failure to resolve reports under the name
300314 // the caller used.
301315 let commandName = typeof reference === "string" ? reference : undefined ;
302- this . inProcessDepth ++ ;
303316 try {
304- const resolved = this . resolveReference ( reference , commandArguments ) ;
305- const command = resolved . command ;
306- commandName = resolved . commandName ;
307- commandArguments = resolved . commandArguments ;
317+ await this . dispatchInProcess ( reference , async ( dispatch ) => {
318+ const resolved = this . resolveReference ( reference , commandArguments ) ;
319+ const command = resolved . command ;
320+ commandName = dispatch . commandName = resolved . commandName ;
321+ commandArguments = resolved . commandArguments ;
322+
323+ this . commands . push ( { commandName, commandArguments } ) ;
324+ const restoreOptions = this . primeOptions ( command ) ;
325+ try {
326+ if (
327+ ! ( await this . canExecuteResolvedCommand (
328+ commandName ,
329+ commandArguments ,
330+ undefined ,
331+ command ,
332+ ) )
333+ ) {
334+ let commandWithArgs = commandName ;
335+ if ( commandArguments && commandArguments . length ) {
336+ commandWithArgs += ` ${ commandArguments . join ( " " ) } ` ;
337+ }
338+ this . $errors . failWithHelp (
339+ `Command '${ commandWithArgs } ' cannot be executed.` ,
340+ ) ;
341+ }
308342
309- this . commands . push ( { commandName, commandArguments } ) ;
310- const restoreOptions = this . primeOptions ( command ) ;
311- try {
312- if (
313- ! ( await this . canExecuteResolvedCommand (
343+ await this . runResolvedCommandInProcess (
344+ command ,
314345 commandName ,
315346 commandArguments ,
316- undefined ,
317- command ,
318- ) )
319- ) {
320- let commandWithArgs = commandName ;
321- if ( commandArguments && commandArguments . length ) {
322- commandWithArgs += ` ${ commandArguments . join ( " " ) } ` ;
323- }
324- this . $errors . failWithHelp (
325- `Command '${ commandWithArgs } ' cannot be executed.` ,
326347 ) ;
348+ } finally {
349+ restoreOptions ( ) ;
350+ this . commands . pop ( ) ;
327351 }
328-
329- await this . runResolvedCommandInProcess (
330- command ,
331- commandName ,
332- commandArguments ,
333- ) ;
334- } finally {
335- restoreOptions ( ) ;
336- this . commands . pop ( ) ;
337- }
352+ } ) ;
338353 } catch ( ex ) {
339354 await this . $errors . reportCommandError ( ex , ( ) =>
340355 this . printHelpSuggestion ( commandName ) ,
341356 ) ;
342357
343358 throw ex ;
344- } finally {
345- this . inProcessDepth -- ;
346359 }
347360 }
348361
@@ -357,10 +370,10 @@ export class CommandsService
357370 reference : CommandReference ,
358371 commandArguments : string [ ] = [ ] ,
359372 ) : Promise < boolean > {
360- this . inProcessDepth ++ ;
361- try {
373+ return this . dispatchInProcess ( reference , async ( dispatch ) => {
362374 const resolved = this . resolveReference ( reference , commandArguments ) ;
363375 const { commandName, command } = resolved ;
376+ dispatch . commandName = commandName ;
364377 commandArguments = resolved . commandArguments ;
365378
366379 this . commands . push ( { commandName, commandArguments } ) ;
@@ -376,11 +389,47 @@ export class CommandsService
376389 restoreOptions ( ) ;
377390 this . commands . pop ( ) ;
378391 }
392+ } ) ;
393+ }
394+
395+ /**
396+ * Runs `body` as an in-process dispatch, rejecting it when another dispatch
397+ * is in flight and this one was not started from inside it.
398+ */
399+ private async dispatchInProcess < T > (
400+ reference : CommandReference ,
401+ body : ( dispatch : IInProcessDispatch ) => Promise < T > ,
402+ ) : Promise < T > {
403+ const running = _ . last ( this . inProcessDispatches ) ;
404+ if ( running && this . dispatchContext . getStore ( ) !== running ) {
405+ throw new Error (
406+ `Cannot dispatch '${ this . describeReference ( reference ) } ' in process ` +
407+ `while '${ helpers . stringReplaceAll ( running . commandName , "|" , " " ) } ' ` +
408+ "is still running: in-process dispatches must nest, not overlap; " +
409+ "await the running one first." ,
410+ ) ;
411+ }
412+
413+ const dispatch : IInProcessDispatch = {
414+ commandName : this . describeReference ( reference ) ,
415+ } ;
416+ this . inProcessDispatches . push ( dispatch ) ;
417+ try {
418+ return await this . dispatchContext . run ( dispatch , ( ) => body ( dispatch ) ) ;
379419 } finally {
380- this . inProcessDepth -- ;
420+ this . inProcessDispatches . pop ( ) ;
381421 }
382422 }
383423
424+ private describeReference ( reference : CommandReference ) : string {
425+ const definition =
426+ typeof reference === "string" ? null : toCommandDefinition ( reference ) ;
427+ const name = definition
428+ ? _ . castArray ( definition . name ) [ 0 ]
429+ : String ( reference ) ;
430+ return helpers . stringReplaceAll ( name , "|" , " " ) ;
431+ }
432+
384433 /** @deprecated Use {@link runCommand}. */
385434 public executeCommandInProcess (
386435 commandName : string ,
0 commit comments