Skip to content

Commit

Permalink
Stdin using notebook raw_input (#1)
Browse files Browse the repository at this point in the history
* Added stdin input via the notebook frontend

* Added support for single line code snippets.

* Updated README. 

* Updated Dockerfile. 

* Changed file cleanup to happen right after execution, rather than before kernel shutdown.
  • Loading branch information
XaverKlemenschits authored Mar 22, 2020
1 parent c9a327f commit ff9d635
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 24 deletions.
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
FROM jupyter/minimal-notebook
MAINTAINER Brendan Rius <ping@brendan-rius.com>
MAINTAINER Xaver Klemenschits <klemenschits@iue.tuwien.ac.at>

USER root

WORKDIR /tmp

COPY ./ jupyter_c_kernel/

RUN pip install --no-cache-dir jupyter_c_kernel/
RUN cd jupyter_c_kernel && install_c_kernel --user
RUN pip install --no-cache-dir -e jupyter_c_kernel/ > piplog.txt
RUN cd jupyter_c_kernel && install_c_kernel --user > installlog.txt

WORKDIR /home/$NB_USER/

Expand Down
22 changes: 14 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

## Use with Docker (recommended)

* `docker pull brendanrius/jupyter-c-kernel`
* `docker run -p 8888:8888 brendanrius/jupyter-c-kernel`
* `docker pull xaverklemenschits/jupyter-c-kernel`
* `docker run -p 8888:8888 xaverklemenschits/jupyter-c-kernel`
* Copy the given URL containing the token, and browse to it. For instance:

```
Copy/paste this URL into your browser when you connect for the first time,
to login with a token:
Expand All @@ -24,9 +24,14 @@ Works only on Linux and OS X. Windows is not supported yet. If you want to use t
* pip

### Step-by-step:
* `pip install jupyter-c-kernel`
* `install_c_kernel`
* `jupyter-notebook`. Enjoy!
```bash
git clone https://github.com/XaverKlemenschits/jupyter-c-kernel.git
cd jupyter-c-kernel
pip install -e .
cd jupyter_c_kernel && install_c_kernel --user
# now you can start the notebook
jupyter notebook
```

## Example of notebook

Expand All @@ -47,9 +52,10 @@ change the code in real-time in Docker. For that, just run the docker box like
that:

```bash
git clone https://github.com/brendan-rius/jupyter-c-kernel.git
git clone https://github.com/XaverKlemenschits/jupyter-c-kernel.git
cd jupyter-c-kernel
docker run -v $(pwd):/jupyter/jupyter_c_kernel/ -p 8888:8888 brendanrius/jupyter-c-kernel
docker build -t myName/jupyter .
docker run -v $(pwd):/tmp/jupyter_c_kernel/ -p 8888:8888 myName/jupyter
```

This clones the source, run the kernel, and binds the current folder (the one
Expand Down
2 changes: 1 addition & 1 deletion jupyter_c_kernel/install_c_kernel
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def install_my_kernel_spec(user=True, prefix=None):
# TODO: Copy resources once they're specified

print('Installing IPython kernel spec')
KernelSpecManager().install_kernel_spec(td, 'c', user=user, replace=True, prefix=prefix)
KernelSpecManager().install_kernel_spec(td, 'c', user=user, prefix=prefix)


def _is_root():
Expand Down
87 changes: 75 additions & 12 deletions jupyter_c_kernel/kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,19 @@ class RealTimeSubprocess(subprocess.Popen):
A subprocess that allows to read its stdout and stderr in real time
"""

def __init__(self, cmd, write_to_stdout, write_to_stderr):
inputRequest = "<inputRequest>"

def __init__(self, cmd, write_to_stdout, write_to_stderr, read_from_stdin):
"""
:param cmd: the command to execute
:param write_to_stdout: a callable that will be called with chunks of data from stdout
:param write_to_stderr: a callable that will be called with chunks of data from stderr
"""
self._write_to_stdout = write_to_stdout
self._write_to_stderr = write_to_stderr
self._read_from_stdin = read_from_stdin

super().__init__(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0)
super().__init__(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, bufsize=0)

self._stdout_queue = Queue()
self._stdout_thread = Thread(target=RealTimeSubprocess._enqueue_output, args=(self.stdout, self._stdout_queue))
Expand Down Expand Up @@ -60,36 +63,60 @@ def read_all_from_queue(queue):

stdout_contents = read_all_from_queue(self._stdout_queue)
if stdout_contents:
self._write_to_stdout(stdout_contents)
contents = stdout_contents.decode()
# if there is input request, make output and then
# ask frontend for input
start = contents.find(self.__class__.inputRequest)
if(start >= 0):
contents = contents.replace(self.__class__.inputRequest, '')
if(len(contents) > 0):
self._write_to_stdout(contents)
readLine = self._read_from_stdin()
self.stdin.write(readLine.encode())
self.stdin.write(b"\n")
else:
self._write_to_stdout(contents)

stderr_contents = read_all_from_queue(self._stderr_queue)
if stderr_contents:
self._write_to_stderr(stderr_contents)
self._write_to_stderr(stderr_contents.decode())


class CKernel(Kernel):
implementation = 'jupyter_c_kernel'
implementation_version = '1.0'
language = 'c'
language_version = 'C11'
language_info = {'name': 'c',
language_info = {'name': 'text/x-csrc',
'mimetype': 'text/plain',
'file_extension': '.c'}
banner = "C kernel.\n" \
"Uses gcc, compiles in C11, and creates source code files and executables in temporary folder.\n"

main_head = "#include <stdio.h>\n" \
"#include <math.h>\n" \
"int main(){\n"

main_foot = "\nreturn 0;\n}"

def __init__(self, *args, **kwargs):
super(CKernel, self).__init__(*args, **kwargs)
self._allow_stdin = True
self.files = []
mastertemp = tempfile.mkstemp(suffix='.out')
os.close(mastertemp[0])
self.master_path = mastertemp[1]
filepath = path.join(path.dirname(path.realpath(__file__)), 'resources', 'master.c')
self.resDir = path.join(path.dirname(path.realpath(__file__)), 'resources')
filepath = path.join(self.resDir, 'master.c')
subprocess.call(['gcc', filepath, '-std=c11', '-rdynamic', '-ldl', '-o', self.master_path])

def cleanup_files(self):
"""Remove all the temporary files created by the kernel"""
# keep the list of files create in case there is an exception
# before they can be deleted as usual
for file in self.files:
os.remove(file)
if(os.path.exists(file)):
os.remove(file)
os.remove(self.master_path)

def new_temp_file(self, **kwargs):
Expand All @@ -107,10 +134,14 @@ def _write_to_stdout(self, contents):
def _write_to_stderr(self, contents):
self.send_response(self.iopub_socket, 'stream', {'name': 'stderr', 'text': contents})

def _read_from_stdin(self):
return self.raw_input()

def create_jupyter_subprocess(self, cmd):
return RealTimeSubprocess(cmd,
lambda contents: self._write_to_stdout(contents.decode()),
lambda contents: self._write_to_stderr(contents.decode()))
self._write_to_stdout,
self._write_to_stderr,
self._read_from_stdin)

def compile_with_gcc(self, source_filename, binary_filename, cflags=None, ldflags=None):
cflags = ['-std=c11', '-fPIC', '-shared', '-rdynamic'] + cflags
Expand All @@ -123,6 +154,8 @@ def _filter_magics(self, code):
'ldflags': [],
'args': []}

actualCode = ''

for line in code.splitlines():
if line.startswith('//%'):
key, value = line[3:].split(":", 2)
Expand All @@ -136,12 +169,33 @@ def _filter_magics(self, code):
for argument in re.findall(r'(?:[^\s,"]|"(?:\\.|[^"])*")+', value):
magics['args'] += [argument.strip('"')]

return magics
# only keep lines which did not contain magics
else:
actualCode += line + '\n'

return magics, actualCode

# check whether int main() is specified, if not add it around the code
# also add common magics like -lm
def _add_main(self, magics, code):
x = re.search("int\s+main\s*\(", code)
if x is None:
code = self.main_head + code + self.main_foot
magics['cflags'] += ['-lm']

return magics, code

def do_execute(self, code, silent, store_history=True,
user_expressions=None, allow_stdin=False):
user_expressions=None, allow_stdin=True):

magics, code = self._filter_magics(code)

magics, code = self._add_main(magics, code)

magics = self._filter_magics(code)
# replace stdio with wrapped version
headerDir = "\"" + self.resDir + "/stdio_wrap.h" + "\""
code = code.replace("<stdio.h>", headerDir)
code = code.replace("\"stdio.h\"", headerDir)

with self.new_temp_file(suffix='.c') as source_file:
source_file.write(code)
Expand All @@ -155,6 +209,11 @@ def do_execute(self, code, silent, store_history=True,
self._write_to_stderr(
"[C kernel] GCC exited with code {}, the executable will not be executed".format(
p.returncode))

# delete source files before exit
os.remove(source_file.name)
os.remove(binary_file.name)

return {'status': 'ok', 'execution_count': self.execution_count, 'payload': [],
'user_expressions': {}}

Expand All @@ -163,6 +222,10 @@ def do_execute(self, code, silent, store_history=True,
p.write_contents()
p.write_contents()

# now remove the files we have just created
os.remove(source_file.name)
os.remove(binary_file.name)

if p.returncode != 0:
self._write_to_stderr("[C kernel] Executable exited with code {}".format(p.returncode))
return {'status': 'ok', 'execution_count': self.execution_count, 'payload': [], 'user_expressions': {}}
Expand Down
10 changes: 10 additions & 0 deletions jupyter_c_kernel/resources/stdio_wrap.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#include <stdio.h>

/* Replace all the necessary input functions */
#define scanf(...) printf("<inputRequest>");\
fflush(stdout);\
scanf(__VA_ARGS__);

#define getchar() printf("<inputRequest>");\
fflush(stdout);\
getchar();

0 comments on commit ff9d635

Please sign in to comment.