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:
parent
a956d51a9a
commit
e47c13f177
7 changed files with 92 additions and 15 deletions
|
@ -204,6 +204,8 @@ overrides:
|
|||
parserOptions:
|
||||
project:
|
||||
- ./examples/tsconfig.json
|
||||
rules:
|
||||
'import/no-extraneous-dependencies': 0
|
||||
|
||||
- files:
|
||||
- ./bench/**/*.{ts,js}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
13
examples/napi/electron-renderer/index.html
Normal file
13
examples/napi/electron-renderer/index.html
Normal 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>
|
7
examples/napi/electron-renderer/index.js
Normal file
7
examples/napi/electron-renderer/index.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
const { ipcRenderer } = require('electron')
|
||||
|
||||
const { callThreadsafeFunction } = require('../index')
|
||||
|
||||
callThreadsafeFunction(() => {})
|
||||
|
||||
ipcRenderer.on('ping', () => ipcRenderer.send('pong'))
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -7,5 +7,5 @@
|
|||
"target": "ES2018",
|
||||
"skipLibCheck": false
|
||||
},
|
||||
"exclude": ["dist", "electron.js"]
|
||||
"exclude": ["dist", "electron.js", "electron-renderer"]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue