voila-dashboards / voila

Voilà turns Jupyter notebooks into standalone web applications
https://voila.readthedocs.io
Other
5.43k stars 504 forks source link

How to programmatically quit Voilà? #864

Open enricogandini opened 3 years ago

enricogandini commented 3 years ago

Thanks everybody for the awesome Voilà project!

I wanted to ask if there is a way to programmatically quit Voilà, killing the Python kernel. I am building a Voilà app based on asynchronous ipywidgets. I would like to quit the app and kill the Python kernel when the user performs some action on the widgets.

I tried quit(), exit(), and sys.exit(), and I executed Voilà with --debug option. All the aforementioned functions make Voilà print a message on the terminal: [Voila] AsyncIOLoopKernelRestarter: restarting kernel (1/5), keep random ports. It looks like the aforementioned functions try to kill the Python kernel, which is immediately restarted by Voilà.

Is there some other function that I should be using to kill Voilà and the associated kernel? Or is there some Voilà option that I am not aware of?

agoose77 commented 3 years ago

I don't know whether Voila does this, but you can implement this yourself by invoking os.kill with os.getppid, if you have the right permissions: https://docs.python.org/3/library/os.html#os.getppid

enricogandini commented 3 years ago

I don't know whether Voila does this, but you can implement this yourself by invoking os.kill with os.getppid, if you have the right permissions: https://docs.python.org/3/library/os.html#os.getppid

Thanks! I tried using os.kill(os.getppid()), but the terminal on which I launched Voilà is still using Voilà. When i launch Voilà with option --debug, from the terminal I get the message [Voila] activity on 038cadf3-7fcc-4d6b-aeb1-c0af1f4c76ee: error. The browser tab on which the Voilà app was opened is still open.

agoose77 commented 3 years ago

What OS are you using?

martinRenou commented 3 years ago

os.kill(os.getppid()) would only kill the kernel, not the voila application.

But you might be able to do it if you know the Voila pid?

enricogandini commented 3 years ago

I am using Ubuntu 18.04; I think I have all the necessary permissions, since I am still testing the app on my local machine. When I will deploy it, I do not know if I will still have all necessary permissions. But I am really interested in knowing what would be the best way to kill the Voilà application.

martinRenou commented 3 years ago

Maybe something like this would work:

import psutil

for proc in psutil.process_iter():
    if 'voila' in proc.name():
       os.kill(proc.pid)

Please do not hold me responsible if anything goes wrong though 😄 Killing the voila application from the kernel might lead to unexpected behaviors.

agoose77 commented 3 years ago

@martinRenou shouldn't getppid return the PID of the parent process? ah, it seems that this only applies to fork

agoose77 commented 3 years ago

Another option is to use a wrapper to launch voila, and capture the PID

capture wrapper:

#!/usr/bin/env bash
set -eu
export CAPTURED_PID="$$"
exec "$@"

./capture voila ... would launch voila with the PID stored in CAPTURED_PID. I assume that kernels inherit all the appropriate ENV vars, and that killing this process would kill the child kernel processes.

enricogandini commented 3 years ago

Thanks for your answers @martinRenou and @agoose77! I tried both your approaches, but I keep getting the same error when running Voilà with --debug option, and neither the browser page nor the Voilà process on the terminal are terminated.

martinRenou commented 3 years ago

neither the browser page

The page will not close itself if you kill the voila process. The user will just get a non-responsive page, and they might not even understand what happened.

enricogandini commented 3 years ago

Yes, the page is in fact non-responsive after I try to kill the process with any of the aforementioned methods. But the terminal is still running Voilà, and the user can still reload the page to restart the app. I see the error message, but Voilà is not really killed.

Maybe I am asking for something impossible or unreasonable, I am not an expert in web development.

agoose77 commented 3 years ago

@enricogandini it's probably an error in your script. This works with my capture wrapper:

{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": ["import os,signal;os.kill(int(os.environ['CAPTURED_PID']),signal.SIGKILL)"]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
enricogandini commented 3 years ago

Yes, I did not convert the CAPTURED_PID from string to integer, and I did not include signal.SIGKILL.

Executing voila through the capture wrapper described by @agoose77, and including os.kill(int(os.environ["CAPTURED_PID"]), signal.SIGKILL) in my notebook, produces an unresponsive browser tab (that cannot be reloaded by the user), and kills the Voilà process in the terminal.

Thanks a lot for your answers!

agoose77 commented 3 years ago

@enricogandini I tested with getppid and it works for me too, i.e. without a wrapper:

import os, signal
os.kill(os.getppid(), signal.SIGKILL)
agoose77 commented 3 years ago

To give the user a more friendly UX, you would want to use JS to trigger the browser tab to close.

enricogandini commented 3 years ago

Yes, using os.getppid works just as well, and is more convenient than the capture wrapper. It is necessary to include signal.SIGKILL.

I am sure that using JavaScript to kill the process would give users a better experience, but I don't know anything about JavaScript, I was looking for a Python solution, and I am satisfied with it!

agoose77 commented 3 years ago

RE using JS, I mean that you could use it alongside the shutdown logic. The best way would probably be to add some JS that connects to the kernel, and triggers the window close when it detects that the connection is lost.

enricogandini commented 3 years ago

Thanks for the suggestion, I will find out how to execute JS code inside a Python Jupyter notebook.

But I am quite satisfied with the Python solution you provided. As I mentioned at the beginning of this issue, I am writing an app that uses asynchronous widgets. So, when the user reaches reaches the end of the app (after passing through many widgets), I show him one last "Thank you, goodbye!" widget, and I kill the process so he cannot reload the page. In my opinion, this is a reasonably good user experience!

chuckmandu commented 2 years ago

RE using JS, I mean that you could use it alongside the shutdown logic. The best way would probably be to add some JS that connects to the kernel, and triggers the window close when it detects that the connection is lost.

How to detect IPython kernel events in Javascript within the Voila environment is where I get stuck. Any guidance is appreciated.