|
| 1 | +/*! |
| 2 | + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. |
| 3 | + * SPDX-License-Identifier: Apache-2.0 |
| 4 | + */ |
| 5 | + |
| 6 | +import { DevEnvironment } from '../shared/clients/codecatalystClient' |
| 7 | +import { DevEnvActivity } from '../shared/clients/devenvClient' |
| 8 | +import globals from '../shared/extensionGlobals' |
| 9 | +import * as vscode from 'vscode' |
| 10 | +import { Timeout } from '../shared/utilities/timeoutUtils' |
| 11 | +import { showMessageWithCancel } from '../shared/utilities/messages' |
| 12 | +import { isCloud9 } from '../shared/extensionUtilities' |
| 13 | + |
| 14 | +/** If we should be sending the dev env activity timestamps to track user activity */ |
| 15 | +export function shouldTrackUserActivity(maxInactivityMinutes: DevEnvironment['inactivityTimeoutMinutes']): boolean { |
| 16 | + // This value is a static value initialized when the dev env is first created. |
| 17 | + // |
| 18 | + // If it is updated, the dev env is restarted and this extension will restarted and grab the latest value. |
| 19 | + // Due to this, we do not need to keep track of this value since the |
| 20 | + // |
| 21 | + // For more info see: https://docs.aws.amazon.com/codecatalyst/latest/APIReference/API_UpdateDevEnvironment.html#codecatalyst-UpdateDevEnvironment-request-inactivityTimeoutMinutes |
| 22 | + return maxInactivityMinutes > 0 |
| 23 | +} |
| 24 | + |
| 25 | +export class InactivityMessage implements vscode.Disposable { |
| 26 | + #message: Message | undefined |
| 27 | + #nextWholeMinute: NodeJS.Timeout | undefined |
| 28 | + #beforeMessageShown: NodeJS.Timeout | undefined |
| 29 | + |
| 30 | + /** Indicates to show first message 5 minutes before shutdown */ |
| 31 | + static readonly firstMessageBeforeShutdown = 5 |
| 32 | + |
| 33 | + /** |
| 34 | + * Sets up all messages that are displayed to the user when the dev env is inactive and starts to get close to shutdown. |
| 35 | + * |
| 36 | + * @param maxInactivityMinutes The # of minutes a CodeCatalyst Dev Env can be inactive till it shuts down. |
| 37 | + * @param devEnvActivity The DevEnvActivity client |
| 38 | + * @param relativeMinuteMillis How many milliseconds we want a "minute" to be. This is |
| 39 | + * useful for testing purposes. |
| 40 | + */ |
| 41 | + async setupMessage( |
| 42 | + maxInactivityMinutes: DevEnvironment['inactivityTimeoutMinutes'], |
| 43 | + devEnvActivity: DevEnvActivity, |
| 44 | + relativeMinuteMillis: number = 60_000 |
| 45 | + ) { |
| 46 | + devEnvActivity.onActivityUpdate(async latestActivityTimestamp => { |
| 47 | + this._setupMessage(maxInactivityMinutes, latestActivityTimestamp, devEnvActivity, relativeMinuteMillis) |
| 48 | + }) |
| 49 | + |
| 50 | + // Send an initial update to the dev env on startup |
| 51 | + devEnvActivity.sendActivityUpdate() |
| 52 | + } |
| 53 | + |
| 54 | + private _setupMessage( |
| 55 | + maxInactivityMinutes: number, |
| 56 | + latestTimestamp: number, |
| 57 | + devEnvActivity: DevEnvActivity, |
| 58 | + relativeMinuteMillis: number |
| 59 | + ) { |
| 60 | + this.clearOldMessage() |
| 61 | + |
| 62 | + const { millisToWait, minutesSinceTimestamp } = this.millisUntilNextWholeMinute( |
| 63 | + latestTimestamp, |
| 64 | + relativeMinuteMillis |
| 65 | + ) |
| 66 | + const minutesUntilShutdown = maxInactivityMinutes - minutesSinceTimestamp |
| 67 | + const minutesUntilFirstMessage = minutesUntilShutdown - InactivityMessage.firstMessageBeforeShutdown |
| 68 | + |
| 69 | + /** Wait until we are {@link InactivityMessage.firstMessageBeforeShutdown} minutes before shutdown. */ |
| 70 | + this.#beforeMessageShown = globals.clock.setTimeout(() => { |
| 71 | + const userIsActive = () => devEnvActivity.sendActivityUpdate() |
| 72 | + const willRefreshOnStaleTimestamp = async () => await devEnvActivity.isLocalActivityStale() |
| 73 | + |
| 74 | + this.message().show( |
| 75 | + minutesSinceTimestamp + minutesUntilFirstMessage, |
| 76 | + minutesUntilShutdown - minutesUntilFirstMessage, |
| 77 | + userIsActive, |
| 78 | + willRefreshOnStaleTimestamp, |
| 79 | + relativeMinuteMillis |
| 80 | + ) |
| 81 | + }, millisToWait + minutesUntilFirstMessage * relativeMinuteMillis) |
| 82 | + } |
| 83 | + |
| 84 | + private clearOldMessage() { |
| 85 | + if (this.#nextWholeMinute) { |
| 86 | + clearTimeout(this.#nextWholeMinute) |
| 87 | + this.#nextWholeMinute = undefined |
| 88 | + } |
| 89 | + if (this.#beforeMessageShown) { |
| 90 | + clearTimeout(this.#beforeMessageShown) |
| 91 | + this.#beforeMessageShown = undefined |
| 92 | + } |
| 93 | + if (this.#message) { |
| 94 | + this.#message.dispose() |
| 95 | + this.#message = undefined |
| 96 | + } |
| 97 | + } |
| 98 | + |
| 99 | + /** |
| 100 | + * The latest activity timestamp may not always be the current time, it may be from the past. |
| 101 | + * So the amount of time that has passed since that timestamp may not be a whole minute. |
| 102 | + * |
| 103 | + * This returns the amount of time we need to wait until the next whole minute, along with how many |
| 104 | + * minutes would have passed assuming the caller waitied until the next minute. |
| 105 | + * |
| 106 | + * Eg: |
| 107 | + * - 1 minute and 29 seconds have passed since the given timestamp. |
| 108 | + * - returns { millisToWait: 31_000, minutesSinceTimestamp: 2} |
| 109 | + */ |
| 110 | + private millisUntilNextWholeMinute( |
| 111 | + latestTimestamp: number, |
| 112 | + relativeMinuteMillis: number |
| 113 | + ): { millisToWait: number; minutesSinceTimestamp: number } { |
| 114 | + const millisSinceLastTimestamp = new Date().getTime() - latestTimestamp |
| 115 | + const millisSinceLastWholeMinute = millisSinceLastTimestamp % relativeMinuteMillis |
| 116 | + |
| 117 | + const millisToWait = millisSinceLastWholeMinute !== 0 ? relativeMinuteMillis - millisSinceLastWholeMinute : 0 |
| 118 | + const minutesSinceTimestamp = (millisSinceLastTimestamp + millisToWait) / relativeMinuteMillis |
| 119 | + |
| 120 | + return { millisToWait, minutesSinceTimestamp } |
| 121 | + } |
| 122 | + |
| 123 | + private message() { |
| 124 | + this.clearOldMessage() |
| 125 | + return (this.#message = new Message()) |
| 126 | + } |
| 127 | + |
| 128 | + dispose() { |
| 129 | + this.clearOldMessage() |
| 130 | + } |
| 131 | +} |
| 132 | + |
| 133 | +class Message implements vscode.Disposable { |
| 134 | + private currentWarningMessageTimeout: Timeout | undefined |
| 135 | + |
| 136 | + /** |
| 137 | + * Show the warning message |
| 138 | + * |
| 139 | + * @param minutesUserWasInactive total minutes user was inactive. |
| 140 | + * @param minutesUntilShutdown remaining minutes until shutdown. |
| 141 | + * @param userIsActive function to call when we want to indicate the user is active |
| 142 | + * @param willRefreshOnStaleTimestamp sanity checks with the dev env api that the latest activity timestamp |
| 143 | + * is the same as what this client has locally. If stale, the warning message |
| 144 | + * will be refreshed asynchronously. Returns true if the message will be refreshed. |
| 145 | + * @param relativeMinuteMillis How many milliseconds we want a "minute" to be. This is |
| 146 | + * useful for testing purposes. |
| 147 | + */ |
| 148 | + async show( |
| 149 | + minutesUserWasInactive: number, |
| 150 | + minutesUntilShutdown: number, |
| 151 | + userIsActive: () => Promise<any> | any, |
| 152 | + willRefreshOnStaleTimestamp: () => Promise<boolean>, |
| 153 | + relativeMinuteMillis: number |
| 154 | + ) { |
| 155 | + this.clearExistingMessage() |
| 156 | + // Show a new message every minute |
| 157 | + this.currentWarningMessageTimeout = new Timeout(1 * relativeMinuteMillis) |
| 158 | + |
| 159 | + if (await willRefreshOnStaleTimestamp()) { |
| 160 | + return |
| 161 | + } |
| 162 | + |
| 163 | + if (minutesUntilShutdown <= 1) { |
| 164 | + // Recursive base case, with only 1 minute left we do not want to show warning messages anymore, |
| 165 | + // since we will show a shutdown message instead. |
| 166 | + this.clearExistingMessage() |
| 167 | + |
| 168 | + const imHere = `I'm here!` |
| 169 | + vscode.window |
| 170 | + .showWarningMessage( |
| 171 | + `Your CodeCatalyst Dev Environment has been inactive for ${minutesUserWasInactive} minutes, and will stop soon.`, |
| 172 | + imHere |
| 173 | + ) |
| 174 | + .then(res => { |
| 175 | + if (res === imHere) { |
| 176 | + userIsActive() |
| 177 | + } |
| 178 | + }) |
| 179 | + return |
| 180 | + } |
| 181 | + |
| 182 | + this.currentWarningMessageTimeout.token.onCancellationRequested(c => { |
| 183 | + if (c.agent === 'user') { |
| 184 | + // User clicked the 'Cancel' button, indicate they are active. |
| 185 | + userIsActive() |
| 186 | + } else { |
| 187 | + // The message timed out, show the updated message. |
| 188 | + this.show( |
| 189 | + minutesUserWasInactive + 1, |
| 190 | + minutesUntilShutdown - 1, |
| 191 | + userIsActive, |
| 192 | + willRefreshOnStaleTimestamp, |
| 193 | + relativeMinuteMillis |
| 194 | + ) |
| 195 | + } |
| 196 | + }) |
| 197 | + |
| 198 | + if (isCloud9()) { |
| 199 | + // C9 does not support message with progress, so just show a warning message. |
| 200 | + vscode.window |
| 201 | + .showWarningMessage( |
| 202 | + this.buildInactiveWarningMessage(minutesUserWasInactive, minutesUntilShutdown), |
| 203 | + 'Cancel' |
| 204 | + ) |
| 205 | + .then(() => { |
| 206 | + this.currentWarningMessageTimeout!.cancel() |
| 207 | + }) |
| 208 | + } else { |
| 209 | + showMessageWithCancel( |
| 210 | + this.buildInactiveWarningMessage(minutesUserWasInactive, minutesUntilShutdown), |
| 211 | + this.currentWarningMessageTimeout |
| 212 | + ) |
| 213 | + } |
| 214 | + } |
| 215 | + |
| 216 | + clearExistingMessage() { |
| 217 | + if (this.currentWarningMessageTimeout) { |
| 218 | + this.currentWarningMessageTimeout.dispose() |
| 219 | + this.currentWarningMessageTimeout = undefined |
| 220 | + } |
| 221 | + } |
| 222 | + |
| 223 | + dispose() { |
| 224 | + this.clearExistingMessage() |
| 225 | + } |
| 226 | + |
| 227 | + private buildInactiveWarningMessage(inactiveMinutes: number, remainingMinutes: number) { |
| 228 | + return `Your CodeCatalyst Dev Environment has been inactive for ${inactiveMinutes} minutes, shutting it down in ${remainingMinutes} minutes.` |
| 229 | + } |
| 230 | +} |
0 commit comments