Avoid windows shutdown bug

This commit is contained in:
Daz DeBoer
2026-03-20 12:31:53 -06:00
parent 2cab5e3c71
commit 52fac46c3c
6 changed files with 61 additions and 4 deletions

View File

@@ -15,6 +15,7 @@ import {
} from '../../configuration' } from '../../configuration'
import {saveDeprecationState} from '../../deprecation-collector' import {saveDeprecationState} from '../../deprecation-collector'
import {handleMainActionError} from '../../errors' import {handleMainActionError} from '../../errors'
import {forceExit} from '../../force-exit'
/** /**
* The main entry point for the action, called by Github Actions for the step. * The main entry point for the action, called by Github Actions for the step.
@@ -67,7 +68,7 @@ export async function run(): Promise<void> {
} }
// Explicit process.exit() to prevent waiting for hanging promises. // Explicit process.exit() to prevent waiting for hanging promises.
process.exit() await forceExit()
} }
run() run()

View File

@@ -2,6 +2,7 @@ import * as setupGradle from '../../setup-gradle'
import {CacheConfig, SummaryConfig} from '../../configuration' import {CacheConfig, SummaryConfig} from '../../configuration'
import {handlePostActionError} from '../../errors' import {handlePostActionError} from '../../errors'
import {forceExit} from '../../force-exit'
// Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in // Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in
// @actions/toolkit when a failed upload closes the file descriptor causing any in-process reads to // @actions/toolkit when a failed upload closes the file descriptor causing any in-process reads to
@@ -19,7 +20,7 @@ export async function run(): Promise<void> {
} }
// Explicit process.exit() to prevent waiting for promises left hanging by `@actions/cache` on save. // Explicit process.exit() to prevent waiting for promises left hanging by `@actions/cache` on save.
process.exit() await forceExit()
} }
run() run()

View File

@@ -12,6 +12,7 @@ import {
} from '../../configuration' } from '../../configuration'
import {failOnUseOfRemovedFeature, saveDeprecationState} from '../../deprecation-collector' import {failOnUseOfRemovedFeature, saveDeprecationState} from '../../deprecation-collector'
import {handleMainActionError} from '../../errors' import {handleMainActionError} from '../../errors'
import {forceExit} from '../../force-exit'
/** /**
* The main entry point for the action, called by Github Actions for the step. * The main entry point for the action, called by Github Actions for the step.
@@ -42,7 +43,7 @@ export async function run(): Promise<void> {
} }
// Explicit process.exit() to prevent waiting for hanging promises. // Explicit process.exit() to prevent waiting for hanging promises.
process.exit() await forceExit()
} }
run() run()

View File

@@ -4,6 +4,7 @@ import * as dependencyGraph from '../../dependency-graph'
import {CacheConfig, DependencyGraphConfig, SummaryConfig} from '../../configuration' import {CacheConfig, DependencyGraphConfig, SummaryConfig} from '../../configuration'
import {handlePostActionError} from '../../errors' import {handlePostActionError} from '../../errors'
import {emitDeprecationWarnings, restoreDeprecationState} from '../../deprecation-collector' import {emitDeprecationWarnings, restoreDeprecationState} from '../../deprecation-collector'
import {forceExit} from '../../force-exit'
// Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in // Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in
// @actions/toolkit when a failed upload closes the file descriptor causing any in-process reads to // @actions/toolkit when a failed upload closes the file descriptor causing any in-process reads to
@@ -27,7 +28,7 @@ export async function run(): Promise<void> {
} }
// Explicit process.exit() to prevent waiting for promises left hanging by `@actions/cache` on save. // Explicit process.exit() to prevent waiting for promises left hanging by `@actions/cache` on save.
process.exit() await forceExit()
} }
run() run()

14
sources/src/force-exit.ts Normal file
View File

@@ -0,0 +1,14 @@
const WINDOWS_EXIT_DELAY_MS = 50
export function getForcedExitDelayMs(platform: NodeJS.Platform = process.platform): number {
return platform === 'win32' ? WINDOWS_EXIT_DELAY_MS : 0
}
export async function forceExit(platform: NodeJS.Platform = process.platform): Promise<never> {
const exitDelayMs = getForcedExitDelayMs(platform)
if (exitDelayMs > 0) {
await new Promise(resolve => setTimeout(resolve, exitDelayMs))
}
return process.exit()
}

View File

@@ -0,0 +1,39 @@
import {afterEach, describe, expect, it, jest} from '@jest/globals'
import {forceExit, getForcedExitDelayMs} from '../../src/force-exit'
describe('forceExit', () => {
afterEach(() => {
jest.restoreAllMocks()
jest.useRealTimers()
})
it('adds a short delay on Windows before exiting', async () => {
jest.useFakeTimers()
const exitSpy = jest.spyOn(process, 'exit').mockImplementation((() => undefined) as never)
const exitPromise = forceExit('win32')
await jest.advanceTimersByTimeAsync(49)
expect(exitSpy).not.toHaveBeenCalled()
await jest.advanceTimersByTimeAsync(1)
await expect(exitPromise).resolves.toBeUndefined()
expect(exitSpy).toHaveBeenCalledTimes(1)
})
it('exits immediately on non-Windows platforms', async () => {
const exitSpy = jest.spyOn(process, 'exit').mockImplementation((() => undefined) as never)
await expect(forceExit('linux')).resolves.toBeUndefined()
expect(exitSpy).toHaveBeenCalledTimes(1)
})
it('only delays on Windows', () => {
expect(getForcedExitDelayMs('win32')).toBe(50)
expect(getForcedExitDelayMs('linux')).toBe(0)
expect(getForcedExitDelayMs('darwin')).toBe(0)
})
})