fix(napi): check if the tokio runtime exists when registering the module

And recreate it if it does not exist.

In windows, electron renderer process will crash if:
1. Import some NAPI module that enable `tokio-rt` flag in renderer process.
2. Reload the page.
3. Call a function imported from that NAPI module.

Because the tokio runtime will be dropped when reloading the page, and won't create again, but currently we assume that the runtime must exist in tokio-based `within_runtime_if_available`.
This will cause some panic like this:

```
thread '<unnamed>' panicked at 'called `Option::unwrap()` on a `None` value', napi-rs\crates\napi\src\tokio_runtime.rs:72:42
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Error: Renderer process crashed: crashed, exitCode: -529697949
    at EventEmitter.<anonymous> (napi-rs\examples\napi\electron.js:33:9)
    at EventEmitter.emit (node:events:525:35)
```
This commit is contained in:
Bo 2023-03-28 12:03:00 +08:00 committed by GitHub
parent a956d51a9a
commit e47c13f177
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 92 additions and 15 deletions

View file

@ -204,6 +204,8 @@ overrides:
parserOptions:
project:
- ./examples/tsconfig.json
rules:
'import/no-extraneous-dependencies': 0
- files:
- ./bench/**/*.{ts,js}

View file

@ -488,6 +488,8 @@ unsafe extern "C" fn napi_register_module_v1(
#[cfg(all(windows, feature = "napi4", feature = "tokio_rt"))]
{
crate::tokio_runtime::ensure_runtime();
crate::tokio_runtime::RT_REFERENCE_COUNT.fetch_add(1, Ordering::SeqCst);
unsafe {
sys::napi_add_env_cleanup_hook(

View file

@ -1,11 +1,11 @@
use std::future::Future;
use std::{future::Future, sync::RwLock};
use once_cell::sync::Lazy;
use tokio::runtime::Runtime;
use crate::{sys, JsDeferred, JsUnknown, NapiValue, Result};
pub(crate) static mut RT: Lazy<Option<Runtime>> = Lazy::new(|| {
fn create_runtime() -> Option<Runtime> {
#[cfg(not(target_arch = "wasm32"))]
{
let runtime = tokio::runtime::Runtime::new().expect("Create tokio runtime failed");
@ -19,20 +19,32 @@ pub(crate) static mut RT: Lazy<Option<Runtime>> = Lazy::new(|| {
.build()
.ok()
}
});
}
pub(crate) static RT: Lazy<RwLock<Option<Runtime>>> = Lazy::new(|| RwLock::new(create_runtime()));
#[cfg(windows)]
pub(crate) static RT_REFERENCE_COUNT: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
/// Ensure that the Tokio runtime is initialized.
/// In windows the Tokio runtime will be dropped when Node env exits.
/// But in Electron renderer process, the Node env will exits and recreate when the window reloads.
/// So we need to ensure that the Tokio runtime is initialized when the Node env is created.
#[cfg(windows)]
pub(crate) fn ensure_runtime() {
let mut rt = RT.write().unwrap();
if rt.is_none() {
*rt = create_runtime();
}
}
#[cfg(windows)]
pub(crate) unsafe extern "C" fn drop_runtime(arg: *mut std::ffi::c_void) {
use std::sync::atomic::Ordering;
if RT_REFERENCE_COUNT.fetch_sub(1, Ordering::SeqCst) == 1 {
if let Some(rt) = Lazy::get_mut(unsafe { &mut RT }) {
rt.take();
}
RT.write().unwrap().take();
}
unsafe {
@ -49,7 +61,7 @@ pub fn spawn<F>(fut: F) -> tokio::task::JoinHandle<F::Output>
where
F: 'static + Send + Future<Output = ()>,
{
unsafe { RT.as_ref() }.unwrap().spawn(fut)
RT.read().unwrap().as_ref().unwrap().spawn(fut)
}
/// Runs a future to completion
@ -59,7 +71,7 @@ pub fn block_on<F>(fut: F) -> F::Output
where
F: 'static + Send + Future<Output = ()>,
{
unsafe { RT.as_ref() }.unwrap().block_on(fut)
RT.read().unwrap().as_ref().unwrap().block_on(fut)
}
// This function's signature must be kept in sync with the one in lib.rs, otherwise napi
@ -69,7 +81,7 @@ where
/// then call the provided closure. Otherwise it will just call the provided closure.
#[inline]
pub fn within_runtime_if_available<F: FnOnce() -> T, T>(f: F) -> T {
let _rt_guard = unsafe { RT.as_ref() }.unwrap().enter();
let _rt_guard = RT.read().unwrap().as_ref().unwrap().enter();
f()
}

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Electron test</title>
</head>
<body>
<div>Electron test</div>
<script src="./index.js"></script>
</body>
</html>

View file

@ -0,0 +1,7 @@
const { ipcRenderer } = require('electron')
const { callThreadsafeFunction } = require('../index')
callThreadsafeFunction(() => {})
ipcRenderer.on('ping', () => ipcRenderer.send('pong'))

View file

@ -1,6 +1,8 @@
const assert = require('assert')
const { readFileSync } = require('fs')
const { app, BrowserWindow, ipcMain } = require('electron')
const {
readFileAsync,
callThreadsafeFunction,
@ -10,6 +12,42 @@ const {
const FILE_CONTENT = readFileSync(__filename, 'utf8')
const createWindowAndReload = async () => {
await app.whenReady()
const win = new BrowserWindow({
width: 800,
height: 600,
show: false,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
},
})
await win.loadFile('./electron-renderer/index.html')
await new Promise((resolve, reject) => {
win.webContents.on('render-process-gone', (e, detail) => {
reject(
new Error(
`Renderer process crashed: ${detail.reason}, exitCode: ${detail.exitCode}`,
),
)
})
// reload to check if there is any crash
win.reload()
// Wait for a while to make sure if a crash happens, the 'resolve' function should be called after the crash
setTimeout(() => {
// make sure the renderer process is still alive
ipcMain.once('pong', () => resolve())
win.webContents.send('ping')
}, 500)
})
}
async function main() {
const ctrl = new AbortController()
const promise = withAbortController(1, 2, ctrl.signal)
@ -45,10 +83,13 @@ async function main() {
Array.from({ length: 100 }, (_, i) => i + 1).reduce((a, b) => a + b),
)
console.info(createExternalTypedArray())
process.exit(0)
}
main().catch((e) => {
console.error(e)
process.exit(1)
})
Promise.all([main(), createWindowAndReload()])
.then(() => {
process.exit(0)
})
.catch((e) => {
console.error(e)
process.exit(1)
})

View file

@ -7,5 +7,5 @@
"target": "ES2018",
"skipLibCheck": false
},
"exclude": ["dist", "electron.js"]
"exclude": ["dist", "electron.js", "electron-renderer"]
}