-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Alternative aproach to plugin auto update #3629
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
Conversation
Veetaha
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All the bloat has gone away...
Downgrading to stable is tricky. nightly has a greater semver version and vscode doesn't delete extension bundle when you uninstall it right away. Thus, when the user clicks uninstall and install, vscode looks into the cache and sees that it already has the version that is greater than the one on the marketplace and just resurrects the already installed nightly back. Though I am not sure that this is true.
editors/code/src/config.ts
Outdated
| private get serverPath() { return this.cfg.get("serverPath") as null | string; } | ||
| get updatesChannel() { return this.cfg.get("updates.channel") as UpdatesChannel; } | ||
| get serverPath() { return this.cfg.get("serverPath") as null | string; } | ||
| get channel(): "stable" | "nightly" { return this.cfg.get("updates.channel") ?? "stable"; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| get channel(): "stable" | "nightly" { return this.cfg.get("updates.channel") ?? "stable"; } | |
| get channel() { return this.cfg.get("updates.channel") as UpdatesChannel; } |
The default stable should be returned due to the declaration in package.json configurations contribution point.
And I'd like to preserve the explicit as cast, since this is a downcast any -> UpdatesChannel
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And I'd like to preserve the explicit as cast, since this is a downcast any -> UpdatesChannel
The type of get is
get<T>(section: string): T | undefined;So, this is explicitely to get rid of |undefined I guess an ! would also work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess we should change all other configs to assert non undefined instead of as, but that's for other PR
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me elaborate here. The design of this get<T>() method is quite unsound. It lets you pass a type argument that will denote its return type.
const string | undefined = cfg.get<string>("name");But that is very deceiving. By looking at this code you won't expect it to fail here since there is nothing unsound in it at the first glance.
However, what happens under the hood is the unchecked downcast any -> T.
Here in vscode impl you can see that this function returns a variable of type any:

And any is considered harmful, since it is implicitly convertible to literally any type (instead of any it is now recommended to use unknown).
So the user does can pass a value of some other type other than declared in our json schema in package.json and this will fail at runtime, i.e.: see this comment
https://github.com/rust-analyzer/rust-analyzer/blob/2a3543d1953daeb240c214ececd273f1040b0516/editors/code/src/config.ts#L186-L187
My concern is to mark such unsound casts explicitly with as instead of silently passing the type arguments to the function that does an implicit cast under the hood so we don't even notice the unsoundness.
I know you are used to trusting vscode guys, but I don't. They are the same people as we and they do the same mistakes as we too and this is one of their bad design decisions because the proper signature for this function would be
get(opt: string): unknown;
or they should throw an error if the input doesn't match the declared json schema type in package.json.
Anyway, I'll at other the code later today!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: type arguments, ! null assertion and as T casts have no influence on the runtime behavior. They are just to silence the compiler complaints.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is out of scope of this PR, but I'd prefer to just run with upstream API. If they cast any to T, then they probably have the reasons to do so. In general, VS Code code base is of impecable code quality, so, in general, I trust them blindly when it comes to TypeScript.
For the case of settings in the editor, I independently think that lenient validation is the right approach. It's more important that the thing works with broken settings, than to bail loudly on errors, in this context.
79c6828 to
7869bc0
Compare
|
Should be ready for review now! r? @Veetaha |
|
#3635 further simplifies tag discovery |
| "request": "launch", | ||
| "runtimeExecutable": "${execPath}", | ||
| "args": [ | ||
| // "--user-data-dir=${workspaceFolder}/target/code", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this option for? Does this overwrite the globalStoragePath?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, this overrides storage path, extensions and settings, and it makes it easier to look inside the store. It’s deliberately commented out, so that it can be easily restored when needed
| } | ||
|
|
||
| const version = spawnSync(binaryPath, ["--version"], { encoding: "utf8" }).stdout; | ||
| const version = spawnSync(ctx.serverPath, ["--version"], { encoding: "utf8" }).stdout; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should still use getServer() thing here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will require adding persistentState to ctx, which I’d rather avoid.
| Stable = "stable", | ||
| Nightly = "nightly" | ||
| } | ||
| export type UpdatesChannel = "stable" | "nightly"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good thing that TypeScript allows for stringly-typed apis, though I prefer enums, but no too strong arguments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, makes sense, will update tomorrow
editors/code/src/config.ts
Outdated
| private get serverPath() { return this.cfg.get("serverPath") as null | string; } | ||
| get updatesChannel() { return this.cfg.get("updates.channel") as UpdatesChannel; } | ||
| get serverPath() { return this.cfg.get("serverPath") as null | string; } | ||
| get channel(): "stable" | "nightly" { return this.cfg.get("updates.channel", "stable"); } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my comments on the outdated code #3629 (comment).
And there is no need to specify the default stable. This is what I tried to eliminate in #3131, the second source of truth!
editors/code/src/ctx.ts
Outdated
| private constructor( | ||
| readonly config: Config, | ||
| readonly state: PersistentState, | ||
| // readonly state: PersistentState, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we won't use getServer() in the command handler, then we don't need this comment
| // readonly state: PersistentState, |
| vscode.window.showWarningMessage(`You are running a nightly version of rust-analyzer extension. | ||
| To switch to stable, uninstall the extension and re-install it from the marketplace`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd suggest testing that this does work. I.e. uninstalling and installing does switch us to stable.
(see my previous review summary comment #3629 (review))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yup, verified manually that it works.
editors/code/src/main.ts
Outdated
| }; | ||
|
|
||
| const lastCheck = state.lastCheck; | ||
| const now = new Date().getTime(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| const now = new Date().getTime(); | |
| const now = Date.now(); |
to your service )
| return; | ||
| }; | ||
|
|
||
| const lastCheck = state.lastCheck; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| const lastCheck = state.lastCheck; | |
| const { lastCheck } = state; |
This looks more cyberpunky
| if (!shouldDownloadNightly) return; | ||
|
|
||
| const release = await fetchRelease("nightly").catch((e) => { | ||
| log.error(e); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| log.error(e); | |
| log.downloadError(e); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, no, we only need to show message if this was not an auto-update attempt.
| const dest = path.join(config.globalStoragePath, "rust-analyzer.vsix"); | ||
| await download(artifact.browser_download_url, dest, "Downloading rust-analyzer extension"); | ||
|
|
||
| await vscode.commands.executeCommand("workbench.extensions.installExtension", vscode.Uri.file(dest)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Either we should use vscodeInstallExtension() from util.ts or remove that code from there.
| if (state.releaseId === undefined) { // Show error only for the initial download | ||
| vscode.window.showErrorMessage(`Failed to download rust-analyzer nightly ${e}`); | ||
| } | ||
| return undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should definitely be consistent on using undefined vs null. I know that e.g. TypeScript compiler team never uses null and dart guys always use it , but there is no general opinion on this question and I don't have one too. Though, should we choose one and stick with it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
undefined it is: when in doubt, do what github.com/Microsoft/vscode-languageserver-node/ does
editors/code/src/main.ts
Outdated
| if (release === undefined || release.id === state.releaseId) return; | ||
|
|
||
| const updateOrCancel = await vscode.window.showInformationMessage( | ||
| "New version of rust-analyzer (nightly) is available (requires restart).", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| "New version of rust-analyzer (nightly) is available (requires restart).", | |
| "New version of rust-analyzer (nightly) is available (requires reload).", |
Let's use reload, it seems to be pervasive in vscode ecosystem.
| const exists = await fs.stat(dest).then(() => true, () => false); | ||
| if (!exists) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| const exists = await fs.stat(dest).then(() => true, () => false); | |
| if (!exists) { | |
| if (!fs.existsSync(dest)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FIY: the async fs.exists() version is deprecated...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, and for this we should amend the import:
import { promises as fs, existsSync as doesFileExist } from "fs";There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe async stat is the more correct approach.
| await fs.promises.mkdir(installationDir).catch(err => assert( | ||
| err?.code === "EEXIST", | ||
| `Couldn't create directory "${installationDir}" to download ` + | ||
| `${artifactFileName} artifact: ${err?.message}` | ||
| )); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This mkdir is for a reason
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent catch!
|
|
||
| get(): T { | ||
| const val = this.storage.get(this.key, this.defaultVal); | ||
| log.debug(this.key, "==", val); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I''d suggest to log the persistent state at least somewhere.
Everything now happens in main.ts, in the bootstrap family of functions. The current flow is: * check everything only on extension installation. * if the user is on nightly channel, try to download the nightly extension and reload. * when we install nightly extension, we persist its release id, so that we can check if the current release is different. * if server binary was not downloaded by the current version of the extension, redownload it (we persist the version of ext that downloaded the server).
|
bors r+ |
Build succeeded |
|
bors r+ |
|
Already running a review |
|
bors merge |
|
Already running a review |
This is very much WIP (as in, I haven't run this once), but I like the result so far.
cc @Veetaha
The primary focus here on simplification:
main.ts, implemented as linear functions without abstractions. This is stateful side-effective code, so arguments from Carmack very much apply. We need all user interractions, all mutations, and all network requests to happen in a single file.