summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCasper Beyer <caspervonb@pm.me>2021-08-25 04:34:09 +0800
committerGitHub <noreply@github.com>2021-08-24 22:34:09 +0200
commita3fd4bb998d875e130ad0db40d9392192468bba1 (patch)
tree0588bb8c53281899e8c1be68d06bb41be33a26fd
parent85a56e7144cb2e213eb670f754027d19e31c315a (diff)
fix(cli): dispatch unload event on watch drop (#11696)
-rw-r--r--cli/main.rs80
-rw-r--r--cli/tests/integration/watcher_tests.rs75
2 files changed, 141 insertions, 14 deletions
diff --git a/cli/main.rs b/cli/main.rs
index 9749496c6..650ab62a2 100644
--- a/cli/main.rs
+++ b/cli/main.rs
@@ -892,29 +892,81 @@ async fn run_with_watch(flags: Flags, script: String) -> Result<(), AnyError> {
})
};
+ /// The FileWatcherModuleExecutor provides module execution with safe dispatching of life-cycle events by tracking the
+ /// state of any pending events and emitting accordingly on drop in the case of a future
+ /// cancellation.
+ struct FileWatcherModuleExecutor {
+ worker: MainWorker,
+ pending_unload: bool,
+ }
+
+ impl FileWatcherModuleExecutor {
+ pub fn new(worker: MainWorker) -> FileWatcherModuleExecutor {
+ FileWatcherModuleExecutor {
+ worker,
+ pending_unload: false,
+ }
+ }
+
+ /// Execute the given main module emitting load and unload events before and after execution
+ /// respectively.
+ pub async fn execute(
+ &mut self,
+ main_module: &ModuleSpecifier,
+ ) -> Result<(), AnyError> {
+ self.worker.execute_module(main_module).await?;
+ self.worker.execute_script(
+ &located_script_name!(),
+ "window.dispatchEvent(new Event('load'))",
+ )?;
+ self.pending_unload = true;
+
+ let result = self.worker.run_event_loop(false).await;
+ self.pending_unload = false;
+
+ if let Err(err) = result {
+ return Err(err);
+ }
+
+ self.worker.execute_script(
+ &located_script_name!(),
+ "window.dispatchEvent(new Event('unload'))",
+ )?;
+
+ Ok(())
+ }
+ }
+
+ impl Drop for FileWatcherModuleExecutor {
+ fn drop(&mut self) {
+ if self.pending_unload {
+ self
+ .worker
+ .execute_script(
+ &located_script_name!(),
+ "window.dispatchEvent(new Event('unload'))",
+ )
+ .unwrap();
+ }
+ }
+ }
+
let operation =
|(program_state, main_module): (Arc<ProgramState>, ModuleSpecifier)| {
let flags = flags.clone();
let permissions = Permissions::from_options(&flags.into());
async move {
- let main_module = main_module.clone();
- let mut worker = create_main_worker(
+ // We make use an module executor guard to ensure that unload is always fired when an
+ // operation is called.
+ let mut executor = FileWatcherModuleExecutor::new(create_main_worker(
&program_state,
main_module.clone(),
permissions,
None,
- );
- debug!("main_module {}", main_module);
- worker.execute_module(&main_module).await?;
- worker.execute_script(
- &located_script_name!(),
- "window.dispatchEvent(new Event('load'))",
- )?;
- worker.run_event_loop(false).await?;
- worker.execute_script(
- &located_script_name!(),
- "window.dispatchEvent(new Event('unload'))",
- )?;
+ ));
+
+ executor.execute(&main_module).await?;
+
Ok(())
}
};
diff --git a/cli/tests/integration/watcher_tests.rs b/cli/tests/integration/watcher_tests.rs
index 080e1f5b9..872a6b259 100644
--- a/cli/tests/integration/watcher_tests.rs
+++ b/cli/tests/integration/watcher_tests.rs
@@ -322,6 +322,81 @@ fn run_watch() {
drop(t);
}
+#[test]
+fn run_watch_load_unload_events() {
+ let t = TempDir::new().expect("tempdir fail");
+ let file_to_watch = t.path().join("file_to_watch.js");
+ std::fs::write(
+ &file_to_watch,
+ r#"
+ setInterval(() => {}, 0);
+ window.addEventListener("load", () => {
+ console.log("load");
+ });
+
+ window.addEventListener("unload", () => {
+ console.log("unload");
+ });
+ "#,
+ )
+ .expect("error writing file");
+
+ let mut child = util::deno_cmd()
+ .current_dir(util::testdata_path())
+ .arg("run")
+ .arg("--watch")
+ .arg("--unstable")
+ .arg(&file_to_watch)
+ .env("NO_COLOR", "1")
+ .stdout(std::process::Stdio::piped())
+ .stderr(std::process::Stdio::piped())
+ .spawn()
+ .expect("failed to spawn script");
+
+ let stdout = child.stdout.as_mut().unwrap();
+ let mut stdout_lines =
+ std::io::BufReader::new(stdout).lines().map(|r| r.unwrap());
+ let stderr = child.stderr.as_mut().unwrap();
+ let mut stderr_lines =
+ std::io::BufReader::new(stderr).lines().map(|r| r.unwrap());
+
+ // Wait for the first load event to fire
+ assert!(stdout_lines.next().unwrap().contains("load"));
+
+ // Change content of the file, this time without an interval to keep it alive.
+ std::fs::write(
+ &file_to_watch,
+ r#"
+ window.addEventListener("load", () => {
+ console.log("load");
+ });
+
+ window.addEventListener("unload", () => {
+ console.log("unload");
+ });
+ "#,
+ )
+ .expect("error writing file");
+
+ // Events from the file watcher is "debounced", so we need to wait for the next execution to start
+ std::thread::sleep(std::time::Duration::from_secs(1));
+
+ // Wait for the restart
+ assert!(stderr_lines.next().unwrap().contains("Restarting"));
+
+ // Confirm that the unload event was dispatched from the first run
+ assert!(stdout_lines.next().unwrap().contains("unload"));
+
+ // Followed by the load event of the second run
+ assert!(stdout_lines.next().unwrap().contains("load"));
+
+ // Which is then unloaded as there is nothing keeping it alive.
+ assert!(stdout_lines.next().unwrap().contains("unload"));
+
+ child.kill().unwrap();
+ drop(t);
+}
+
/// Confirm that the watcher continues to work even if module resolution fails at the *first* attempt
#[test]
fn run_watch_not_exit() {