diff --git a/src/node/cli.ts b/src/node/cli.ts index 9eb6e5163e8a..44347702b68a 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -83,6 +83,7 @@ export interface UserProvidedArgs extends UserProvidedCodeArgs { "socket-mode"?: string "trusted-origins"?: string[] version?: boolean + "idle-timeout"?: number "proxy-domain"?: string[] "reuse-window"?: boolean "new-window"?: boolean @@ -137,6 +138,13 @@ export type Options = { export const options: Options> = { auth: { type: AuthType, description: "The type of authentication to use." }, +<<<<<<< HEAD +======= + "auth-user": { + type: "string", + description: "The username for http-basic authentication." + }, +>>>>>>> c6a85663 (Add idle-timeout: Timeout in minutes to wait before shutting down when idle) password: { type: "string", 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> = { type: "string", description: "GitHub authentication token (can only be passed in via $GITHUB_TOKEN or the config file).", }, + "idle-timeout": { type: "number", description: "Timeout in minutes to wait before shutting down when idle." }, "proxy-domain": { type: "string[]", description: "Domain used for proxying ports." }, "ignore-last-opened": { type: "boolean", @@ -477,6 +486,7 @@ export interface DefaultedArgs extends ConfigArgs { } host: string port: number + "idle-timeout": number "proxy-domain": string[] verbose: boolean usingEnvPassword: boolean @@ -570,6 +580,10 @@ export async function setDefaults(cliArgs: UserProvidedArgs, configArgs?: Config args.password = process.env.PASSWORD } + if (process.env.IDLE_TIMEOUT) { + args["idle-timeout"] = parseInt(process.env.IDLE_TIMEOUT, 10) + } + if (process.env.CS_DISABLE_FILE_DOWNLOADS?.match(/^(1|true)$/)) { args["disable-file-downloads"] = true } diff --git a/src/node/heart.ts b/src/node/heart.ts index aac917257f23..ac2af5d80410 100644 --- a/src/node/heart.ts +++ b/src/node/heart.ts @@ -1,20 +1,27 @@ import { logger } from "@coder/logger" import { promises as fs } from "fs" +import { wrapper } from "./wrapper" /** * Provides a heartbeat using a local file to indicate activity. */ export class Heart { private heartbeatTimer?: NodeJS.Timeout + private idleCheckTimer?: NodeJS.Timeout private heartbeatInterval = 60000 public lastHeartbeat = 0 public constructor( private readonly heartbeatPath: string, + private readonly idleTimeout: number | undefined, private readonly isActive: () => Promise, ) { this.beat = this.beat.bind(this) this.alive = this.alive.bind(this) + // Start idle check timer if timeout is configured + if (this.idleTimeout) { + this.startIdleCheck() + } } public alive(): boolean { @@ -44,6 +51,17 @@ export class Heart { } } + private startIdleCheck(): void { + // Check every minute if the idle timeout has been exceeded + this.idleCheckTimer = setInterval(() => { + const timeSinceLastBeat = Date.now() - this.lastHeartbeat + if (timeSinceLastBeat > this.idleTimeout! * 60 * 1000) { + logger.warn(`Idle timeout of ${this.idleTimeout} minutes exceeded`) + wrapper.exit(5) + } + }, 60000) + } + /** * Call to clear any heartbeatTimer for shutdown. */ @@ -51,6 +69,9 @@ export class Heart { if (typeof this.heartbeatTimer !== "undefined") { clearTimeout(this.heartbeatTimer) } + if (typeof this.idleCheckTimer !== "undefined") { + clearInterval(this.idleCheckTimer) + } } } diff --git a/src/node/main.ts b/src/node/main.ts index b3c4e4c14500..c6116a7a00bc 100644 --- a/src/node/main.ts +++ b/src/node/main.ts @@ -152,6 +152,10 @@ export const runCodeServer = async ( logger.info(" - Not serving HTTPS") } + if (args["idle-timeout"]) { + logger.info(` - Idle timeout set to ${args["idle-timeout"]} minutes`) + } + if (args["disable-proxy"]) { logger.info(" - Proxy disabled") } else if (args["proxy-domain"].length > 0) { diff --git a/src/node/routes/index.ts b/src/node/routes/index.ts index e61cbd65795c..a9ac9c8ebbe6 100644 --- a/src/node/routes/index.ts +++ b/src/node/routes/index.ts @@ -31,7 +31,7 @@ import * as vscode from "./vscode" * Register all routes and middleware. */ export const register = async (app: App, args: DefaultedArgs): Promise => { - const heart = new Heart(path.join(paths.data, "heartbeat"), async () => { + const heart = new Heart(path.join(paths.data, "heartbeat"), args["idle-timeout"], async () => { return new Promise((resolve, reject) => { // getConnections appears to not call the callback when there are no more // connections. Feels like it must be a bug? For now add a timer to make