Skip to content

Conversation

@matklad
Copy link
Contributor

@matklad matklad commented Mar 17, 2020

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:

  • local simplification of data structures and control-flow: using union of strings instead of an enum, using unwrapped GitHub API responses
  • global simplification of control flow: all logic is now in 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.
  • as a side-effect of condensing everything to functions, we can get rid of various enums. The enums were basically a reified control flow:
enum E { A, B }

fn foo() -> E {
    if cond { E::A } else { E::B }
}

fn bar(e: E) {
    match e {
        E::A => do_a(),
        E::B => do_b(),
    }
}

==>>

fn all() {
    if cond { do_a() } else { do_b() }
}
  • simplification of model: we don't need to reinstall on settings update, we can just ask the user to reload, we don't need to handle nightly=>stable fallback, we can ask the user to reinstall extension, (todo) we don't need to parse out the date from the version, we can use build id for nightly and for stable we can write the info directly into package.json.

Copy link
Contributor

@Veetaha Veetaha left a 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.

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"; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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

Copy link
Contributor Author

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?

Copy link
Contributor Author

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

Copy link
Contributor

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:
image

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!

Copy link
Contributor

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.

Copy link
Contributor Author

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.

@matklad matklad force-pushed the bootstrap branch 3 times, most recently from 79c6828 to 7869bc0 Compare March 18, 2020 11:29
@matklad
Copy link
Contributor Author

matklad commented Mar 18, 2020

Should be ready for review now!

r? @Veetaha

@matklad
Copy link
Contributor Author

matklad commented Mar 18, 2020

#3635 further simplifies tag discovery

"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
// "--user-data-dir=${workspaceFolder}/target/code",
Copy link
Contributor

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?

Copy link
Contributor Author

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;
Copy link
Contributor

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?

Copy link
Contributor Author

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";
Copy link
Contributor

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.

Copy link
Contributor Author

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

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"); }
Copy link
Contributor

@Veetaha Veetaha Mar 18, 2020

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!

private constructor(
readonly config: Config,
readonly state: PersistentState,
// readonly state: PersistentState,
Copy link
Contributor

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

Suggested change
// readonly state: PersistentState,

Comment on lines +112 to +116
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`);
Copy link
Contributor

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))

Copy link
Contributor Author

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.

};

const lastCheck = state.lastCheck;
const now = new Date().getTime();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const now = new Date().getTime();
const now = Date.now();

to your service )

return;
};

const lastCheck = state.lastCheck;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const lastCheck = state.lastCheck;
const { lastCheck } = state;

This looks more cyberpunky

if (!shouldDownloadNightly) return;

const release = await fetchRelease("nightly").catch((e) => {
log.error(e);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
log.error(e);
log.downloadError(e);

Copy link
Contributor Author

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));
Copy link
Contributor

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;
Copy link
Contributor

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?

Copy link
Contributor Author

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

if (release === undefined || release.id === state.releaseId) return;

const updateOrCancel = await vscode.window.showInformationMessage(
"New version of rust-analyzer (nightly) is available (requires restart).",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"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.

Comment on lines +205 to +208
const exists = await fs.stat(dest).then(() => true, () => false);
if (!exists) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const exists = await fs.stat(dest).then(() => true, () => false);
if (!exists) {
if (!fs.existsSync(dest)) {

Copy link
Contributor

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...

Copy link
Contributor

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";

Copy link
Contributor Author

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.

Comment on lines -69 to -73
await fs.promises.mkdir(installationDir).catch(err => assert(
err?.code === "EEXIST",
`Couldn't create directory "${installationDir}" to download ` +
`${artifactFileName} artifact: ${err?.message}`
));
Copy link
Contributor

@Veetaha Veetaha Mar 18, 2020

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

Copy link
Contributor Author

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);
Copy link
Contributor

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).
@matklad
Copy link
Contributor Author

matklad commented Mar 19, 2020

bors r+

@bors
Copy link
Contributor

bors bot commented Mar 19, 2020

@bors bors bot merged commit aca3c30 into rust-lang:master Mar 19, 2020
@matklad matklad deleted the bootstrap branch March 19, 2020 08:13
@matklad
Copy link
Contributor Author

matklad commented Mar 19, 2020

bors r+

@bors
Copy link
Contributor

bors bot commented Mar 19, 2020

Already running a review

@matklad
Copy link
Contributor Author

matklad commented Mar 19, 2020

bors merge

@bors
Copy link
Contributor

bors bot commented Mar 19, 2020

Already running a review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants