Skip to content

Commit ff53d2c

Browse files
authored
Merge pull request #6389 from aws/autoMerge/feature/amazonqLSP
Merge master into feature/amazonqLSP
2 parents df611ea + af447e8 commit ff53d2c

File tree

10 files changed

+463
-92
lines changed

10 files changed

+463
-92
lines changed

packages/core/src/dev/activation.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { getSessionId } from '../shared/telemetry/util'
2424
import { NotificationsController } from '../notifications/controller'
2525
import { DevNotificationsState } from '../notifications/types'
2626
import { QuickPickItem } from 'vscode'
27+
import { ChildProcess } from '../shared/utilities/processUtils'
2728

2829
interface MenuOption {
2930
readonly label: string
@@ -44,6 +45,7 @@ export type DevFunction =
4445
| 'editAuthConnections'
4546
| 'notificationsSend'
4647
| 'forceIdeCrash'
48+
| 'startChildProcess'
4749

4850
export type DevOptions = {
4951
context: vscode.ExtensionContext
@@ -126,6 +128,11 @@ const menuOptions: () => Record<DevFunction, MenuOption> = () => {
126128
detail: `Will SIGKILL ExtHost, { pid: ${process.pid}, sessionId: '${getSessionId().slice(0, 8)}-...' }, but the IDE itself will not crash.`,
127129
executor: forceQuitIde,
128130
},
131+
startChildProcess: {
132+
label: 'ChildProcess: Start child process',
133+
detail: 'Start ChildProcess from our utility wrapper for testing',
134+
executor: startChildProcess,
135+
},
129136
}
130137
}
131138

@@ -578,3 +585,15 @@ async function editNotifications() {
578585
await targetNotificationsController.pollForEmergencies()
579586
})
580587
}
588+
589+
async function startChildProcess() {
590+
const result = await createInputBox({
591+
title: 'Enter a command',
592+
}).prompt()
593+
if (result) {
594+
const [command, ...args] = result?.toString().split(' ') ?? []
595+
getLogger().info(`Starting child process: '${command}'`)
596+
const processResult = await ChildProcess.run(command, args, { collect: true })
597+
getLogger().info(`Child process exited with code ${processResult.exitCode}`)
598+
}
599+
}

packages/core/src/lambda/commands/downloadLambda.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { LaunchConfiguration, getReferencedHandlerPaths } from '../../shared/deb
1414
import { makeTemporaryToolkitFolder, fileExists, tryRemoveFolder } from '../../shared/filesystemUtilities'
1515
import * as localizedText from '../../shared/localizedText'
1616
import { getLogger } from '../../shared/logger'
17-
import { HttpResourceFetcher } from '../../shared/resourcefetcher/httpResourceFetcher'
17+
import { HttpResourceFetcher } from '../../shared/resourcefetcher/node/httpResourceFetcher'
1818
import { createCodeAwsSamDebugConfig } from '../../shared/sam/debugger/awsSamDebugConfiguration'
1919
import * as pathutils from '../../shared/utilities/pathUtils'
2020
import { localize } from '../../shared/utilities/vsCodeUtils'

packages/core/src/shared/logger/logger.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import * as vscode from 'vscode'
77

8-
export type LogTopic = 'crashMonitoring' | 'dev/beta' | 'notifications' | 'test' | 'unknown' | 'lsp'
8+
export type LogTopic = 'crashMonitoring' | 'dev/beta' | 'notifications' | 'test' | 'childProcess' | 'lsp' | 'unknown'
99

