Skip to content

Commit 8596bd9

Browse files
committed
Add idle-timeout: Timeout in minutes to wait before shutting down when idle
1 parent f4f0265 commit 8596bd9

File tree

4 files changed

+36
-5
lines changed

4 files changed

+36
-5
lines changed

src/node/cli.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export interface UserProvidedArgs extends UserProvidedCodeArgs {
8383
"socket-mode"?: string
8484
"trusted-origins"?: string[]
8585
version?: boolean
86+
"idle-timeout"?: number
8687
"proxy-domain"?: string[]
8788
"reuse-window"?: boolean
8889
"new-window"?: boolean
@@ -137,6 +138,13 @@ export type Options<T> = {
137138

138139
export const options: Options<Required<UserProvidedArgs>> = {
139140
auth: { type: AuthType, description: "The type of authentication to use." },
141+
<<<<<<< HEAD
142+
=======
143+
"auth-user": {
144+
type: "string",
145+
description: "The username for http-basic authentication."
146+
},
147+
>>>>>>> c6a85663 (Add idle-timeout: Timeout in minutes to wait before shutting down when idle)
140148
password: {
141149
type: "string",
142150
description: "The password for password authentication (can only be passed in via $PASSWORD or the config file).",
@@ -251,6 +259,7 @@ export const options: Options<Required<UserProvidedArgs>> = {
251259
type: "string",
252260
description: "GitHub authentication token (can only be passed in via $GITHUB_TOKEN or the config file).",
253261
},
262+
"idle-timeout": { type: "number", description: "Timeout in minutes to wait before shutting down when idle." },
254263
"proxy-domain": { type: "string[]", description: "Domain used for proxying ports." },
255264
"ignore-last-opened": {
256265
type: "boolean",
@@ -477,6 +486,7 @@ export interface DefaultedArgs extends ConfigArgs {
477486
}
478487
host: string
479488
port: number
489+
"idle-timeout": number
480490
"proxy-domain": string[]
481491
verbose: boolean
482492
usingEnvPassword: boolean
@@ -570,6 +580,10 @@ export async function setDefaults(cliArgs: UserProvidedArgs, configArgs?: Config
570580
args.password = process.env.PASSWORD
571581
}
572582

583+
if (process.env.IDLE_TIMEOUT) {
584+
args["idle-timeout"] = parseInt(process.env.IDLE_TIMEOUT, 10)
585+
}
586+
573587
if (process.env.CS_DISABLE_FILE_DOWNLOADS?.match(/^(1|true)$/)) {
574588
args["disable-file-downloads"] = true
575589
}

src/node/heart.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export class Heart {
1111

1212
public constructor(
1313
private readonly heartbeatPath: string,
14+
private readonly idleTimeout: number | undefined,
1415
private readonly isActive: () => Promise<boolean>,
1516
) {
1617
this.beat = this.beat.bind(this)
@@ -36,7 +37,10 @@ export class Heart {
3637
if (typeof this.heartbeatTimer !== "undefined") {
3738
clearTimeout(this.heartbeatTimer)
3839
}
39-
this.heartbeatTimer = setTimeout(() => heartbeatTimer(this.isActive, this.beat), this.heartbeatInterval)
40+
this.heartbeatTimer = setTimeout(
41+
() => heartbeatTimer(this.isActive, this.beat, this.lastHeartbeat, this.idleTimeout),
42+
this.heartbeatInterval
43+
)
4044
try {
4145
return await fs.writeFile(this.heartbeatPath, "")
4246
} catch (error: any) {
@@ -61,8 +65,21 @@ export class Heart {
6165
*
6266
* Extracted to make it easier to test.
6367
*/
64-
export async function heartbeatTimer(isActive: Heart["isActive"], beat: Heart["beat"]) {
68+
export async function heartbeatTimer(
69+
isActive: Heart["isActive"],
70+
beat: Heart["beat"],
71+
lastHeartbeat: number,
72+
idleTimeout?: number,
73+
) {
6574
try {
75+
// Check for idle timeout first
76+
if (idleTimeout) {
77+
const timeSinceLastBeat = Date.now() - lastHeartbeat
78+
if (timeSinceLastBeat > idleTimeout * 60 * 1000) {
79+
logger.warn(`Idle timeout of ${idleTimeout} minutes exceeded`)
80+
process.exit(0)
81+
}
82+
}
6683
if (await isActive()) {
6784
beat()
6885
}

src/node/routes/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import * as vscode from "./vscode"
3131
* Register all routes and middleware.
3232
*/
3333
export const register = async (app: App, args: DefaultedArgs): Promise<Disposable["dispose"]> => {
34-
const heart = new Heart(path.join(paths.data, "heartbeat"), async () => {
34+
const heart = new Heart(path.join(paths.data, "heartbeat"), args["idle-timeout"], async () => {
3535
return new Promise((resolve, reject) => {
3636
// getConnections appears to not call the callback when there are no more
3737
// connections. Feels like it must be a bug? For now add a timer to make

test/unit/node/heart.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ describe("heartbeatTimer", () => {
9595
const isActive = true
9696
const mockIsActive = jest.fn().mockResolvedValue(isActive)
9797
const mockBeatFn = jest.fn()
98-
await heartbeatTimer(mockIsActive, mockBeatFn)
98+
await heartbeatTimer(mockIsActive, mockBeatFn, 0)
9999
expect(mockIsActive).toHaveBeenCalled()
100100
expect(mockBeatFn).toHaveBeenCalled()
101101
})
@@ -104,7 +104,7 @@ describe("heartbeatTimer", () => {
104104
const error = new Error(errorMsg)
105105
const mockIsActive = jest.fn().mockRejectedValue(error)
106106
const mockBeatFn = jest.fn()
107-
await heartbeatTimer(mockIsActive, mockBeatFn)
107+
await heartbeatTimer(mockIsActive, mockBeatFn, 0)
108108
expect(mockIsActive).toHaveBeenCalled()
109109
expect(mockBeatFn).not.toHaveBeenCalled()
110110
expect(logger.warn).toHaveBeenCalledWith(errorMsg)

0 commit comments

Comments
 (0)