Skip to content

Commit c2ff0b3

Browse files
committed
Add button to start Jupyter server in Slicer's Python environment
re Slicer#39
1 parent c59b08f commit c2ff0b3

File tree

8 files changed

+288
-169
lines changed

8 files changed

+288
-169
lines changed

JupyterKernel/Resources/UI/qSlicerJupyterKernelModuleWidget.ui

Lines changed: 77 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -6,109 +6,109 @@
66
<rect>
77
<x>0</x>
88
<y>0</y>
9-
<width>504</width>
10-
<height>423</height>
9+
<width>409</width>
10+
<height>384</height>
1111
</rect>
1212
</property>
1313
<property name="windowTitle">
1414
<string>Form</string>
1515
</property>
1616
<layout class="QVBoxLayout" name="verticalLayout">
1717
<item>
18-
<widget class="ctkCollapsibleButton" name="SetupCollapsibleButton">
18+
<widget class="ctkCollapsibleButton" name="InternalServerCollapsibleButton">
1919
<property name="text">
20-
<string>Setup</string>
20+
<string>Jupyter server in Slicer Python environment</string>
2121
</property>
2222
<layout class="QFormLayout" name="formLayout">
23+
<item row="0" column="0">
24+
<widget class="QLabel" name="label_2">
25+
<property name="text">
26+
<string>Notebook directory:</string>
27+
</property>
28+
</widget>
29+
</item>
30+
<item row="0" column="1">
31+
<widget class="ctkPathLineEdit" name="NotebookPathLineEdit"/>
32+
</item>
33+
<item row="3" column="0" colspan="2">
34+
<widget class="QLabel" name="JupyterServerStatusLabel">
35+
<property name="text">
36+
<string/>
37+
</property>
38+
</widget>
39+
</item>
2340
<item row="2" column="0" colspan="2">
24-
<widget class="QGroupBox" name="AutomaticKernelInstallationGroupBox">
25-
<property name="title">
26-
<string>Automatic Slicer kernel installation: </string>
41+
<widget class="QPushButton" name="StopJupyterNotebookPushButton">
42+
<property name="text">
43+
<string>Stop Jupyter server</string>
2744
</property>
28-
<layout class="QFormLayout" name="formLayout_2">
29-
<item row="1" column="0">
30-
<widget class="QLabel" name="PythonScriptsFolderLabel">
31-
<property name="toolTip">
32-
<string>Folder that contains 'jupyter-kernelspec' executable</string>
33-
</property>
34-
<property name="text">
35-
<string>Python Scripts folder:</string>
36-
</property>
37-
</widget>
38-
</item>
39-
<item row="1" column="1">
40-
<widget class="ctkPathLineEdit" name="PythonScriptsFolderEdit">
41-
<property name="toolTip">
42-
<string>Folder that contains 'jupyter-kernelspec' executable</string>
43-
</property>
44-
</widget>
45-
</item>
46-
<item row="2" column="0" colspan="2">
47-
<widget class="QPushButton" name="InstallSlicerKernelPushButton">
48-
<property name="text">
49-
<string>Install Slicer kernel in Jupyter</string>
50-
</property>
51-
</widget>
52-
</item>
53-
<item row="3" column="0" colspan="2">
54-
<widget class="QLabel" name="InstallSlicerKernelStatusLabel">
55-
<property name="text">
56-
<string/>
57-
</property>
58-
</widget>
59-
</item>
60-
</layout>
6145
</widget>
6246
</item>
63-
<item row="6" column="0" colspan="2">
64-
<widget class="QGroupBox" name="groupBox">
65-
<property name="title">
66-
<string>Slicer kernel installation</string>
47+
<item row="1" column="0" colspan="2">
48+
<widget class="QPushButton" name="StartJupyterNotebookPushButton">
49+
<property name="text">
50+
<string>Start Jupyter server</string>
6751
</property>
68-
<layout class="QVBoxLayout" name="verticalLayout_2">
69-
<item>
70-
<widget class="QLabel" name="KernelFolderLabel_2">
71-
<property name="text">
72-
<string>Run this command in a terminal in your Python environment:</string>
73-
</property>
74-
</widget>
75-
</item>
76-
<item>
77-
<widget class="QTextEdit" name="ManualInstallCommandTextEdit">
78-
<property name="maximumSize">
79-
<size>
80-
<width>16777215</width>
81-
<height>50</height>
82-
</size>
83-
</property>
84-
<property name="readOnly">
85-
<bool>true</bool>
86-
</property>
87-
</widget>
88-
</item>
89-
<item>
90-
<widget class="QPushButton" name="CopyCommandToClipboardPushButton">
91-
<property name="text">
92-
<string>Copy command to clipboard</string>
93-
</property>
94-
</widget>
95-
</item>
96-
</layout>
9752
</widget>
9853
</item>
9954
</layout>
10055
</widget>
10156
</item>
10257
<item>
103-
<widget class="ctkCollapsibleButton" name="ControlCollapsibleButton">
58+
<spacer name="verticalSpacer_2">
59+
<property name="orientation">
60+
<enum>Qt::Vertical</enum>
61+
</property>
62+
<property name="sizeType">
63+
<enum>QSizePolicy::Fixed</enum>
64+
</property>
65+
<property name="sizeHint" stdset="0">
66+
<size>
67+
<width>20</width>
68+
<height>5</height>
69+
</size>
70+
</property>
71+
</spacer>
72+
</item>
73+
<item>
74+
<widget class="ctkCollapsibleButton" name="ExternalServerCollapsibleButton">
75+
<property name="sizePolicy">
76+
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
77+
<horstretch>0</horstretch>
78+
<verstretch>0</verstretch>
79+
</sizepolicy>
80+
</property>
10481
<property name="text">
105-
<string>Control</string>
82+
<string>Jupyter server in external Python environment</string>
83+
</property>
84+
<property name="collapsed">
85+
<bool>true</bool>
10686
</property>
10787
<layout class="QVBoxLayout" name="verticalLayout_3">
10888
<item>
109-
<widget class="QPushButton" name="StartJupyterNotebookPushButton">
89+
<widget class="QLabel" name="label">
90+
<property name="text">
91+
<string>To install Slicer Kernel, execute this command in a terminal in the external Python environment:</string>
92+
</property>
93+
</widget>
94+
</item>
95+
<item>
96+
<widget class="QTextEdit" name="ManualInstallCommandTextEdit">
97+
<property name="maximumSize">
98+
<size>
99+
<width>16777215</width>
100+
<height>50</height>
101+
</size>
102+
</property>
103+
<property name="readOnly">
104+
<bool>true</bool>
105+
</property>
106+
</widget>
107+
</item>
108+
<item>
109+
<widget class="QPushButton" name="CopyCommandToClipboardPushButton">
110110
<property name="text">
111-
<string>Start Jupyter notebook</string>
111+
<string>Copy command to clipboard</string>
112112
</property>
113113
</widget>
114114
</item>

