Skip to content
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

Add --target electron-renderer (for generating electron preloads) #1928

Closed
wants to merge 1 commit into from

Conversation

silvanshade
Copy link
Contributor

@silvanshade silvanshade commented Dec 22, 2019

What does this PR do?

  1. It causes imports to be omitted from the pkg/output.js
  2. It creates a pkg/preloads.js with the omitted imports from 1), but in a format suitable for electron-renderer processes with node integration disabled

Why is this needed?

Background

Electron apps use a multi-process model. For this PR we only need to care about the renderer process. We assume that the primary renderer process app script is pkg/output.js.

The electron renderer process can make use of node modules, but (by default) only if they are provided through a preloads.js script. This is because the primary renderer process script (our pkg/output.js) does not have access to node require and will not resolve node modules. However, the preloads.js script does have node require in scope, and can make use of all the normal node functionality.

This separation is intended to prevent XSS to RCE attacks. You can read more about it here.

How this PR addresses this issue

The output generated from wasm-bindgen and wasm-pack, when using node modules, contains imports that use require. Since require is not available to the primary renderer process script, an electron app using this output will fail to load with an error about require not being in scope.

The solution to that problem is to omit the imports and to not generate code that uses require. That's what 1) does.

That still leaves the issue of how to make those imports available to the renderer script. That's what 2) does, by generating an appropriate preloads.js script.

Can't you just use webpack with one of the electron targets for this?

No. The webpack electron targets do not help with 1) or with 2).

What the electron-renderer and electron-preload targets do can be seen here.

For the most part they just disable the generation of chunked modules for certain globals (like electron, and ipc).

However, the electron-renderer target is still treated as a node target, where the generated modules are included into the final output using require, which is exactly what we don't want.

In fact, the webpack electron-renderer target does not work with current electron because of this. There is an issue referencing the problem here but no resolution.

Demonstration

I've uploaded two branches for illustration purposes.

Branch: webpack-activity-monitor

The first branch is webpack-activity-monitor.

If you checkout the code and go to the examples/activity-monitor, you can run the following to start the example:

npm i
npn run build
npm start

This branch adapts the template from rustwasm/rust-webpack-template but only uses webpack for the electron renderer script.

If you start the app, you can see that it doesn't function properly. In fact, it fails exactly at the parts where one of the imports (os.cpus from node-sys) is used. It doesn't throw an error, but the results are null (I'm not entirely sure why it behaves this way instead of throwing an error, but in any case it doesn't work).

If you uncomment the following line in the webpack.config.ts, you might expect that the example would work, since we are using the electron-renderer target. Instead, the example still fails, but now it does give an error -- specifically ReferenceError: require is not defined.

This demonstrates that the electron-renderer target doesn't address the problem this PR is intended to solve.

I have tried experimenting with several combinations of the plugins (e.g., externals, IgnorePlugin, etc.) to see if it is possible to ignore the imports in pkg/output.js but none of them see to do the right thing (see the next section).

Branch: webpack-activity-monitor-bundler

The second branch is webpack-activity-monitor-bundler.

This branch also adapts the template from rustwasm/rust-webpack-template and only uses webpack on the renderer script. The difference between this and the other branch is that I preprocess the pkg/output.js to strip the imports before being compiled with webpack. (See the pack-app-renderer-postprocess npm script.)

Now, if you run the following, the app compiles and runs as expected:

npm i
npn run build
npm start

This demonstrates the following:

  1. the imports in the pkg/output.js are the main source of the problem
  2. it's possible to use webpack and wasm-pack with the bundler target for electron (previously I didn't think this worked, but testing all of this has been complicated)

Regarding 2), I've verified that it's possible to use either the web target or the bundler target from wasm-pack for electron apps. Using the web target is simpler, but in real world apps it's more likely that the bundler target would be used for various reasons. That leads me to propose an alternative interface for this functionality.

Alternatives

Rather than adding --target electron-renderer, we could instead add two separate flags: --omit-module-imports and --generate-preloads. These flags would function independently of the choice of --target.

