From ff9d635f89a763854278789e2c79c0772eea4927 Mon Sep 17 00:00:00 2001 From: Xaver K Date: Sun, 22 Mar 2020 12:52:45 +0100 Subject: [PATCH] Stdin using notebook raw_input (#1) * 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. --- Dockerfile | 6 +- README.md | 22 ++++--- jupyter_c_kernel/install_c_kernel | 2 +- jupyter_c_kernel/kernel.py | 87 +++++++++++++++++++++---- jupyter_c_kernel/resources/stdio_wrap.h | 10 +++ 5 files changed, 103 insertions(+), 24 deletions(-) create mode 100644 jupyter_c_kernel/resources/stdio_wrap.h diff --git a/Dockerfile b/Dockerfile index 8ff4b8b..38acb16 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ FROM jupyter/minimal-notebook -MAINTAINER Brendan Rius +MAINTAINER Xaver Klemenschits USER root @@ -7,8 +7,8 @@ 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/ diff --git a/README.md b/README.md index 7d96ad1..4c9e57a 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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 @@ -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 diff --git a/jupyter_c_kernel/install_c_kernel b/jupyter_c_kernel/install_c_kernel index fcd009d..4b23026 100644 --- a/jupyter_c_kernel/install_c_kernel +++ b/jupyter_c_kernel/install_c_kernel @@ -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(): diff --git a/jupyter_c_kernel/kernel.py b/jupyter_c_kernel/kernel.py index 7ed7e71..81f97a2 100644 --- a/jupyter_c_kernel/kernel.py +++ b/jupyter_c_kernel/kernel.py @@ -14,7 +14,9 @@ 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 = "" + + 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 @@ -22,8 +24,9 @@ class RealTimeSubprocess(subprocess.Popen): """ 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)) @@ -60,10 +63,23 @@ class RealTimeSubprocess(subprocess.Popen): 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): @@ -71,25 +87,36 @@ class CKernel(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 \n" \ + "#include \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): @@ -107,10 +134,14 @@ class CKernel(Kernel): 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 @@ -123,6 +154,8 @@ class CKernel(Kernel): 'ldflags': [], 'args': []} + actualCode = '' + for line in code.splitlines(): if line.startswith('//%'): key, value = line[3:].split(":", 2) @@ -136,12 +169,33 @@ class CKernel(Kernel): 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 = self._filter_magics(code) + magics, code = self._filter_magics(code) + + magics, code = self._add_main(magics, code) + + # replace stdio with wrapped version + headerDir = "\"" + self.resDir + "/stdio_wrap.h" + "\"" + code = code.replace("", headerDir) + code = code.replace("\"stdio.h\"", headerDir) with self.new_temp_file(suffix='.c') as source_file: source_file.write(code) @@ -155,6 +209,11 @@ class CKernel(Kernel): 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': {}} @@ -163,6 +222,10 @@ class CKernel(Kernel): 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': {}} diff --git a/jupyter_c_kernel/resources/stdio_wrap.h b/jupyter_c_kernel/resources/stdio_wrap.h new file mode 100644 index 0000000..0374711 --- /dev/null +++ b/jupyter_c_kernel/resources/stdio_wrap.h @@ -0,0 +1,10 @@ +#include + +/* Replace all the necessary input functions */ +#define scanf(...) printf("");\ +fflush(stdout);\ +scanf(__VA_ARGS__); + +#define getchar() printf("");\ +fflush(stdout);\ +getchar();