diff --git a/README.md b/README.md index 07e2e0e..ce9dbf3 100755 --- a/README.md +++ b/README.md @@ -98,15 +98,17 @@ jupyter notebook kdb+Notebooks.ipynb If you have [Docker installed](https://www.docker.com/community-edition) you can alternatively run: - docker run -it -p 8888:8888 --name myjupyterq kxsys/jupyterq - + docker run -it --name myjupyterq -p 8888:8888 kxsys/jupyterq + Now point your browser at http://localhost:8888/notebooks/kdb%2BNotebooks.ipynb. For subsequent runs, you will not be prompted to redo the license setup when calling: docker start -ai myjupyterq -**N.B.** [instructions regarding headless/presets are available](https://github.com/KxSystems/embedPy/docker/README.md#headlesspresets) +To change the port or use the image to run your own notebooks, see the docker [README](docker/README.md#runoptions) + +**N.B.** [instructions regarding headless/presets are available](https://github.com/KxSystems/embedPy/blob/master/docker/README.md#headlesspresets) **N.B.** [build instructions for the image are available](docker/README.md) diff --git a/build/doit.sh b/build/doit.sh new file mode 100755 index 0000000..81fd3e0 --- /dev/null +++ b/build/doit.sh @@ -0,0 +1,32 @@ +usage=\ +"Usage:\n\tdoit.sh [repodir] + +\tbuilds conda package, set NOTEST in env to skip tests during conda build (faster) +" + +if [ $# -lt 1 ] +then + printf "$usage" >&2 + exit 1 +elif [ ! -d $1 ] +then + printf "$1 doesn't exist\n\n$usage" >&2 + exit 1 +fi + +: "${JUPYTERQ_VERSION:=local_dev}" +[ -z "$NOTEST" ] && export QLIC="${QLIC:=$QHOME}" + +JUPYTERQ_REQS=$(paste -sd "|" $1/requirements.txt) + +export JUPYTERQ_REQS +export JUPYTERQ_VERSION +export QLIC + +set -x +if [ ! -z "$QLIC" ] +then + conda build -c kx --no-long-test-prefix $1/conda-recipe +else + conda build -c kx --no-test conda-recipe +fi diff --git a/conda-recipe/bld.bat b/conda-recipe/bld.bat index 677d9b0..d33115a 100644 --- a/conda-recipe/bld.bat +++ b/conda-recipe/bld.bat @@ -7,9 +7,21 @@ copy /Y jupyterq*.q? %QHOME%\ || goto :error xcopy kxpy %QHOME%\kxpy /I/S/E/H|| goto :error copy /Y /B w64\jupyterq.dll %QHOME%\w64\ || goto :error set JK=%PREFIX%\share\jupyter\kernels\qpk +set JL=jupyterq_licensemgr\jupyterq_licensemgr mkdir %JK% +mkdir %PREFIX%\etc\jupyter\jupyter_notebook_config.d +mkdir %PREFIX%\etc\jupyter\nbconfig\notebook.d +mkdir %PREFIX%\share\jupyter\nbextensions\jupyterq_licensemgr copy kernelspec\* %JK% +cd jupyterq_licensemgr +python setup.py install +cd .. + +copy %JL%\index.js %PREFIX%\share\jupyter\nbextensions\jupyterq_licensemgr +copy %JL%\jupyterq_licensemgr.json %PREFIX%\etc\jupyter\nbconfig\notebook.d +copy %JL%\jupyterq_licensemgr_config.json %PREFIX%\etc\jupyter\jupyter_notebook_config.d + exit /b 0 :error exit /b %errorlevel% diff --git a/conda-recipe/build.sh b/conda-recipe/build.sh index 6e72ec2..7534d2e 100644 --- a/conda-recipe/build.sh +++ b/conda-recipe/build.sh @@ -9,8 +9,13 @@ fi make -f build/makefile jupyterq mkdir -p $QHOME/$QLIBDIR JK=$PREFIX/share/jupyter/kernels/qpk -mkdir -p $JK +JL=jupyterq_licensemgr/jupyterq_licensemgr +mkdir -p $JK $PREFIX/etc/jupyter/nbconfig/notebook.d $PREFIX/etc/jupyter/jupyter_notebook_config.d $PREFIX/share/jupyter/nbextensions/jupyterq_licensemgr cp kernelspec/* $JK +(cd jupyterq_licensemgr && python setup.py install) +cp $JL/index.js $PREFIX/share/jupyter/nbextensions/jupyterq_licensemgr +cp $JL/jupyterq_licensemgr.json $PREFIX/etc/jupyter/nbconfig/notebook.d +cp $JL/jupyterq_licensemgr_config.json $PREFIX/etc/jupyter/jupyter_notebook_config.d cp jupyterq*.q $QHOME cp -r kxpy $QHOME cp $QLIBDIR/jupyterq.so $QHOME/$QLIBDIR diff --git a/conda-recipe/run_test.sh b/conda-recipe/run_test.sh index 834a392..68c743e 100755 --- a/conda-recipe/run_test.sh +++ b/conda-recipe/run_test.sh @@ -1,5 +1,5 @@ #!/bin/bash -if [ -e ${QLIC}/kc.lic ] +if [ -e ${QLIC}/kc.lic ] || [ -e ${QLIC}/k4.lic ] then tests/test.sh else diff --git a/docker/Dockerfile b/docker/Dockerfile index 5d1c0df..845f502 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -5,25 +5,20 @@ ARG embedpy_img=kxsys/embedpy:latest #### FROM $embedpy_img AS embedpy - -# do not clean here, its cleaned later! -# no upgrade either as it comes from embedPy -RUN apt-get update - -#### - FROM embedpy AS juypterq -RUN apt-get -yy --option=Dpkg::options::=--force-unsafe-io --no-install-recommends install \ - build-essential \ +RUN apt-get update \ + && apt-get -yy --option=Dpkg::options::=--force-unsafe-io --no-install-recommends install build-essential \ && apt-get clean \ && find /var/lib/apt/lists -type f -delete COPY build/makefile requirements.txt *.q *.p *.ipynb /opt/kx/jupyterq/ +COPY jupyterq_licensemgr/ /opt/kx/jupyterq/jupyterq_licensemgr/ COPY src/c/ /opt/kx/jupyterq/src/c/ COPY kxpy/ /opt/kx/jupyterq/kxpy/ COPY kernelspec/ /opt/kx/jupyterq/kernelspec/ COPY examples/ /opt/kx/jupyterq/examples/ +# COPY kdb+Notebooks.ipynb /opt/kx/jupyterq/examples/kdb+Notebooks.ipynb RUN make -C /opt/kx/jupyterq jupyterq @@ -50,32 +45,65 @@ LABEL org.label-schema.schema-version="1.0" \ org.label-schema.build-date="$BUILD_DATE" \ org.label-schema.docker.cmd="docker run -v `pwd`/q:/tmp/q -p $PORT:$PORT kxsys/jupyterq" -RUN apt-get -yy --option=Dpkg::options::=--force-unsafe-io --no-install-recommends install \ - libgl1-mesa-glx \ - && apt-get clean \ - && find /var/lib/apt/lists -type f -delete - COPY --from=juypterq /opt/kx/jupyterq /opt/kx/jupyterq RUN chown kx:kx /opt/kx/jupyterq /opt/kx/jupyterq/kdb+Notebooks.ipynb RUN find /opt/kx/jupyterq -maxdepth 1 -type f -name 'jupyterq_*.q' | xargs ln -s -t /opt/kx/q \ && ln -s -t /opt/kx/q /opt/kx/jupyterq/kxpy \ && ln -s -t /opt/kx/q/l64 /opt/kx/jupyterq/l64/jupyterq.so +RUN apt-get update \ + && apt-get -yy --option=Dpkg::options::=--force-unsafe-io --no-install-recommends install xinetd libgl1-mesa-glx \ + && apt-get clean \ + && find /var/lib/apt/lists -type f -delete + USER kx RUN . /opt/conda/etc/profile.d/conda.sh \ && conda activate kx \ && conda install nomkl \ && conda install --file /opt/kx/jupyterq/requirements.txt \ - && conda clean -y --all \ - && jupyter kernelspec install --user --name=qpk /opt/kx/jupyterq/kernelspec \ - && jupyter trust /opt/kx/jupyterq/kdb+Notebooks.ipynb + && conda clean -y --all + +USER root + +RUN chown -R kx:kx /opt/kx/jupyterq/jupyterq_licensemgr + +USER kx + +RUN . /opt/conda/etc/profile.d/conda.sh \ + && conda activate kx \ + && cd /opt/kx/jupyterq/jupyterq_licensemgr \ + && python setup.py build \ + && pip install --user --no-deps . # remove token auth -RUN mkdir ~/.jupyter \ +RUN mkdir -p ~/.jupyter \ && echo "c.NotebookApp.token = u''" > ~/.jupyter/jupyter_notebook_config.py +RUN . /opt/conda/etc/profile.d/conda.sh \ + && conda activate kx \ + && jupyter kernelspec install --user --name=qpk /opt/kx/jupyterq/kernelspec \ + && jupyter nbextension install --user --py jupyterq_licensemgr \ + && jupyter nbextension enable --user --py jupyterq_licensemgr \ + && jupyter serverextension enable --py jupyterq_licensemgr \ + && jupyter trust /opt/kx/jupyterq/kdb+Notebooks.ipynb + USER root +COPY docker/init /init +RUN chmod 0755 /init + + +## Create new Entry folders +## Notebooks, Data & Scripts can all be mounted to these folders + +ARG nbroot=/jqnotebooks +ARG nbdata=/jqdata +ARG nbscripts=/jqscripts + +ENV NBROOT=${nbroot} +ENV NBDATA=${nbdata} +ENV NBSCRIPTS=${nbscipts} + +VOLUME ${nbroot} ${nbdata} ${nbscripts} ENTRYPOINT ["/init"] -CMD ["/bin/sh", "-l", "-c", "printf '\npoint your browser at http://127.0.0.1:%s/notebooks/kdb%%2BNotebooks.ipynb\n\n' $PORT && exec jupyter notebook --notebook-dir=/opt/kx/jupyterq --ip='0.0.0.0' --port=$PORT --no-browser"] diff --git a/docker/README.md b/docker/README.md index 355b1d3..91ddfe5 100644 --- a/docker/README.md +++ b/docker/README.md @@ -16,13 +16,14 @@ To build the project locally you run: Once built, you should have a local `jupyterq` image, you can run the following to use it: - docker run -it -p 8888:8888 jupyterq + docker run --rm -it -p 8888:8888 jupyterq **N.B.** if you wish to use an alternative source for [embedPy](https://github.com/KxSystems/embedPy) then you can append `--build-arg embedpy_img=embedpy` to your argument list. Other build arguments are supported and you should browse the `Dockerfile` to see what they are. + # Deploy [travisCI](https://travis-ci.org/) is configured to monitor when tags of the format `/^[0-9]+\./` are added to the [GitHub hosted project](https://github.com/KxSystems/jupyterq), a corresponding Docker image is generated and made available on [Docker Cloud](https://cloud.docker.com/) @@ -36,6 +37,27 @@ To do a deploy, you simply tag and push your releases as usual: git push --tag +## Run Options + +These options apply both to a locally build image if you have one or the `kxsys/jupyterq` image + +To change the port the container exposes the notebook server on: + + docker run --rm -it -p 9000:9000 -e PORT=9000 kxsys/jupyterq + +You can use the image to run your own JupyterQ notebooks without building another docker image, the directory with your notebooks should be mounted in the container at /jqnotebooks. + +For example if your notebooks are on the host machine in a directory `examples`, then: + +On Windows + + docker run -it -p 8888:8888 -v %cd%\examples:/jqnotebooks kxsys/jupyterq + +Or on Mac / Linux + + docker run -it -p 8888:8888 -v $(pwd)/examples:/jqnotebooks kxsys/jupyterq + + ## Related Links * [Docker](https://docker.com) diff --git a/docker/init b/docker/init new file mode 100755 index 0000000..e683e15 --- /dev/null +++ b/docker/init @@ -0,0 +1,19 @@ +#!/bin/sh +/usr/sbin/xinetd -d & + +HOME=/home/kx +export HOME + +target=$NBROOT +if find "$target" -mindepth 1 -print -quit 2>/dev/null | grep -q .; then + echo "Not empty, do nothing" +else + echo "'$NBROOT' is empty, copying default file" && cp -R /opt/kx/jupyterq/examples $NBROOT && cp /opt/kx/jupyterq/kdb+Notebooks.ipynb $NBROOT +fi + +chmod -R a+w $NBROOT +chmod -R a+w $NBDATA + +printf '\npoint your browser at http://localhost:%s/notebooks/kdb%%2BNotebooks.ipynb\n\n' "$PORT" + +exec chpst -u kx /bin/sh -l -c "cd /opt/kx/jupyterq && QLIC=/home/kx exec jupyter notebook --notebook-dir='$NBROOT' --ip='0.0.0.0' --port='$PORT' --no-browser" diff --git a/examples/remote_example.ipynb b/examples/remote_example.ipynb index 1abb2d6..0bcf063 100644 --- a/examples/remote_example.ipynb +++ b/examples/remote_example.ipynb @@ -11,23 +11,12 @@ }, { "cell_type": "code", - "execution_count": 1, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "/ Open another q process\n", - "system $[.z.o like\"w*\";\"start \";],\"q rmt.q -p 4044\"" + "system $[.z.o like\"w*\";\"start \";\"\"],\"q rmt.q -p 4044\"" ] }, { @@ -40,13 +29,12 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ + "\\\n", "/ run the cell to see the content of the file\n", - "/%loadscript rmt.q" + "/%loadscript rmt.q\n" ] }, { @@ -58,23 +46,11 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": { - "collapsed": false, "scrolled": true }, - "outputs": [ - { - "data": { - "text/plain": [ - "5\r\n" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "/ Open connection\n", "h:hopen`::4044\n", @@ -94,10 +70,8 @@ }, { "cell_type": "code", - "execution_count": 1, - "metadata": { - "collapsed": false - }, + "execution_count": null, + "metadata": {}, "outputs": [], "source": [ "/%remote h\n", @@ -115,9 +89,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "/%remote h\n", @@ -135,22 +107,9 @@ }, { "cell_type": "code", - "execution_count": 3, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - "1b\r\n" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "\"Hello World\"~h\"hello\"" ] @@ -164,58 +123,9 @@ }, { "cell_type": "code", - "execution_count": 2, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - "time sym price \r\n", - "-------------------------\r\n", - "00:16:31.025 A 15.17091\r\n", - "00:32:08.956 A 15.1598 \r\n", - "01:15:49.472 A 14.11597\r\n", - "01:23:51.162 A 10.83889\r\n", - "01:28:03.675 A 11.95991\r\n", - "01:42:57.587 A 13.75638\r\n", - "02:01:41.131 A 16.13745\r\n", - "03:13:29.907 A 19.49975\r\n", - "03:28:48.269 A 15.75905\r\n", - "04:15:16.827 A 10.81235\r\n", - "04:53:08.684 A 19.44167\r\n", - "05:28:08.747 A 10.13921\r\n", - "05:31:41.221 A 17.14878\r\n", - "05:38:18.182 A 11.94651\r\n", - "06:44:20.731 A 19.32632\r\n", - "07:02:54.903 A 10.57525\r\n", - "07:16:54.852 A 12.56066\r\n", - "07:24:52.364 A 11.02443\r\n", - "07:29:31.107 A 18.6711 \r\n", - "08:04:51.809 A 17.5201 \r\n", - "..\r\n" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "text/plain": [ - "sym| price \r\n", - "---| --------\r\n", - "A | 15.18263\r\n", - "B | 14.2251 \r\n", - "C | 15.23465\r\n" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "/%remote h\n", "select from table where sym=`A\n", @@ -231,65 +141,9 @@ }, { "cell_type": "code", - "execution_count": 4, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - "time sym price \r\n", - "-------------------------\r\n", - "00:03:17.484 C 13.92752\r\n", - "00:16:31.025 A 15.17091\r\n", - "00:32:08.956 A 15.1598 \r\n", - "00:33:10.867 B 14.06664\r\n", - "00:44:37.591 C 11.78084\r\n", - "00:53:08.324 B 13.01772\r\n", - "00:53:31.660 C 17.85033\r\n", - "01:04:18.632 B 15.3471 \r\n", - "01:11:57.854 B 17.11172\r\n", - "01:15:49.472 A 14.11597\r\n" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "text/plain": [ - "time sym price \r\n", - "-------------------------\r\n", - "00:03:17.484 C 13.92752\r\n", - "00:33:10.867 B 14.06664\r\n", - "00:44:37.591 C 11.78084\r\n", - "00:53:08.324 B 13.01772\r\n", - "01:15:49.472 A 14.11597\r\n", - "01:17:27.614 C 14.93183\r\n", - "01:23:51.162 A 10.83889\r\n", - "01:28:03.675 A 11.95991\r\n", - "01:42:57.587 A 13.75638\r\n", - "02:33:08.380 B 12.29662\r\n", - "02:43:26.164 C 14.70788\r\n", - "03:07:08.088 B 12.30638\r\n", - "03:27:45.539 C 14.39081\r\n", - "04:03:04.576 C 13.89056\r\n", - "04:06:03.749 B 13.91543\r\n", - "04:15:16.827 A 10.81235\r\n", - "04:20:03.661 B 12.78212\r\n", - "04:26:56.542 C 12.39234\r\n", - "04:28:21.989 B 11.50813\r\n", - "04:34:15.435 B 11.56732\r\n", - "..\r\n" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "10#t:h\"table\"\n", "select from t where price<15" @@ -297,10 +151,8 @@ }, { "cell_type": "code", - "execution_count": 5, - "metadata": { - "collapsed": false - }, + "execution_count": null, + "metadata": {}, "outputs": [], "source": [ "/ Close q process\n", diff --git a/jupyterq_help.q b/jupyterq_help.q index e1dca93..8238b66 100755 --- a/jupyterq_help.q +++ b/jupyterq_help.q @@ -18,7 +18,7 @@ p)def< findkw(soup,kw): title=link.get_text() return kw,title,href bs:.p.import[`bs4;`:BeautifulSoup;>] -refcard:@[.Q.hg;`:http://code.kx.com/q/ref/card/;{0}]; / .Q.hg doesn't support https? +refcard:@[req:{"c"$.p.import[`urllib.request][`:urlopen][x][`:read][]`};"https://code.kx.com/q/ref/card/";{0}]; offline:0~refcard; if[not offline;findkw:findkw[bs[refcard;`html.parser];]]; find:{$[offline&kw:(x:`$sstring x)in qkw;"Sorry no help is available, as the kernel did not have access to code.kx.com when it was started";kw;findkw x;0]} diff --git a/jupyterq_kernel.q b/jupyterq_kernel.q index f03d219..a7a984a 100644 --- a/jupyterq_kernel.q +++ b/jupyterq_kernel.q @@ -72,7 +72,7 @@ sndsrv:{$[null srvh;pend;srvh]x} / queue or send command t srvexec:{[f;z;s;mc]sndsrv(`.qpk.execmsg;f;z;s;mc)} / exec a request on the server pending:() / pending commands for server as it starts up pend:{pending,:enlist x} / queue a command to the server -srvreg:{srvh::0-hopen x;srvh each pending;pending::()} / server registration, exec all pending messages +srvreg:{(srvh::neg hopen x)each pending;pending::()} / server registration, exec all pending messages srvregsi:{srvsi::neg .z.w;} / server register standard input handle closeport:{system"p 0";srvh"\\p 0"} / close kernel and server port srvstarterr:{starterr` sv("server startup error";x;y)} / execution server startup error diff --git a/jupyterq_licensemgr/LICENSE b/jupyterq_licensemgr/LICENSE new file mode 100755 index 0000000..fe19d61 --- /dev/null +++ b/jupyterq_licensemgr/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018 Kx Systems + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/jupyterq_licensemgr/MANIFEST.in b/jupyterq_licensemgr/MANIFEST.in new file mode 100644 index 0000000..7080a4e --- /dev/null +++ b/jupyterq_licensemgr/MANIFEST.in @@ -0,0 +1 @@ +recursive-include jupyterq_licensemgr *.json *.js *.q diff --git a/jupyterq_licensemgr/README.md b/jupyterq_licensemgr/README.md new file mode 100644 index 0000000..bf051b8 --- /dev/null +++ b/jupyterq_licensemgr/README.md @@ -0,0 +1 @@ +serverextension for automatic licensing diff --git a/jupyterq_licensemgr/jupyterq_licensemgr/__init__.py b/jupyterq_licensemgr/jupyterq_licensemgr/__init__.py new file mode 100644 index 0000000..7c2ee97 --- /dev/null +++ b/jupyterq_licensemgr/jupyterq_licensemgr/__init__.py @@ -0,0 +1,92 @@ +from notebook.base.handlers import IPythonHandler +from os import path, getenv, popen +from urllib.parse import parse_qs, urlparse +from socket import gethostname +from base64 import b64decode +from json import dumps +from getpass import getuser +from sys import stderr, exit + +QHOME = getenv('QHOME', path.join(path.expanduser("~"),"q")) +if not QHOME or not path.isdir(QHOME): + print('QHOME env is not set to a directory', file=stderr) + exit(2) + +QLIC = getenv('QLIC', QHOME) +if not path.isdir(QLIC): + print('QLIC env is not set to a directory for storing the license', file=stderr) + exit(2) + +QLIC_KC = path.join(QLIC, 'kc.lic') +QLIC_K4 = path.join(QLIC, 'k4.lic') + +# kc.lic.py compatibility +for el, ln in [('QLIC_K4', 'k4.lic'), ('QLIC_KC', 'kc.lic')]: + lic = getenv(el) + if lic: + with open(path.join(qlic, ln), 'wb') as file: + file.write(b64decode(lic)) + +def _jupyter_server_extension_paths(): + return [{ + "module": "jupyterq_licensemgr" + }] + +def _jupyter_nbextension_paths(): + return [dict( + section="notebook", + src=".", + dest="jupyterq_licensemgr", + require="jupyterq_licensemgr/index")] + +def load_jupyter_server_extension(nb): + class LicenseCheckHandler(IPythonHandler): + def get(self): + result = {"action": None, "info": None, "ok": False, "hostname": gethostname(), "user": getuser() } + status = ' '.join(popen('q "' + path.join(path.dirname(path.realpath(__file__)), 'check_q.q') + '" -q 2>&1').read().replace("\t",' ').splitlines()[0].split(" ")[1:]) + if status == "license daemon returned: 'blocked -- please confirm your email": + result['action'] = "dialog" + result['info'] = "Email address unconfirmed" + result['description'] = "Check your email for a request from Kx to verify your license." + elif status == "license daemon returned: 'blocked -- please contact ondemand@kx.com": + result['action'] = "dialog" + result['info'] = "Revoked license" + result['description'] = "This license has been revoked. If you did not do this, please contact ondemand@kx.com" + elif status == "host": + result['action'] = "license" + result['info'] = "Wrong hostname on license" + elif status == "kc.lic" or status == "k4.lic" or status == 'detected and no license found.': # no license, or corrupt license + result['action'] = "license" + result['info'] = "Unlicensed workstation" + elif status == "ok": + result['action'] = "ready" + result['ok'] = True + else: + # possible: new q error message?, someone messed with q binary? + nb.log.error("jupyterq_licensemgr issue: " + status) + result['action'] = "dialog" + result['info'] = "License check failed" + result['description'] = "There's an issue with the license manager. Check your installation carefully and/or reinstall your application" + self.add_header("Content-Type", "application/json") + nb.log.info("jupyterq_licensemgr check request") + self.finish(dumps(result).encode()) + + class LicenseSubmitHandler(IPythonHandler): + def get(self): + d = self.get_arguments("d") + if d != []: + with open(QLIC_KC, "wb") as file: + file.write(b64decode(d[0].replace(' ','+'))) + self.add_header("Content-Type", "image/gif") + nb.log.info("jupyterq_licensemgr submitted from client") + for x in nb.kernel_manager.list_kernels(): + nb.log.info("jupyterq_licensemgr restarting " + x['id']) + nb.kernel_manager.restart_kernel(x['id']) + self.finish(b64decode("R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")) + + wa = nb.web_app + wa.add_handlers('.*$', [ + ('/kx/license_check.json', LicenseCheckHandler), + ('/kx/license_submit.py', LicenseSubmitHandler) + ]) + nb.log.info("jupyterq_licensemgr module enabled") diff --git a/jupyterq_licensemgr/jupyterq_licensemgr/check_q.q b/jupyterq_licensemgr/jupyterq_licensemgr/check_q.q new file mode 100644 index 0000000..fd88494 --- /dev/null +++ b/jupyterq_licensemgr/jupyterq_licensemgr/check_q.q @@ -0,0 +1,2 @@ +1"k ok"; +\\ diff --git a/jupyterq_licensemgr/jupyterq_licensemgr/index.js b/jupyterq_licensemgr/jupyterq_licensemgr/index.js new file mode 100644 index 0000000..f9b6702 --- /dev/null +++ b/jupyterq_licensemgr/jupyterq_licensemgr/index.js @@ -0,0 +1,133 @@ +define(['jquery', 'base/js/namespace', 'base/js/dialog'], function($, J, D){ + +// [name] is the name of the event "click", "mouseover", .. +// same as you'd pass it to bind() +// [fn] is the handler function +$.fn.bindFirst = function(name, fn) { + // bind as you normally would + // don't want to miss out on any jQuery magic + this.on(name, fn); + + // Thanks to a comment by @Martin, adding support for + // namespaced events too. + this.each(function() { + var handlers = $._data(this, 'events')[name.split('.')[0]]; + // take out the handler we just inserted from the end + var handler = handlers.pop(); + // move it at the beginning + handlers.splice(0, 0, handler); + }); +}; + + var IFRAME = null + var DIALOG = null; + var LICSTATUS = 'unknown'; // unknown, licensed, unlicensed + var NEEDEDLIC = false; + var NOTEBOOK = J.notebook; + + + var BASE = location.protocol + "//" + location.host; + function close() { + if(DIALOG != null) DIALOG.remove(); + $('.modal-backdrop').remove() + DIALOG = IFRAME = null; + } + + var actions = { + "license": function(text) { + if(IFRAME != null) return; + LICSTATUS='unlicensed'; + + close(); + + IFRAME = $('