After some consideration, this is probably the better option. Most real world applications will probably end up using --target bundler, but some may still want to use --target web (especially modern apps making full use of modules or other bundlers like rollup).

Furthermore, real world applications are likely going to make use of multiple preload scripts with more complex structure than what we would auto-generate.

In fact, the most conservative solution would be to not even provide a --generate-preloads flag but only an --omit-module-imports flag. That might be okay, but it would still be nice to have some sort of feedback as to which modules are omitted so the user an idea as to how they should construct the preloads script. I don't know the ideal way to do this. The omitted imports could be reported on stdout or something at the very least.

@silvanshade
Copy link
Contributor Author

silvanshade commented Dec 27, 2019

Just want to mention that this is ready for review whenever someone gets a chance to take a look at it (/cc @Pauan @alexcrichton).

Please see the message I posted awhile back to the #wg-wasm discord channel (linked in the original post above).

To summarize though, a preload.js file needs to be generated somehow (this is orthogonal to whether or not webpack is used), and it can be and should be generated automatically in this case.

Secondly, the web target almost works perfectly for electron renderer process targets, but the module imports need to be omitted (and placed in a preload.js).

I have two examples where I need this target:

For the activity-monitor example, you can see where I've created a static preload.js file, and then an npm build step here which comments out the imports from the generated JS. This target would do all of that automatically.

I'd be happy to explain more about this on discord.

@alexcrichton
Copy link
Contributor

Thanks for the PR here!

I'm personally somewhat wary to add another output type as I feel we already have a few too many. Is there a standard/accepted way to take "normal JS" and make it suitable for use in electron, and if so could we perhaps recommend that process instead of adding a new output type?

Or otherwise, do you know about the stability of electron? How much does this sort of interface change over time? I'm a bit worried that electron isn't the most stable thing and would, in the limit, require some sort of versioning on our end or something like that which would be a bummer.

@silvanshade
Copy link
Contributor Author

silvanshade commented Jan 7, 2020

I'm personally somewhat wary to add another output type as I feel we already have a few too many.

That's a reasonable concern. I think to some extent this might be unavoidable though just given the complexity of building and deploying apps that use all of this technology and given the current state of the overall ecosystem.

Maybe there is a need to split some part of the code generation out of bindgen so that it doesn't seem as much of a commitment to add a new backend? Not sure how that would work but it might be something to consider.

Is there a standard/accepted way to take "normal JS" and make it suitable for use in electron, and if so could we perhaps recommend that process instead of adding a new output type?

Not that I know of, unfortunately.

The standard tools for repackaging/rewriting JS in this situation would be webpack or babel. In theory it shouldn't be terribly difficult to write a babel transform that would accomplish the same thing, but that would require another separate (and significant) dependency and complicate the workflow.

I don't know of any webpack plugin that does this directly but it may be possible to write one.

Although both of those alternatives could potentially work, I feel they would be a bit of a heavyweight solution to this problem and come with a worse user experience.

The webpack workflow in particular already seems fragile and I've run into a number of issues getting it to work correctly with current versions of all the other tooling (including electron). Some of that might be due to the relative immaturity of the webassembly and module loading support.

Or otherwise, do you know about the stability of electron? How much does this sort of interface change over time? I'm a bit worried that electron isn't the most stable thing and would, in the limit, require some sort of versioning on our end or something like that which would be a bummer.

I don't think that the preload interface changes very often at this point. The earliest reference I can find to the preload script functionality is in the changelog for Electron 0.37.6 on April 15, 2016. Current stable release is 7.1.7 from December 19, 2019.

It used to be that preload scripts were not required but what happened is node integration for renderer process scripts was disabled in version 5.0.0, which means that require and all the related functionality from node are not available in the main scripts anymore.

