Skip to content

Commit aa3f1a7

Browse files
feat(utils): Add some util functions for upcoming feature (#5657)
This adds some util functions that are needed for an upcoming feature. Each commit is its own isolated util, so review them individually. **A significant amount of the changes come from minor refactors in files** --- <!--- REMINDER: Ensure that your PR meets the guidelines in CONTRIBUTING.md --> License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --------- Signed-off-by: nkomonen-amazon <[email protected]>
1 parent 359b1d8 commit aa3f1a7

File tree

18 files changed

+642
-131
lines changed

18 files changed

+642
-131
lines changed

package-lock.json

Lines changed: 46 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4103,6 +4103,7 @@
41034103
"@types/node": "^16.18.95",
41044104
"@types/node-fetch": "^2.6.8",
41054105
"@types/prismjs": "^1.26.0",
4106+
"@types/proper-lockfile": "^4.1.4",
41064107
"@types/readline-sync": "^1.4.3",
41074108
"@types/semver": "^7.5.0",
41084109
"@types/sinon": "^10.0.5",
@@ -4182,6 +4183,8 @@
41824183
"mime-types": "^2.1.32",
41834184
"node-fetch": "^2.7.0",
41844185
"portfinder": "^1.0.32",
4186+
"proper-lockfile": "^4.1.2",
4187+
"ps-list": "^8.1.1",
41854188
"semver": "^7.5.4",
41864189
"stream-buffers": "^3.0.2",
41874190
"strip-ansi": "^5.2.0",

packages/core/src/shared/extensionUtilities.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ export function productName() {
4141
return isAmazonQ() ? 'Amazon Q' : `${getIdeProperties().company} Toolkit`
4242
}
4343

44+
export const getExtensionId = () => {
45+
return isAmazonQ() ? VSCODE_EXTENSION_ID.amazonq : VSCODE_EXTENSION_ID.awstoolkit
46+
}
47+
4448
/** Gets the "AWS" or "Amazon Q" prefix (in package.json: `commands.category`). */
4549
export function commandsPrefix(): string {
4650
return isAmazonQ() ? 'Amazon Q' : getIdeProperties().company

packages/core/src/shared/fs/fs.ts

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import crypto from 'crypto'
2323
import { waitUntil } from '../utilities/timeoutUtils'
2424
import { telemetry } from '../telemetry/telemetry'
2525
import { getLogger } from '../logger/logger'
26+
import { toUri } from '../utilities/uriUtils'
2627

2728
const vfs = vscode.workspace.fs
2829
type Uri = vscode.Uri
@@ -88,7 +89,7 @@ export class FileSystem {
8889

8990
/** Creates the directory as well as missing parent directories. */
9091
async mkdir(path: Uri | string): Promise<void> {
91-
const uri = this.#toUri(path)
92+
const uri = toUri(path)
9293
const errHandler = createPermissionsErrorHandler(this.isWeb, vscode.Uri.joinPath(uri, '..'), '*wx')
9394

9495
// Certain URIs are not supported with vscode.workspace.fs in Cloud9
@@ -105,7 +106,7 @@ export class FileSystem {
105106

106107
// TODO: rename to readFileBytes()?
107108
async readFile(path: Uri | string): Promise<Uint8Array> {
108-
const uri = this.#toUri(path)
109+
const uri = toUri(path)
109110
const errHandler = createPermissionsErrorHandler(this.isWeb, uri, 'r**')
110111

111112
if (isCloud9()) {
@@ -117,7 +118,7 @@ export class FileSystem {
117118

118119
// TODO: rename to readFile()?
119120
async readFileAsString(path: Uri | string, decoder: TextDecoder = FileSystem.#decoder): Promise<string> {
120-
const uri = this.#toUri(path)
121+
const uri = toUri(path)
121122
const bytes = await this.readFile(uri)
122123
return decoder.decode(bytes)
123124
}
@@ -127,7 +128,7 @@ export class FileSystem {
127128
* so we must do it ourselves (this implementation is inefficient).
128129
*/
129130
async appendFile(path: Uri | string, content: Uint8Array | string): Promise<void> {
130-
path = this.#toUri(path)
131+
path = toUri(path)
131132

132133
const currentContent: Uint8Array = (await this.existsFile(path)) ? await this.readFile(path) : new Uint8Array(0)
133134
const currentLength = currentContent.length
@@ -146,7 +147,7 @@ export class FileSystem {
146147
if (path === undefined || path === '') {
147148
return false
148149
}
149-
const uri = this.#toUri(path)
150+
const uri = toUri(path)
150151
if (uri.fsPath === undefined || uri.fsPath === '') {
151152
return false
152153
}
@@ -212,7 +213,7 @@ export class FileSystem {
212213
data: string | Uint8Array,
213214
opts?: WriteFileOptions & { atomic?: boolean }
214215
): Promise<void> {
215-
const uri = this.#toUri(path)
216+
const uri = toUri(path)
216217
const errHandler = createPermissionsErrorHandler(this.isWeb, uri, '*w*')
217218
const content = this.#toBytes(data)
218219

@@ -249,7 +250,7 @@ export class FileSystem {
249250
// 3. Finally, do a regular file write, but may result in invalid file content
250251
//
251252
// For telemetry, we will only report failures as to not overload with succeed events.
252-
const tempFile = this.#toUri(`${uri.fsPath}.${crypto.randomBytes(8).toString('hex')}.tmp`)
253+
const tempFile = toUri(`${uri.fsPath}.${crypto.randomBytes(8).toString('hex')}.tmp`)
253254
try {
254255
await write(tempFile)
255256
await fs.rename(tempFile, uri)
@@ -290,8 +291,8 @@ export class FileSystem {
290291
}
291292

292293
async rename(oldPath: vscode.Uri | string, newPath: vscode.Uri | string) {
293-
const oldUri = this.#toUri(oldPath)
294-
const newUri = this.#toUri(newPath)
294+
const oldUri = toUri(oldPath)
295+
const newUri = toUri(newPath)
295296
const errHandler = createPermissionsErrorHandler(this.isWeb, oldUri, 'rw*')
296297

297298
if (isCloud9()) {
@@ -352,7 +353,7 @@ export class FileSystem {
352353
* The stat of the file, throws if the file does not exist or on any other error.
353354
*/
354355
async stat(uri: vscode.Uri | string): Promise<vscode.FileStat> {
355-
const path = this.#toUri(uri)
356+
const path = toUri(uri)
356357
return await vfs.stat(path)
357358
}
358359

@@ -370,7 +371,7 @@ export class FileSystem {
370371
async delete(fileOrDir: string | vscode.Uri, opt_?: { recursive?: boolean; force?: boolean }): Promise<void> {
371372
const opt = { ...opt_, recursive: !!opt_?.recursive }
372373
opt.force = opt.force === false ? opt.force : !!(opt.force || opt.recursive)
373-
const uri = this.#toUri(fileOrDir)
374+
const uri = toUri(fileOrDir)
374375
const parent = vscode.Uri.joinPath(uri, '..')
375376
const errHandler = createPermissionsErrorHandler(this.isWeb, parent, '*wx')
376377

@@ -427,7 +428,7 @@ export class FileSystem {
427428
}
428429

429430
async readdir(uri: vscode.Uri | string): Promise<[string, vscode.FileType][]> {
430-
const path = this.#toUri(uri)
431+
const path = toUri(uri)
431432

432433
// readdir is not a supported vscode API in Cloud9
433434
if (isCloud9()) {
@@ -441,8 +442,8 @@ export class FileSystem {
441442
}
442443

443444
async copy(source: vscode.Uri | string, target: vscode.Uri | string): Promise<void> {
444-
const sourcePath = this.#toUri(source)
445-
const targetPath = this.#toUri(target)
445+
const sourcePath = toUri(source)
446+
const targetPath = toUri(target)
446447
return await vfs.copy(sourcePath, targetPath, { overwrite: true })
447448
}
448449

@@ -454,7 +455,7 @@ export class FileSystem {
454455
async checkPerms(file: string | vscode.Uri, perms: PermissionsTriplet): Promise<void> {
455456
// TODO: implement checkExactPerms() by checking the file mode.
456457
// public static async checkExactPerms(file: string | vscode.Uri, perms: `${PermissionsTriplet}${PermissionsTriplet}${PermissionsTriplet}`)
457-
const uri = this.#toUri(file)
458+
const uri = toUri(file)
458459
const errHandler = createPermissionsErrorHandler(this.isWeb, uri, perms)
459460
const flags = Array.from(perms) as (keyof typeof this.modeMap)[]
460461
const mode = flags.reduce((m, f) => m | this.modeMap[f], nodeConstants.F_OK)
@@ -634,19 +635,6 @@ export class FileSystem {
634635
return data
635636
}
636637

637-
/**
638-
* Retrieve the Uri of the file.
639-
*
640-
* @param path The file path for which to retrieve metadata.
641-
* @return The Uri about the file.
642-
*/
643-
#toUri(path: string | vscode.Uri): vscode.Uri {
644-
if (path instanceof vscode.Uri) {
645-
return path
646-
}
647-
return vscode.Uri.file(path)
648-
}
649-
650638
private get modeMap() {
651639
return {
652640
'*': 0,
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import vscode from 'vscode'
7+
import _path from 'path'
8+
import _fsNode from 'fs/promises'
9+
import lockFile, { LockOptions } from 'proper-lockfile'
10+
import { ToolkitError } from '../../errors'
11+
import { toUri } from '../../utilities/uriUtils'
12+
13+
/**
14+
* FileSystem related methods that only work in Node.js
15+
*/
16+
export class NodeFileSystem {
17+
protected constructor() {}
18+
static #instance: NodeFileSystem
19+
static get instance(): NodeFileSystem {
20+
return (this.#instance ??= new NodeFileSystem())
21+
}
22+
23+
/**
24+
* Acquires a lock on the given file, then runs the given callback, releasing the lock when done.
25+
*
26+
* The reason this is node specific:
27+
* - The proper-lockfile module uses the mkdir() method for lockfiles as it behaves as an atomic exists + make lockfile
28+
* - But the VSC Filesystem implementation does not throw an error when the lock file (directory) already exists,
29+
* so we need to use the node fs module, which does not work in web.
30+
*
31+
* @param uri The uri of the file to lock
32+
* @param lockId Some way to identify who acquired the lock
33+
* @param callback The callback to run once the lock is acquired
34+
*/
35+
public async lock(uri: vscode.Uri | string, callback: () => Promise<void>): Promise<void> {
36+
let release = undefined
37+
try {
38+
try {
39+
const path = toUri(uri).fsPath
40+
release = await lockFile.lock(path, this.lockOptions)
41+
} catch (err) {
42+
if (!(err instanceof Error)) {
43+
throw err
44+
}
45+
throw ToolkitError.chain(err, `Failed in lock: ${uri}`, { code: 'NodeLockError' })
46+
}
47+
48+
try {
49+
return await callback()
50+
} catch (err) {
51+
if (!(err instanceof Error)) {
52+
throw err
53+
}
54+
throw ToolkitError.chain(err, `Failed in callback of lock: ${uri}`, { code: 'NodeLockError' })
55+
}
56+
} finally {
57+
await release?.()
58+
}
59+
}
60+
61+
protected get lockOptions() {
62+
const options: LockOptions = {
63+
stale: 5000, // lockfile becomes stale after this many millis
64+
update: 1000, // update lockfile mtime every this many millis, useful for a long running callback that exceeds the time
65+
retries: {
66+
maxRetryTime: 10_000, // How long to try to acquire the lock before giving up
67+
minTimeout: 100, // How long to wait between each retrying, but changes with exponential backoff
68+
factor: 2, // Exponential backoff (doubles each retry)
69+
},
70+
}
71+
return options
72+
}
73+
}
74+
75+
export const fsNode = NodeFileSystem.instance
76+
export default NodeFileSystem.instance

packages/core/src/shared/globalState.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ type globalKey =
5858
| 'isExtensionFirstUse'
5959
| 'lastExtensionVersion'
6060
| 'lastSelectedRegion'
61+
| 'lastOsStartTime'
6162
| 'recentCredentials'
6263
// List of regions enabled in AWS Explorer.
6364
| 'region'

0 commit comments

Comments
 (0)