1010
class ErrorLog {
1111
constructor(

packages/core/src/shared/resourcefetcher/httpResourceFetcher.ts

Lines changed: 18 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,13 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import * as fs from 'fs' // eslint-disable-line no-restricted-imports
7-
import * as http from 'http'
8-
import * as https from 'https'
9-
import * as stream from 'stream'
10-
import got, { Response, RequestError, CancelError } from 'got'
11-
import urlToOptions from 'got/dist/source/core/utils/url-to-options'
12-
import Request from 'got/dist/source/core'
136
import { VSCODE_EXTENSION_ID } from '../extensions'
147
import { getLogger, Logger } from '../logger'
158
import { ResourceFetcher } from './resourcefetcher'
16-
import { Timeout, CancellationError, CancelEvent } from '../utilities/timeoutUtils'
17-
import { isCloud9 } from '../extensionUtilities'
18-
import { Headers } from 'got/dist/source/core'
9+
import { Timeout, CancelEvent } from '../utilities/timeoutUtils'
10+
import request, { RequestError } from '../request'
1911
import { withRetries } from '../utilities/functionUtils'
2012

21-
// XXX: patched Got module for compatability with older VS Code versions (e.g. Cloud9)
22-
// `got` has also deprecated `urlToOptions`
23-
const patchedGot = got.extend({
24-
request: (url, options, callback) => {
25-
if (url.protocol === 'https:') {
26-
return https.request({ ...options, ...urlToOptions(url) }, callback)
27-
}
28-
return http.request({ ...options, ...urlToOptions(url) }, callback)
29-
},
30-
})
31-
32-
/** Promise that resolves/rejects when all streams close. Can also access streams directly. */
33-
type FetcherResult = Promise<void> & {
34-
/** Download stream piped to `fsStream`. */
35-
requestStream: Request // `got` doesn't add the correct types to 'on' for some reason
36-
/** Stream writing to the file system. */
37-
fsStream: fs.WriteStream
38-
}
39-
4013
type RequestHeaders = { eTag?: string; gZip?: boolean }
4114

4215
export class HttpResourceFetcher implements ResourceFetcher {
@@ -66,20 +39,8 @@ export class HttpResourceFetcher implements ResourceFetcher {
6639
*
6740
* @param pipeLocation Optionally pipe the download to a file system location
6841
*/
69-
public get(): Promise<string | undefined>
70-
public get(pipeLocation: string): FetcherResult
71-
public get(pipeLocation?: string): Promise<string | undefined> | FetcherResult {
42+
public get(): Promise<string | undefined> {
7243
this.logger.verbose(`downloading: ${this.logText()}`)
73-
74-
if (pipeLocation) {
75-
const result = this.pipeGetRequest(pipeLocation, this.params.timeout)
76-
result.fsStream.on('exit', () => {
77-
this.logger.verbose(`downloaded: ${this.logText()}`)
78-
})
79-
80-
return result
81-
}
82-
8344
return this.downloadRequest()
8445
}
8546

@@ -95,15 +56,15 @@ export class HttpResourceFetcher implements ResourceFetcher {
9556
public async getNewETagContent(eTag?: string): Promise<{ content?: string; eTag: string }> {
9657
const response = await this.getResponseFromGetRequest(this.params.timeout, { eTag, gZip: true })
9758

98-
const eTagResponse = response.headers.etag
59+
const eTagResponse = response.headers.get('etag')
9960
if (!eTagResponse) {
10061
throw new Error(`This URL does not support E-Tags. Cannot use this function for: ${this.url.toString()}`)
10162
}
10263

10364
// NOTE: Even with use of `gzip` encoding header, the response content is uncompressed.
10465
// Most likely due to the http request library uncompressing it for us.
105-
let contents: string | undefined = response.body.toString()
106-
if (response.statusCode === 304) {
66+
let contents: string | undefined = await response.text()
67+
if (response.status === 304) {
10768
// Explanation: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match
10869
contents = undefined
10970
this.logger.verbose(`E-Tag, ${eTagResponse}, matched. No content downloaded from: ${this.url}`)
@@ -120,7 +81,8 @@ export class HttpResourceFetcher implements ResourceFetcher {
12081
private async downloadRequest(): Promise<string | undefined> {
12182
try {
12283
// HACK(?): receiving JSON as a string without `toString` makes it so we can't deserialize later
123-
const contents = (await this.getResponseFromGetRequest(this.params.timeout)).body.toString()
84+
const resp = await this.getResponseFromGetRequest(this.params.timeout)
85+
const contents = (await resp.text()).toString()
12486
if (this.params.onSuccess) {
12587
this.params.onSuccess(contents)
12688
}
@@ -129,10 +91,10 @@ export class HttpResourceFetcher implements ResourceFetcher {
12991

13092
return contents
13193
} catch (err) {
132-
const error = err as CancelError | RequestError
94+
const error = err as RequestError
13395
this.logger.verbose(
13496
`Error downloading ${this.logText()}: %s`,
135-
error.message ?? error.code ?? error.response?.statusMessage ?? error.response?.statusCode
97+
error.message ?? error.code ?? error.response.statusText ?? error.response.status
13698
)
13799
return undefined
138100
}
@@ -146,56 +108,30 @@ export class HttpResourceFetcher implements ResourceFetcher {
146108
getLogger().debug(`Download for "${this.logText()}" ${event.agent === 'user' ? 'cancelled' : 'timed out'}`)
147109
}
148110

149-
// TODO: make pipeLocation a vscode.Uri
150-
private pipeGetRequest(pipeLocation: string, timeout?: Timeout): FetcherResult {
151-
const requester = isCloud9() ? patchedGot : got
152-
const requestStream = requester.stream(this.url, { headers: this.buildRequestHeaders() })
153-
const fsStream = fs.createWriteStream(pipeLocation)
154-
155-
const done = new Promise<void>((resolve, reject) => {
156-
const pipe = stream.pipeline(requestStream, fsStream, (err) => {
157-
if (err instanceof RequestError) {
158-
return reject(Object.assign(new Error('Failed to download file'), { code: err.code }))
159-
}
160-
err ? reject(err) : resolve()
161-
})
162-
163-
const cancelListener = timeout?.token.onCancellationRequested((event) => {
164-
this.logCancellation(event)
165-
pipe.destroy(new CancellationError(event.agent))
166-
})
167-
168-
pipe.on('close', () => cancelListener?.dispose())
169-
})
170-
171-
return Object.assign(done, { requestStream, fsStream })
172-
}
173-
174-
private async getResponseFromGetRequest(timeout?: Timeout, headers?: RequestHeaders): Promise<Response<string>> {
175-
const requester = isCloud9() ? patchedGot : got
176-
const promise = requester(this.url, {
111+
private async getResponseFromGetRequest(timeout?: Timeout, headers?: RequestHeaders): Promise<Response> {
112+
const req = request.fetch('GET', this.url, {
177113
headers: this.buildRequestHeaders(headers),
178114
})
179115

180116
const cancelListener = timeout?.token.onCancellationRequested((event) => {
181117
this.logCancellation(event)
182-
promise.cancel(new CancellationError(event.agent).message)
118+
req.cancel()
183119
})
184120

185-
return promise.finally(() => cancelListener?.dispose())
121+
return req.response.finally(() => cancelListener?.dispose())
186122
}
187123

188124
private buildRequestHeaders(requestHeaders?: RequestHeaders): Headers {
189-
const headers: Headers = {}
125+
const headers = new Headers()
190126

191-
headers['User-Agent'] = VSCODE_EXTENSION_ID.awstoolkit
127+
headers.set('User-Agent', VSCODE_EXTENSION_ID.awstoolkit)
192128

193129
if (requestHeaders?.eTag !== undefined) {
194-
headers['If-None-Match'] = requestHeaders.eTag
130+
headers.set('If-None-Match', requestHeaders.eTag)
195131
}
196132

197133
if (requestHeaders?.gZip) {
198-
headers['Accept-Encoding'] = 'gzip'
134+
headers.set('Accept-Encoding', 'gzip')
199135
}
200136

201137
return headers
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import * as fs from 'fs' // eslint-disable-line no-restricted-imports
7+
import * as http from 'http'
8+
import * as https from 'https'
9+
import * as stream from 'stream'
10+
import got, { RequestError } from 'got'
11+
import urlToOptions from 'got/dist/source/core/utils/url-to-options'
12+
import Request from 'got/dist/source/core'
13+
import { VSCODE_EXTENSION_ID } from '../../extensions'
14+
import { getLogger, Logger } from '../../logger'
15+
import { Timeout, CancellationError, CancelEvent } from '../../utilities/timeoutUtils'
16+
import { isCloud9 } from '../../extensionUtilities'
17+
import { Headers } from 'got/dist/source/core'
18+
19+
// XXX: patched Got module for compatability with older VS Code versions (e.g. Cloud9)
20+
// `got` has also deprecated `urlToOptions`
21+
const patchedGot = got.extend({
22+
request: (url, options, callback) => {
23+
if (url.protocol === 'https:') {
24+
return https.request({ ...options, ...urlToOptions(url) }, callback)
25+
}
26+
return http.request({ ...options, ...urlToOptions(url) }, callback)
27+
},
28+
})
29+
30+
/** Promise that resolves/rejects when all streams close. Can also access streams directly. */
31+
type FetcherResult = Promise<void> & {
32+
/** Download stream piped to `fsStream`. */
33+
requestStream: Request // `got` doesn't add the correct types to 'on' for some reason
34+
/** Stream writing to the file system. */
35+
fsStream: fs.WriteStream
36+
}
37+
38+
type RequestHeaders = { eTag?: string; gZip?: boolean }
39+
40+
/**
41+
* Legacy HTTP Resource Fetcher used specifically for streaming information.
42+
* Only kept around until web streams are compatible with node streams
43+
*/
44+
export class HttpResourceFetcher {
45+
private readonly logger: Logger = getLogger()
46+
47+
/**
48+
*
49+
* @param url URL to fetch a response body from via the `get` call
50+
* @param params Additional params for the fetcher
51+
* @param {boolean} params.showUrl Whether or not to the URL in log statements.
52+
* @param {string} params.friendlyName If URL is not shown, replaces the URL with this text.
53+
* @param {function} params.onSuccess Function to execute on successful request. No effect if piping to a location.
54+
* @param {Timeout} params.timeout Timeout token to abort/cancel the request. Similar to `AbortSignal`.
55+
*/
56+
public constructor(
57+
private readonly url: string,
58+
private readonly params: {
59+
showUrl: boolean
60+
friendlyName?: string
61+
timeout?: Timeout
62+
}
63+
) {}
64+
65+
/**
66+
* Returns the contents of the resource, or undefined if the resource could not be retrieved.
67+
*
68+
* @param pipeLocation Optionally pipe the download to a file system location
69+
*/
70+
public get(pipeLocation: string): FetcherResult {
71+
this.logger.verbose(`downloading: ${this.logText()}`)
72+
73+
const result = this.pipeGetRequest(pipeLocation, this.params.timeout)
74+
result.fsStream.on('exit', () => {
75+
this.logger.verbose(`downloaded: ${this.logText()}`)
76+
})
77+
78+
return result
79+
}
80+
81+
private logText(): string {
82+
return this.params.showUrl ? this.url : (this.params.friendlyName ?? 'resource from URL')
83+
}
84+
85+
private logCancellation(event: CancelEvent) {
86+
getLogger().debug(`Download for "${this.logText()}" ${event.agent === 'user' ? 'cancelled' : 'timed out'}`)
87+
}
88+
89+
// TODO: make pipeLocation a vscode.Uri
90+
private pipeGetRequest(pipeLocation: string, timeout?: Timeout): FetcherResult {
91+
const requester = isCloud9() ? patchedGot : got
92+
const requestStream = requester.stream(this.url, { headers: this.buildRequestHeaders() })
93+
const fsStream = fs.createWriteStream(pipeLocation)
94+
95+
const done = new Promise<void>((resolve, reject) => {
96+
const pipe = stream.pipeline(requestStream, fsStream, (err) => {
97+
if (err instanceof RequestError) {
98+
return reject(Object.assign(new Error('Failed to download file'), { code: err.code }))
99+
}
100+
err ? reject(err) : resolve()
101+
})
102+
103+
const cancelListener = timeout?.token.onCancellationRequested((event) => {
104+
this.logCancellation(event)
105+
pipe.destroy(new CancellationError(event.agent))
106+
})
107+
108+
pipe.on('close', () => cancelListener?.dispose())
109+
})
110+
111+
return Object.assign(done, { requestStream, fsStream })
112+
}
113+
114+
private buildRequestHeaders(requestHeaders?: RequestHeaders): Headers {
115+
const headers: Headers = {}
116+
117+
headers['User-Agent'] = VSCODE_EXTENSION_ID.awstoolkit
118+
119+
if (requestHeaders?.eTag !== undefined) {
120+
headers['If-None-Match'] = requestHeaders.eTag
121+
}
122+
123+
if (requestHeaders?.gZip) {
124+
headers['Accept-Encoding'] = 'gzip'
125+
}
126+
127+
return headers
128+
}
129+
}

packages/core/src/shared/utilities/cliUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import * as vscode from 'vscode'
1111
import { getIdeProperties } from '../extensionUtilities'
1212
import { makeTemporaryToolkitFolder, tryRemoveFolder } from '../filesystemUtilities'
1313
import { getLogger } from '../logger'
14-
import { HttpResourceFetcher } from '../resourcefetcher/httpResourceFetcher'
14+
import { HttpResourceFetcher } from '../resourcefetcher/node/httpResourceFetcher'
1515
import { ChildProcess } from './processUtils'
1616

1717
import * as nls from 'vscode-nls'

packages/core/src/shared/utilities/pollingSet.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,14 @@ export class PollingSet<T> extends Set<T> {
4444
this.clearTimer()
4545
}
4646
}
47-
47+
// TODO(hkobew): Overwrite the add method instead of adding seperate method. If we add item to set, timer should always start.
4848
public start(id: T): void {
4949
this.add(id)
5050
this.pollTimer = this.pollTimer ?? globals.clock.setInterval(() => this.poll(), this.interval)
5151
}
52+
53+
public override clear(): void {
54+
this.clearTimer()
55+
super.clear()
56+
}
5257
}

0 commit comments

Comments
 (0)