Skip to content

Add optional idle timeout shutdown #7177

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/node/cli.ts
Original file line number Diff line number Diff line change
@@ -83,6 +83,7 @@
"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 const options: Options<Required<UserProvidedArgs>> = {
auth: { type: AuthType, description: "The type of authentication to use." },
<<<<<<< HEAD

Check failure on line 141 in src/node/cli.ts

GitHub Actions / Build code-server

Merge conflict marker encountered.
=======

Check failure on line 142 in src/node/cli.ts

GitHub Actions / Build code-server

Merge conflict marker encountered.
"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)

Check failure on line 147 in src/node/cli.ts

GitHub Actions / Build code-server

Merge conflict marker encountered.
password: {
type: "string",
description: "The password for password authentication (can only be passed in via $PASSWORD or the config file).",
@@ -251,6 +259,7 @@
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 @@
}
host: string
port: number
"idle-timeout": number
"proxy-domain": string[]
verbose: boolean
usingEnvPassword: boolean
@@ -570,6 +580,10 @@
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
}
21 changes: 21 additions & 0 deletions src/node/heart.ts
Original file line number Diff line number Diff line change
@@ -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<boolean>,
) {
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,13 +51,27 @@ 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.
*/
public dispose(): void {
if (typeof this.heartbeatTimer !== "undefined") {
clearTimeout(this.heartbeatTimer)
}
if (typeof this.idleCheckTimer !== "undefined") {
clearInterval(this.idleCheckTimer)
}
}
}

4 changes: 4 additions & 0 deletions src/node/main.ts
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@
import { Disposable } from "../common/emitter"
import { plural } from "../common/util"
import { createApp, ensureAddress } from "./app"
import { AuthType, DefaultedArgs, Feature, toCodeArgs, UserProvidedArgs } from "./cli"

Check failure on line 7 in src/node/main.ts

GitHub Actions / Lint TypeScript files

Parse errors in imported module './cli': Merge conflict marker encountered. (141:0)
import { commit, version, vsRootPath } from "./constants"
import { register } from "./routes"
import { VSCodeModule } from "./routes/vscode"
@@ -152,6 +152,10 @@
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) {
2 changes: 1 addition & 1 deletion src/node/routes/index.ts
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@
import { HttpCode, HttpError } from "../../common/http"
import { plural } from "../../common/util"
import { App } from "../app"
import { AuthType, DefaultedArgs } from "../cli"

Check failure on line 12 in src/node/routes/index.ts

GitHub Actions / Lint TypeScript files

Parse errors in imported module '../cli': Merge conflict marker encountered. (141:0)
import { commit, rootPath } from "../constants"
import { Heart } from "../heart"
import { ensureAuthenticated, redirect } from "../http"
@@ -31,7 +31,7 @@
* Register all routes and middleware.
*/
export const register = async (app: App, args: DefaultedArgs): Promise<Disposable["dispose"]> => {
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
Loading
Oops, something went wrong.