JupyterKernel/qSlicerJupyterKernelModule.cxx

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
#include "qSlicerApplication.h"
2626
#include "qSlicerPythonManager.h"
2727

28+
#include "PythonQt.h"
29+
2830
// On Windows, pyerrors.h redefines snprintf to _snprintf
2931
// which causes error C2039: '_snprintf': is not a member of 'std'
3032
// while trying to compile std::snprintf in json.hpp (in nlohmann_json).
@@ -38,6 +40,7 @@
3840
#include <QFile>
3941
#include <QLabel>
4042
#include <QMainWindow>
43+
#include <QStandardPaths>
4144
#include <QStatusBar>
4245
#include <QTextStream>
4346

@@ -74,6 +77,7 @@ class qSlicerJupyterKernelModulePrivate
7477
public:
7578
qSlicerJupyterKernelModulePrivate(qSlicerJupyterKernelModule& object);
7679

80+
QProcess InternalJupyterServer;
7781
bool Started;
7882
QString ConnectionFile;
7983
xeus::xkernel * Kernel;
@@ -353,10 +357,17 @@ bool qSlicerJupyterKernelModule::slicerKernelSpecInstallCommandArgs(QString& exe
353357
}
354358

355359
//-----------------------------------------------------------------------------
356-
bool qSlicerJupyterKernelModule::installSlicerKernel(QString pythonScriptsFolder)
360+
bool qSlicerJupyterKernelModule::installInternalJupyterServer()
357361
{
358362
Q_D(qSlicerJupyterKernelModule);
359363

364+
PythonQt::init();
365+
PythonQtObjectPtr context = PythonQt::self()->getMainModule();
366+
context.evalScript(QString("success=False; import JupyterNotebooks; server=JupyterNotebooks.SlicerJupyterServerHelper(); success=server.installRequiredPackages()"));
367+
bool success = context.getVariable("success").toBool();
368+
return success;
369+
/*
370+
360371
QString kernelspecExecutable;
361372
QStringList args;
362373
if (!this->slicerKernelSpecInstallCommandArgs(kernelspecExecutable, args))
@@ -399,36 +410,54 @@ bool qSlicerJupyterKernelModule::installSlicerKernel(QString pythonScriptsFolder
399410
return false;
400411
}
401412
return true;
413+
*/
402414
}
403415

404416
//-----------------------------------------------------------------------------
405-
bool qSlicerJupyterKernelModule::startJupyterNotebook(QString pythonScriptsFolder)
417+
bool qSlicerJupyterKernelModule::startInternalJupyterServer(QString notebookDirectory, bool detached/*=false*/)
406418
{
407419
Q_D(qSlicerJupyterKernelModule);
408-
qSlicerApplication* app = qSlicerApplication::application();
409-
410-
vtkSlicerJupyterKernelLogic* kernelLogic = vtkSlicerJupyterKernelLogic::SafeDownCast(this->logic());
411-
if (!kernelLogic)
420+
QString pythonExecutable = QStandardPaths::findExecutable("PythonSlicer");
421+
d->InternalJupyterServer.setProgram(pythonExecutable);
422+
QStringList args;
423+
args << "-m" << "notebook";
424+
args << "--notebook-dir" << notebookDirectory;
425+
d->InternalJupyterServer.setArguments(args);
426+
bool success = false;
427+
if (detached)
412428
{
413-
qWarning() << Q_FUNC_INFO << " failed: invalid logic";
414-
return false;
429+
success = d->InternalJupyterServer.startDetached();
415430
}
431+
else
432+
{
433+
d->InternalJupyterServer.start();
434+
success = d->InternalJupyterServer.waitForStarted();
435+
}
436+
return success;
437+
}
416438

