public async startDevEnvironmentWithProgress()

in packages/core/src/shared/clients/codecatalystClient.ts [738:902]


    public async startDevEnvironmentWithProgress(
        args: RequiredProps<StartDevEnvironmentRequest, 'id' | 'spaceName' | 'projectName'>,
        timeout: Timeout = new Timeout(1000 * 60 * 60)
    ): Promise<DevEnvironment> {
        // Track the status changes chronologically so that we can
        // 1. reason about hysterisis (weird flip-flops)
        // 2. have visibility in the logs
        const statuses = new Array<{ status: string; start: number }>()
        let alias: string | undefined
        let startAttempts = 0

        function statusesToString() {
            let s = ''
            for (let i = 0; i < statuses.length; i++) {
                const item = statuses[i]
                const nextItem = i < statuses.length - 1 ? statuses[i + 1] : undefined
                const nextTime = nextItem ? nextItem.start : Date.now()
                const elapsed = nextTime - item.start
                s += `${s ? ' ' : ''}${item.status}/${elapsed}ms`
            }
            return `[${s}]`
        }

        function getName(): string {
            const fullname = alias ? alias : args.id
            const shortname = fullname.substring(0, 19) + (fullname.length > 20 ? '…' : '')
            return shortname
        }

        function failedStartMsg(serviceMsg?: string) {
            const LastSeenStatus = statuses[statuses.length - 1]?.status
            const serviceMsg_ = serviceMsg ? `${serviceMsg}: ` : ''
            return `Dev Environment failed to start (${LastSeenStatus}): ${serviceMsg_}${getName()}`
        }

        const doLog = (kind: 'debug' | 'error' | 'info', msg: string) => {
            const fmt = `${msg} (time: %ds${
                startAttempts <= 1 ? '' : ', startAttempts: ' + startAttempts.toString()
            }): %s %s`
            if (kind === 'debug') {
                this.log.debug(fmt, timeout.elapsedTime / 1000, getName(), statusesToString())
            } else if (kind === 'error') {
                this.log.error(fmt, timeout.elapsedTime / 1000, getName(), statusesToString())
            } else {
                this.log.info(fmt, timeout.elapsedTime / 1000, getName(), statusesToString())
            }
        }

        const progress = await showMessageWithCancel(
            localize('AWS.CodeCatalyst.devenv.message', 'CodeCatalyst'),
            timeout
        )
        progress.report({ message: localize('AWS.CodeCatalyst.devenv.checking', 'Checking status...') })

        try {
            const devenv = await this.getDevEnvironment(args)
            alias = devenv.alias
            statuses.push({ status: devenv.status, start: Date.now() })
            if (devenv.status === 'RUNNING') {
                doLog('debug', 'devenv RUNNING')
                timeout.cancel()
                // "Debounce" in case caller did not check if the environment was already running.
                return devenv
            }
        } catch {
            // Continue.
        }

        doLog('debug', 'devenv not started, waiting')

        const pollDevEnv = waitUntil(
            async () => {
                if (timeout.completed) {
                    // TODO: need a better way to "cancel" a `waitUntil`.
                    throw new CancellationError('user')
                }

                const LastSeenStatus = statuses[statuses.length - 1]
                const elapsed = Date.now() - LastSeenStatus.start
                const resp = await this.getDevEnvironment(args)
                const serviceReason = (resp.statusReason ?? '').trim()
                alias = resp.alias

                if (
                    startAttempts > 2 &&
                    elapsed > 10000 &&
                    ['STOPPED', 'FAILED'].includes(LastSeenStatus.status) &&
                    ['STOPPED', 'FAILED'].includes(resp.status)
                ) {
                    const fails = statuses.filter((o) => o.status === 'FAILED').length
                    const code = fails === 0 ? 'BadDevEnvState' : 'FailedDevEnv'

                    if (serviceReason !== '') {
                        // Service gave a status reason like "Compute limit exceeded", show it to the user.
                        throw new ToolkitError(failedStartMsg(resp.statusReason), { code: code })
                    }

                    // If still STOPPED/FAILED after 10+ seconds, don't keep retrying for 1 hour...
                    throw new ToolkitError(failedStartMsg(), { code: code })
                } else if (['STOPPED', 'FAILED'].includes(resp.status)) {
                    progress.report({
                        message: localize('AWS.CodeCatalyst.devenv.resuming', 'Resuming Dev Environment...'),
                    })
                    try {
                        startAttempts++
                        await this.startDevEnvironment(args)
                    } catch (e) {
                        const err = e as AWS.AWSError
                        // - ServiceQuotaExceededException: account billing limit reached
                        // - ValidationException: "… creation has failed, cannot start"
                        // - ConflictException: "Cannot start … because update process is still going on"
                        //   (can happen after "Update Dev Environment")
                        if (err.code === 'ServiceQuotaExceededException') {
                            throw new ToolkitError('Dev Environment failed: quota exceeded', {
                                code: 'ServiceQuotaExceeded',
                                cause: err,
                            })
                        }
                        doLog('info', `devenv not started (${err.code}), waiting`)
                        // Continue retrying...
                    }
                } else if (resp.status === 'STOPPING') {
                    progress.report({
                        message: localize('AWS.CodeCatalyst.devenv.stopping', 'Waiting for Dev Environment to stop...'),
                    })
                } else {
                    progress.report({
                        message: localize('AWS.CodeCatalyst.devenv.starting', 'Opening Dev Environment...'),
                    })
                }

                if (LastSeenStatus?.status !== resp.status) {
                    statuses.push({ status: resp.status, start: Date.now() })
                    if (resp.status !== 'RUNNING') {
                        doLog('debug', `devenv not started, waiting`)
                    }
                }
                return resp.status === 'RUNNING' ? resp : undefined
            },
            // note: the `waitUntil` will resolve prior to the real timeout if it is refreshed
            { interval: 1000, timeout: timeout.remainingTime, truthy: true }
        )

        const devenv = await waitTimeout(pollDevEnv, timeout).catch((e) => {
            if (isUserCancelledError(e)) {
                doLog('info', 'devenv failed to start (user cancelled)')
                e.message = failedStartMsg()
                throw e
            } else if (e instanceof ToolkitError) {
                doLog('error', 'devenv failed to start')
                throw e
            }

            doLog('error', 'devenv failed to start')
            throw new ToolkitError(failedStartMsg(), { code: 'Unknown', cause: e })
        })

        if (!devenv) {
            doLog('error', 'devenv failed to start (timeout)')
            throw new ToolkitError(failedStartMsg(), { code: 'Timeout' })
        }
        doLog('info', 'devenv started')

        return devenv
    }