Skip to content
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

Add optional idle timeout shutdown #7177

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
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
Expand Up @@ -83,6 +83,7 @@
"socket-mode"?: string
"trusted-origins"?: string[]
version?: boolean
"idle-timeout"?: number
"proxy-domain"?: string[]
"reuse-window"?: boolean
"new-window"?: boolean
Expand Down Expand Up @@ -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

View workflow job for this annotation

GitHub Actions / Build code-server

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

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

View workflow job for this annotation

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

View workflow job for this annotation

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).",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -477,6 +486,7 @@
}
host: string
port: number
"idle-timeout": number
"proxy-domain": string[]
verbose: boolean
usingEnvPassword: boolean
Expand Down Expand Up @@ -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
}
Expand Down
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 {
Expand Down Expand Up @@ -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)
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/node/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

View workflow job for this annotation

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"
Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion src/node/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

View workflow job for this annotation

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"
Expand All @@ -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
Expand Down
Loading