417-
QString kernelExecutable = pythonScriptsFolder + "/" + "jupyter-notebook";
418-
419-
QProcess kernelSpecProcess;
420-
kernelSpecProcess.setProcessEnvironment(app->startupEnvironment());
421-
kernelSpecProcess.setProgram(kernelExecutable);
439+
//-----------------------------------------------------------------------------
440+
bool qSlicerJupyterKernelModule::isInternalJupyterServerRunning() const
441+
{
442+
Q_D(const qSlicerJupyterKernelModule);
443+
return d->InternalJupyterServer.state() == QProcess::Running;
444+
}
422445

423-
// TODO: decide if we want to allow users to start notebook from Slicer.
424-
// Detached start would require Qt-5.10 and users might want to start the notebook manually, in a virtual environment.
425-
// kernelSpecProcess.startDetached();
426-
//return true;
427-
return false;
446+
//-----------------------------------------------------------------------------
447+
bool qSlicerJupyterKernelModule::stopInternalJupyterServer()
448+
{
449+
Q_D(qSlicerJupyterKernelModule);
450+
// TOOD: currently, none of these methods work for stopping a server that is
451+
// started using AppLauncher.
452+
// terminate() is not strong enough.
453+
// kill() immediately kills the launcher but not the launched application.
454+
d->InternalJupyterServer.terminate();
455+
//d->InternalJupyterServer.kill();
456+
return d->InternalJupyterServer.waitForFinished(5000);
428457
}
429458

430459
//---------------------------------------------------------------------------
431-
QString qSlicerJupyterKernelModule::resourceFolderPath()
460+
QString qSlicerJupyterKernelModule::kernelSpecPath()
432461
{
433462
vtkSlicerJupyterKernelLogic* kernelLogic = vtkSlicerJupyterKernelLogic::SafeDownCast(this->logic());
434463
if (!kernelLogic)
@@ -442,6 +471,11 @@ QString qSlicerJupyterKernelModule::resourceFolderPath()
442471
return path;
443472
}
444473

474+
QString qSlicerJupyterKernelModule::resourceFolderPath()
475+
{
476+
return this->kernelSpecPath();
477+
}
478+
445479
//---------------------------------------------------------------------------
446480
double qSlicerJupyterKernelModule::pollIntervalSec()
447481
{

JupyterKernel/qSlicerJupyterKernelModule.h

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ qSlicerJupyterKernelModule
3737
Q_INTERFACES(qSlicerLoadableModule);
3838
Q_PROPERTY(double pollIntervalSec READ pollIntervalSec WRITE setPollIntervalSec)
3939
Q_PROPERTY(QString connectionFile READ connectionFile)
40+
Q_PROPERTY(bool internalJupyterServerRunning READ isInternalJupyterServerRunning)
4041
public:
4142

4243
typedef qSlicerLoadableModule Superclass;
@@ -56,10 +57,28 @@ qSlicerJupyterKernelModule
5657

5758
Q_INVOKABLE virtual bool updateKernelSpec();
5859

60+
/// Get path where KernelSpec is created.
61+
Q_INVOKABLE virtual QString kernelSpecPath();
62+
5963
Q_INVOKABLE virtual bool slicerKernelSpecInstallCommandArgs(QString& executable, QStringList& args);
60-
Q_INVOKABLE virtual bool installSlicerKernel(QString pythonScriptsFolder);
61-
Q_INVOKABLE virtual bool startJupyterNotebook(QString pythonScriptsFolder);
6264

65+
/// Install Jupyter server in Slicer's Python environment
66+
Q_INVOKABLE virtual bool installInternalJupyterServer();
67+
68+
/// Start Jupyter server in Slicer's Python environment.
69+
/// Set detached=false to run the server as a child process and shutdown along with the application.
70+
Q_INVOKABLE virtual bool startInternalJupyterServer(QString notebookDirectory, bool detached=true);
71+
72+
/// Stop Jupyter server in Slicer's Python environment.
73+
/// Currently it does not work (it only terminates the launcher and not the actual application).
74+
/// In the future, this method will be fixed or removed.
75+
Q_INVOKABLE virtual bool stopInternalJupyterServer();
76+
77+
/// Returns true if internal Jupyter server is successfully started and still running.
78+
/// Only applicable to server that is started with detached=false.
79+
bool isInternalJupyterServerRunning() const;
80+
81+
/// Deprecated. Use kernelSpecPath() instead.
6382
Q_INVOKABLE virtual QString resourceFolderPath();
6483

6584
double pollIntervalSec();

0 commit comments

Comments
 (0)