-
-
Notifications
You must be signed in to change notification settings - Fork 320
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Create the `abort` plugin to support killing any active / future tasks for a `simple-git` instance
- Loading branch information
Showing
20 changed files
with
318 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'simple-git': minor | ||
--- | ||
|
||
Create the abort plugin to allow cancelling all pending and future tasks. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
## Using an AbortController to terminate tasks | ||
|
||
The easiest way to send a `SIGKILL` to the `git` child processes created by `simple-git` is to use an `AbortController` | ||
in the constructor options for `simpleGit`: | ||
|
||
```typescript | ||
import { simpleGit, GitPluginError, SimpleGit } from 'simple-git'; | ||
|
||
const controller = new AbortController(); | ||
|
||
const git: SimpleGit = simpleGit({ | ||
baseDir: '/some/path', | ||
abort: controller.signal, | ||
}); | ||
|
||
try { | ||
await git.pull(); | ||
} | ||
catch (err) { | ||
if (err instanceof GitPluginError && err.plugin === 'abort') { | ||
// task failed because `controller.abort` was called while waiting for the `git.pull` | ||
} | ||
} | ||
``` | ||
|
||
### Examples: | ||
|
||
#### Share AbortController across many instances | ||
|
||
Run the same operation against multiple repositories, cancel any pending operations when the first has been completed. | ||
|
||
```typescript | ||
const repos = [ | ||
'/path/to/repo-a', | ||
'/path/to/repo-b', | ||
'/path/to/repo-c', | ||
]; | ||
|
||
const controller = new AbortController(); | ||
const result = await Promise.race( | ||
repos.map(baseDir => simpleGit({ baseDir, abort: controller.signal }).fetch()) | ||
); | ||
controller.abort(); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { setMaxListeners } from 'events'; | ||
|
||
export function createAbortController() { | ||
if (typeof AbortController === 'undefined') { | ||
return createMockAbortController() as { controller: AbortController; abort: AbortSignal }; | ||
} | ||
|
||
const controller = new AbortController(); | ||
setMaxListeners(1000, controller.signal); | ||
return { | ||
controller, | ||
abort: controller.signal, | ||
mocked: false, | ||
}; | ||
} | ||
|
||
function createMockAbortController(): unknown { | ||
let aborted = false; | ||
const handlers: Set<() => void> = new Set(); | ||
const abort = { | ||
addEventListener(type: 'abort', handler: () => void) { | ||
if (type !== 'abort') throw new Error('Unsupported event name'); | ||
handlers.add(handler); | ||
}, | ||
removeEventListener(type: 'abort', handler: () => void) { | ||
if (type !== 'abort') throw new Error('Unsupported event name'); | ||
handlers.delete(handler); | ||
}, | ||
get aborted() { | ||
return aborted; | ||
}, | ||
}; | ||
|
||
return { | ||
controller: { | ||
abort() { | ||
if (aborted) throw new Error('abort called when already aborted'); | ||
aborted = true; | ||
handlers.forEach((h) => h()); | ||
}, | ||
}, | ||
abort, | ||
mocked: true, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { SimpleGitOptions } from '../types'; | ||
import { SimpleGitPlugin } from './simple-git-plugin'; | ||
import { GitPluginError } from '../errors/git-plugin-error'; | ||
|
||
export function abortPlugin(signal: SimpleGitOptions['abort']) { | ||
if (!signal) { | ||
return; | ||
} | ||
|
||
const onSpawnAfter: SimpleGitPlugin<'spawn.after'> = { | ||
type: 'spawn.after', | ||
action(_data, context) { | ||
function kill() { | ||
context.kill(new GitPluginError(undefined, 'abort', 'Abort signal received')); | ||
} | ||
|
||
signal.addEventListener('abort', kill); | ||
|
||
context.spawned.on('close', () => signal.removeEventListener('abort', kill)); | ||
}, | ||
}; | ||
|
||
const onSpawnBefore: SimpleGitPlugin<'spawn.before'> = { | ||
type: 'spawn.before', | ||
action(_data, context) { | ||
if (signal.aborted) { | ||
context.kill(new GitPluginError(undefined, 'abort', 'Abort already signaled')); | ||
} | ||
}, | ||
}; | ||
|
||
return [onSpawnBefore, onSpawnAfter]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import { promiseError } from '@kwsites/promise-result'; | ||
import { | ||
assertGitError, | ||
createAbortController, | ||
createTestContext, | ||
newSimpleGit, | ||
SimpleGitTestContext, | ||
wait, | ||
} from '@simple-git/test-utils'; | ||
|
||
import { GitPluginError } from '../..'; | ||
|
||
describe('timeout', () => { | ||
let context: SimpleGitTestContext; | ||
|
||
beforeEach(async () => (context = await createTestContext())); | ||
|
||
it('kills processes on abort signal', async () => { | ||
const { controller, abort } = createAbortController(); | ||
|
||
const threw = promiseError(newSimpleGit(context.root, { abort }).init()); | ||
|
||
await wait(0); | ||
controller.abort(); | ||
|
||
assertGitError(await threw, 'Abort signal received', GitPluginError); | ||
}); | ||
|
||
it('share AbortController across many instances', async () => { | ||
const { controller, abort } = createAbortController(); | ||
const upstream = await newSimpleGit(__dirname).revparse('--git-dir'); | ||
|
||
const repos = await Promise.all('abcdef'.split('').map((p) => context.dir(p))); | ||
|
||
await Promise.all( | ||
repos.map((baseDir) => { | ||
const git = newSimpleGit({ baseDir, abort }); | ||
if (baseDir.endsWith('a')) { | ||
return promiseError(git.init().then(() => controller.abort())); | ||
} | ||
|
||
return promiseError(git.clone(upstream, baseDir)); | ||
}) | ||
); | ||
|
||
const results = await Promise.all( | ||
repos.map((baseDir) => newSimpleGit(baseDir).checkIsRepo()) | ||
); | ||
|
||
expect(results).toContain(false); | ||
expect(results).toContain(true); | ||
}); | ||
}); |
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.