#!/usr/bin/python12######################################################################3# This is a daemon-ization script for the IPython notebook, for running4# it under a specific URL behind a given proxy server. It is probably5# only of use directly in https://cocalc.com.6#7# This is written in Python, but that can be any python2 on the system; not the8# Python that the ipython command runs.9#10#11# Copyright (C) 2016, Sagemath Inc.12# 2-clause BSD:13# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:14# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.15# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.16# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.17# The views and conclusions contained in the software and documentation are those of the authors and should not be interpreted as representing official policies, either expressed or implied, of the SageMath Project.18######################################################################192021import json, os, random, signal, sys, time2223def server_setup():24global SMC, DATA, DAEMON_FILE, mode, INFO_FILE, info, project_id, base_url, ip25# start from home directory, since we want daemon to serve all files in that directory tree.26os.chdir(os.environ['HOME'])2728SMC = os.environ['SMC']2930os.environ["PYTHONUSERBASE"] = os.environ['HOME'] + '/.local'3132DATA = os.path.join(SMC, 'jupyter')33if not os.path.exists(DATA):34os.makedirs(DATA)3536# When run in Daemon mode, it stores info (e.g., pid, port) in this file, in addition to printing37# to standard out. This avoids starting a redundant copy of the daemon, if one is already running.38DAEMON_FILE = os.path.join(DATA, "daemon.json")3940if len(sys.argv) == 1:41print "Usage: %s [start/stop/status] normal Jupyter notebook options..."%sys.argv[0]42print "If start or stop is given, then runs as a daemon; otherwise, runs in the foreground."43sys.exit(1)4445mode = sys.argv[1]46del sys.argv[1]4748INFO_FILE = os.path.join(SMC, 'info.json')49if os.path.exists(INFO_FILE):50info = json.loads(open(INFO_FILE).read())51project_id = info['project_id']52base_url = info['base_url']53ip = info['location']['host']54if ip == 'localhost':55# Listening on localhost for devel purposes -- NOTE: this is a *VERY* significant security risk!56ip = '127.0.0.1'57else:58project_id = ''59base_url = ''60ip = '127.0.0.1'6162def random_port():63# get an available port; a race condition is possible, but very, very unlikely.64while True:65port = random.randint(1025,65536)66a = os.popen("netstat -ano|grep %s|grep LISTEN"%port).read()67if len(a) < 5:68return port6970def command():71port = random_port() # time consuming!72if project_id:73b = "%s/%s/port/jupyter/"%(base_url, project_id)74base = " --NotebookApp.base_url=%s "%(b)75else:76base = ''7778# 2nd argument after "start" ("start" is already eaten, see above)79if len(sys.argv) >= 2:80mathjax_url = sys.argv.pop(1)81else:82mathjax_url = "/static/mathjax/MathJax.js" # fallback8384# --NotebookApp.iopub_data_rate_limit=<Float>85# Default: 086# (bytes/sec) Maximum rate at which messages can be sent on iopub before they87# are limited.88# --NotebookApp.iopub_msg_rate_limit=<Float>89# (msg/sec) Maximum rate at which messages can be sent on iopub before they90# are limited.9192cmd = "jupyter notebook --port-retries=0 --no-browser --NotebookApp.iopub_data_rate_limit=2000000 --NotebookApp.iopub_msg_rate_limit=50 --NotebookApp.mathjax_url=%s %s --ip=%s --port=%s --NotebookApp.token='' --NotebookApp.password=''"%(mathjax_url, base, ip, port)93cmd += " " + ' '.join(sys.argv[1:])94return cmd, base, port9596if '--help' in ''.join(sys.argv):97os.system("jupyter " + ' '.join(sys.argv))98sys.exit(0)99100def is_daemon_running():101if not os.path.exists(DAEMON_FILE):102return False103try:104s = open(DAEMON_FILE).read()105info = json.loads(s)106try:107os.kill(info['pid'],0)108# process exists109return info110except OSError:111# error if no process112return False113except:114# status file corrupted, so ignore it, and115# just fall through to the below....116return False117118119def action(mode):120sys.stdout.flush()121122if mode == 'status':123info = is_daemon_running()124if info:125info['status'] = 'running'126s = info127else:128s = {'status':'stopped'}129print json.dumps(s)130return131132elif mode == 'start':133if os.path.exists(DAEMON_FILE) and time.time() - os.path.getmtime(DAEMON_FILE) < 60:134# If we just tried to start then called again, wait a bit before checking135# on process. Note that this should never happen, since local_hub doesn't136# call this script repeatedly.137time.sleep(10)138139info = is_daemon_running()140if info:141# already running -- nothing to do142print json.dumps(info)143return144145# The below approach to finding the PID is *HIDEOUS* and could in theory break.146# It is the only way I could come up with to do this without modifying source code of ipython :-(147# See http://mail.scipy.org/pipermail/ipython-user/2012-May/010043.html148cmd, base, port = command()149150c = '%s 2> "%s"/jupyter-notebook.err 1>"%s"/jupyter-notebook.log &'%(cmd, DATA, DATA)151sys.stderr.write(c+'\n'); sys.stderr.flush()152os.system(c)153154s = json.dumps({'base':base, 'port':port})155open(DAEMON_FILE,'w').write(s)156157tries = 0158pid = 0159#sys.stderr.write("getting pid...\n"); sys.stderr.flush()160wait = 1161while not pid:162tries += 1163#sys.stderr.write("tries... %s\n"%tries); sys.stderr.flush()164if tries >= 20:165print json.dumps({"error":"Failed to find pid of subprocess."})166sys.exit(1)167168c = "ps -u`whoami` -o pid,cmd|grep '/usr/local/bin/jupyter-notebook'"169for s in os.popen(c).read().splitlines():170v = s.split()171if len(v) < 2 or not v[1].split('/')[-1].startswith('python'):172continue173p = int(v[0])174if "port=%s"%port not in s:175try:176os.kill(p, 9) # kill any other ipython notebook servers by this user177except:178pass179else:180pid = p181if not pid:182time.sleep(wait)183wait *= 1.2184wait = min(wait, 10)185186s = json.dumps({'base':base, 'port':port, 'pid':pid})187print s188open(DAEMON_FILE,'w').write(s)189return190191elif mode == 'stop':192info = is_daemon_running()193if not info:194# not running -- nothing to do195return196# IPython server seems rock solid about responding to kill signals and properly cleaning up.197try:198os.kill(info['pid'], signal.SIGTERM)199except OSError: # maybe already dead200pass201try:202os.unlink(DAEMON_FILE)203except:204pass205return206207elif mode == 'restart':208action('stop')209action('start')210211else:212raise RuntimeError("unknown command '%s'"%mode)213214def main():215server_setup()216action(mode)217218def prepare_file_for_open():219# This is unrelated to running the server; instead, before opening220# a file, we run this to make sure there is a blank JSON template in place.221# This is for compatibility with "new jupyter".222# See https://github.com/sagemathinc/cocalc/issues/1978223for path in sys.argv[1:]:224if not os.path.exists(path) or len(open(path).read().strip()) == 0:225open(path,'w').write('{"cells": [{"outputs": [], "source": [], "cell_type": "code", "metadata": {"collapsed": false}, "execution_count": null}], "nbformat_minor": 0, "nbformat": 4, "metadata": {"language_info": {"mimetype": "text/x-python", "version": "2.7.8", "nbconvert_exporter": "python", "pygments_lexer": "ipython2", "codemirror_mode": {"name": "ipython", "version": 2}, "file_extension": ".py", "name": "python"}, "kernelspec": {"name": "python2", "language": "python", "display_name": "Python 2"}}}')226227228