Preload scripts were supported before that but now they are required for node integration (unless you override some defaults settings making your app insecure and which we shouldn't recommend).

Unfortunately you can't do much interesting with electron apps without some level of node integration so it's hard to avoid dealing with this somehow.

As a point of reference, the list of targets webpack supports is:

  • async-node
  • electron-main
  • electron-renderer
  • electron-preload
  • node
  • node-webkit
  • web
  • webworker

I don't think it's necessary to support all of those targets here; I'd just point out that electron support is important enough to webpack to get it's own target. (Those targets don't help with generating the preload file like we need here though).

One final thing I would say is that I think it would be fine if this functionality were to be exposed but marked as experimental or undocumented (similar to the node module support) and subject to future change or removal. I don't really know how many people would even use this. But having it would certainly make building electron apps in Rust more straightforward.

@alexcrichton
Copy link
Contributor

Is it possible to use the bundler output and then run that through webpack with the electron target?

I find it pretty unfortunate how there's so many JS environments each with slightly enough tweaks to warrant new targets. You're right that we don't want to support all the webpack targets that are supported, but I feel like the line needs to be drawn somewhere so we're not just constantly chasing whatever the new JS environment happens to be.

@silvanshade
Copy link
Contributor Author

Is it possible to use the bundler output and then run that through webpack with the electron target?

It is possible to do that but the electron target doesn't generate the preloads so it doesn't accomplish the same thing as this.

There are two aspects to this which are useful:

  1. omitting the module imports from the pkg/output.js
  2. generating the pkg/preload.js (which just so happens to use the same imports that were omitted in step 1)

Normally, the user is expected to write the preloads file by hand (since everything is JS). That's not such a huge requirement, but here we have exactly the right information to generate it automatically.

I'm not sure we have a way to accomplish the step 1) with the bundler.

It might be possible using one of the plugins to manually go through and exclude each dependency, but then that requires an awful lot of manual configuration from the user to get electron to work (i.e., use the bundler target, configure the excludes, write the preloads, on top of creating whatever bindings you need), which I worry would turn many people away.

But aside from that, I wasn't able to get the bundler target to work for the projects I linked. I spent about a week working with webpack (already having prior experience with it), trying all the different targets, and I spoke with @Pauan about it some as well. I never got it to work properly, just got various different cryptic errors when loading the app.

I also ran into a problem with webpack running extremely slow (~10 minutes) on the output generated from wasm-pack (the output from compiling electron-sys is large). @Pauan suggested that this might be due to a bug in the webassembly processing, and that sounds plausible.

You're right that we don't want to support all the webpack targets that are supported, but I feel like the line needs to be drawn somewhere so we're not just constantly chasing whatever the new JS environment happens to be.

That's fair. However, I do think as far as these things go, electron is a reasonably safe target to consider given the number of large applications that use it (e.g., Slack, Discord, VS Code, etc.) and given that webpack themselves have committed to supporting it.

If someone can show that there's a way to do the same thing as this PR with webpack and a reasonably straight forward workflow, I'd be more likely to advocate for that instead of a new target. But I tried and couldn't figure out how to do that.

@alexcrichton
Copy link
Contributor

Hm well so to be clear I also understand basically nothing about electron, so I have no idea what the design space is here. I'm mostly just sort of praying that we don't have to maintain this in wasm-bindgen because, well, I don't think we can maintain this realistically.

I can give this a rubber stamp that it's at least isolated from the rest of the codebase but I don't think we can realistically given any sort of guarantee about correctness, reliability, maintenance over time, stability over time, etc. This would likely be very difficult to test in-tree as well so it's not really something we can add to CI as well.

Unfortunately we're basically between a rock and a hard place. I think the best we can do is to try to merge support for this but be very clear in the documentation that it may not work and it may regress.

(and that reminds me that updating the documentation would be good too!)

@silvanshade
Copy link
Contributor Author

Hm well so to be clear I also understand basically nothing about electron, so I have no idea what the design space is here.

Fair enough. I was working on an update to the original message which addressed this a bit more, linking to some of the relevant background information on why electron works this way and why we can't easily solve this problem just with the bundler target and webpack (along with an explanation of what the webpack electron targets actually do). I will go ahead and update that (perhaps tonight if possible) so that the overall picture is clearer.

I can give this a rubber stamp that it's at least isolated from the rest of the codebase but I don't think we can realistically given any sort of guarantee about correctness, reliability, maintenance over time, stability over time, etc.

I think that's okay. Being able to write Electron apps in Rust and have them work without one-off ad-hoc postprocessing scripts is still an important step. It will help convince people that Rust is a viable choice for this kind of work.

I'm also willing to help maintain this code since it's necessary in order to use electron-sys, which I'm already maintaining and continuing to develop.

This would likely be very difficult to test in-tree as well so it's not really something we can add to CI as well.

Hmm. How are the other targets currently tested? It should be possible to do some sort of testing with Electron but it depends on what is needed. At a high level, one possibility is that Electron can be installed as a package.json dependency, so we could have a small template app that would get compiled using this backend, which would start the app (and optionally perform some actions, perhaps using webdriver) and then check for successful exit or something like that.

(and that reminds me that updating the documentation would be good too!)

I can do that. Can you point me to where the documentation should be updated?

@silvanshade
Copy link
Contributor Author

One other thing, before finalizing this, there is actually an alternative interface I've considered in the last few days that should accomplish the same thing but might be more desirable from a maintenance and testing point of view. I'll describe that when I update with the other info I mentioned.

@silvanshade
Copy link
Contributor Author

@alexcrichton I've updated the original post with more info about all of this, along with an alternative approach that I think might be better, especially given your concerns about adding another target. Let me know what you think.

Copy link
Contributor

@alexcrichton alexcrichton left a comment

Choose a reason for hiding this comment

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

Ok, we can try adding this and see how it goes. I think the way this works is:

  • If necessary for other features, this will be deleted. You'll get pinged of course though and we can work out what's difficult/etc. I want to point out though that I don't want to hinder other development for this one feature which few of us understand.
  • If there's questions/review/etc you'll get pinged (e.g. issues, PRs, ...)

Does that sound ok?

Can this also update the documentation in guide/* with various words about this new update type and how it's somewhat experimental?

}

fn js_import_header(&self) -> Result<String, Error> {
let mut imports = String::new();
fn js_import_header(&self, imports: &mut String, preloads: &mut String) -> Result<(), Error> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of taking two out-parameters could this return a structure with named fields that document the meaning of each field?

@@ -194,9 +198,10 @@ impl<'a> Context<'a> {
&mut self,
module_name: &str,
needs_manual_start: bool,
) -> Result<(String, String), Error> {
) -> Result<(String, String, Option<String>), Error> {
Copy link
Contributor

Choose a reason for hiding this comment

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

This return type is getting unfortunately pretty unwieldy, could this be updated to a named structure with documented fields?

@@ -182,6 +184,13 @@ impl Bindgen {
Ok(self)
}

pub fn electron_renderer(&mut self, web: bool) -> Result<&mut Bindgen, Error> {
if web {
Copy link
Contributor

Choose a reason for hiding this comment

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

The variable "web" here probably wants a different name

@@ -35,6 +35,7 @@ Options:
--remove-producers-section Remove the telemetry `producers` section
--encode-into MODE Whether or not to use TextEncoder#encodeInto,
valid values are [test, always, never]
--electron-renderer Deprecated, use `--target electron-renderer`
Copy link
Contributor

Choose a reason for hiding this comment

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

It's ok to not add this since there's no need to add an insta-deprecated option

@silvanshade
Copy link
Contributor Author

Thanks for the review. I can make the changes suggested.

Before proceeding to finalize and merge this though I would like to implement the alternative proposal I made (see the "Alternatives" section in the original post) for comparison. I'll make a separate PR for that.

Does that sound ok?

Yeah, those conditions sound fine.

Can this also update the documentation in guide/* with various words about this new update type and how it's somewhat experimental?

Okay, I'll take a look and update the relevant sections.

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.

3 participants