You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
290 lines
8.1 KiB
290 lines
8.1 KiB
4 years ago
|
# coding: utf-8
|
||
|
"""JupyterLab Server process handler"""
|
||
|
|
||
|
# Copyright (c) Jupyter Development Team.
|
||
|
# Distributed under the terms of the Modified BSD License.
|
||
|
|
||
|
import atexit
|
||
|
import logging
|
||
|
import os
|
||
|
import re
|
||
|
from shutil import which as _which
|
||
|
import signal
|
||
|
import subprocess
|
||
|
import sys
|
||
|
import threading
|
||
|
import time
|
||
|
import weakref
|
||
|
|
||
|
from tornado import gen
|
||
|
|
||
|
|
||
|
try:
|
||
|
import pty
|
||
|
except ImportError:
|
||
|
pty = False
|
||
|
|
||
|
if sys.platform == 'win32':
|
||
|
list2cmdline = subprocess.list2cmdline
|
||
|
else:
|
||
|
def list2cmdline(cmd_list):
|
||
|
import pipes
|
||
|
return ' '.join(map(pipes.quote, cmd_list))
|
||
|
|
||
|
logging.basicConfig(format='%(message)s', level=logging.INFO)
|
||
|
|
||
|
|
||
|
def which(command, env=None):
|
||
|
"""Get the full path to a command.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
command: str
|
||
|
The command name or path.
|
||
|
env: dict, optional
|
||
|
The environment variables, defaults to `os.environ`.
|
||
|
"""
|
||
|
env = env or os.environ
|
||
|
path = env.get('PATH') or os.defpath
|
||
|
command_with_path = _which(command, path=path)
|
||
|
|
||
|
# Allow nodejs as an alias to node.
|
||
|
if command == 'node' and not command_with_path:
|
||
|
command = 'nodejs'
|
||
|
command_with_path = _which('nodejs', path=path)
|
||
|
|
||
|
if not command_with_path:
|
||
|
if command in ['nodejs', 'node', 'npm']:
|
||
|
msg = 'Please install Node.js and npm before continuing installation. You may be able to install Node.js from your package manager, from conda, or directly from the Node.js website (https://nodejs.org).'
|
||
|
raise ValueError(msg)
|
||
|
raise ValueError('The command was not found or was not ' +
|
||
|
'executable: %s.' % command)
|
||
|
return os.path.abspath(command_with_path)
|
||
|
|
||
|
|
||
|
class Process(object):
|
||
|
"""A wrapper for a child process.
|
||
|
"""
|
||
|
_procs = weakref.WeakSet()
|
||
|
_pool = None
|
||
|
|
||
|
def __init__(self, cmd, logger=None, cwd=None, kill_event=None,
|
||
|
env=None, quiet=False):
|
||
|
"""Start a subprocess that can be run asynchronously.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
cmd: list
|
||
|
The command to run.
|
||
|
logger: :class:`~logger.Logger`, optional
|
||
|
The logger instance.
|
||
|
cwd: string, optional
|
||
|
The cwd of the process.
|
||
|
env: dict, optional
|
||
|
The environment for the process.
|
||
|
kill_event: :class:`~threading.Event`, optional
|
||
|
An event used to kill the process operation.
|
||
|
quiet: bool, optional
|
||
|
Whether to suppress output.
|
||
|
"""
|
||
|
if not isinstance(cmd, (list, tuple)):
|
||
|
raise ValueError('Command must be given as a list')
|
||
|
|
||
|
if kill_event and kill_event.is_set():
|
||
|
raise ValueError('Process aborted')
|
||
|
|
||
|
self.logger = logger = logger or logging.getLogger('jupyterlab')
|
||
|
self._last_line = ''
|
||
|
if not quiet:
|
||
|
self.logger.info('> ' + list2cmdline(cmd))
|
||
|
self.cmd = cmd
|
||
|
|
||
|
kwargs = {}
|
||
|
if quiet:
|
||
|
kwargs['stdout'] = subprocess.DEVNULL
|
||
|
|
||
|
self.proc = self._create_process(cwd=cwd, env=env, **kwargs)
|
||
|
self._kill_event = kill_event or threading.Event()
|
||
|
|
||
|
Process._procs.add(self)
|
||
|
|
||
|
def terminate(self):
|
||
|
"""Terminate the process and return the exit code.
|
||
|
"""
|
||
|
proc = self.proc
|
||
|
|
||
|
# Kill the process.
|
||
|
if proc.poll() is None:
|
||
|
os.kill(proc.pid, signal.SIGTERM)
|
||
|
|
||
|
# Wait for the process to close.
|
||
|
try:
|
||
|
proc.wait(timeout=2.)
|
||
|
except subprocess.TimeoutExpired:
|
||
|
if os.name == 'nt':
|
||
|
sig = signal.SIGBREAK
|
||
|
else:
|
||
|
sig = signal.SIGKILL
|
||
|
|
||
|
if proc.poll() is None:
|
||
|
os.kill(proc.pid, sig)
|
||
|
|
||
|
finally:
|
||
|
Process._procs.remove(self)
|
||
|
|
||
|
return proc.wait()
|
||
|
|
||
|
def wait(self):
|
||
|
"""Wait for the process to finish.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
The process exit code.
|
||
|
"""
|
||
|
proc = self.proc
|
||
|
kill_event = self._kill_event
|
||
|
while proc.poll() is None:
|
||
|
if kill_event.is_set():
|
||
|
self.terminate()
|
||
|
raise ValueError('Process was aborted')
|
||
|
time.sleep(1.)
|
||
|
return self.terminate()
|
||
|
|
||
|
@gen.coroutine
|
||
|
def wait_async(self):
|
||
|
"""Asynchronously wait for the process to finish.
|
||
|
"""
|
||
|
proc = self.proc
|
||
|
kill_event = self._kill_event
|
||
|
while proc.poll() is None:
|
||
|
if kill_event.is_set():
|
||
|
self.terminate()
|
||
|
raise ValueError('Process was aborted')
|
||
|
yield gen.sleep(1.)
|
||
|
|
||
|
raise gen.Return(self.terminate())
|
||
|
|
||
|
def _create_process(self, **kwargs):
|
||
|
"""Create the process.
|
||
|
"""
|
||
|
cmd = self.cmd
|
||
|
kwargs.setdefault('stderr', subprocess.STDOUT)
|
||
|
|
||
|
cmd[0] = which(cmd[0], kwargs.get('env'))
|
||
|
|
||
|
if os.name == 'nt':
|
||
|
kwargs['shell'] = True
|
||
|
|
||
|
proc = subprocess.Popen(cmd, **kwargs)
|
||
|
return proc
|
||
|
|
||
|
@classmethod
|
||
|
def _cleanup(cls):
|
||
|
"""Clean up the started subprocesses at exit.
|
||
|
"""
|
||
|
for proc in list(cls._procs):
|
||
|
proc.terminate()
|
||
|
|
||
|
|
||
|
class WatchHelper(Process):
|
||
|
"""A process helper for a watch process.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, cmd, startup_regex, logger=None, cwd=None,
|
||
|
kill_event=None, env=None):
|
||
|
"""Initialize the process helper.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
cmd: list
|
||
|
The command to run.
|
||
|
startup_regex: string
|
||
|
The regex to wait for at startup.
|
||
|
logger: :class:`~logger.Logger`, optional
|
||
|
The logger instance.
|
||
|
cwd: string, optional
|
||
|
The cwd of the process.
|
||
|
env: dict, optional
|
||
|
The environment for the process.
|
||
|
kill_event: callable, optional
|
||
|
A function to call to check if we should abort.
|
||
|
"""
|
||
|
super(WatchHelper, self).__init__(cmd, logger=logger,
|
||
|
cwd=cwd, kill_event=kill_event, env=env)
|
||
|
|
||
|
if not pty:
|
||
|
self._stdout = self.proc.stdout
|
||
|
|
||
|
while 1:
|
||
|
line = self._stdout.readline().decode('utf-8')
|
||
|
if not line:
|
||
|
raise RuntimeError('Process ended improperly')
|
||
|
print(line.rstrip())
|
||
|
if re.match(startup_regex, line):
|
||
|
break
|
||
|
|
||
|
self._read_thread = threading.Thread(target=self._read_incoming)
|
||
|
self._read_thread.setDaemon(True)
|
||
|
self._read_thread.start()
|
||
|
|
||
|
def terminate(self):
|
||
|
"""Terminate the process.
|
||
|
"""
|
||
|
proc = self.proc
|
||
|
|
||
|
if proc.poll() is None:
|
||
|
if os.name != 'nt':
|
||
|
# Kill the process group if we started a new session.
|
||
|
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
|
||
|
else:
|
||
|
os.kill(proc.pid, signal.SIGTERM)
|
||
|
|
||
|
# Wait for the process to close.
|
||
|
try:
|
||
|
proc.wait()
|
||
|
finally:
|
||
|
Process._procs.remove(self)
|
||
|
|
||
|
return proc.returncode
|
||
|
|
||
|
def _read_incoming(self):
|
||
|
"""Run in a thread to read stdout and print"""
|
||
|
fileno = self._stdout.fileno()
|
||
|
while 1:
|
||
|
try:
|
||
|
buf = os.read(fileno, 1024)
|
||
|
except OSError as e:
|
||
|
self.logger.debug('Read incoming error %s', e)
|
||
|
return
|
||
|
|
||
|
if not buf:
|
||
|
return
|
||
|
|
||
|
print(buf.decode('utf-8'), end='')
|
||
|
|
||
|
def _create_process(self, **kwargs):
|
||
|
"""Create the watcher helper process.
|
||
|
"""
|
||
|
kwargs['bufsize'] = 0
|
||
|
|
||
|
if pty:
|
||
|
master, slave = pty.openpty()
|
||
|
kwargs['stderr'] = kwargs['stdout'] = slave
|
||
|
kwargs['start_new_session'] = True
|
||
|
self._stdout = os.fdopen(master, 'rb')
|
||
|
else:
|
||
|
kwargs['stdout'] = subprocess.PIPE
|
||
|
|
||
|
if os.name == 'nt':
|
||
|
startupinfo = subprocess.STARTUPINFO()
|
||
|
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||
|
kwargs['startupinfo'] = startupinfo
|
||
|
kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP
|
||
|
kwargs['shell'] = True
|
||
|
|
||
|
return super(WatchHelper, self)._create_process(**kwargs)
|
||
|
|
||
|
|
||
|
# Register the cleanup handler.
|
||
|
atexit.register(Process._cleanup)
|