mirror of
https://github.com/XaverKlemenschits/jupyter-c-kernel.git
synced 2025-04-14 10:26:09 +00:00
277 lines
10 KiB
Python
277 lines
10 KiB
Python
from queue import Queue
|
|
from threading import Thread
|
|
|
|
from ipykernel.kernelbase import Kernel
|
|
import re
|
|
import subprocess
|
|
import tempfile
|
|
import os
|
|
import os.path as path
|
|
|
|
|
|
class RealTimeSubprocess(subprocess.Popen):
|
|
"""
|
|
A subprocess that allows to read its stdout and stderr in real time
|
|
"""
|
|
|
|
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, stdin=subprocess.PIPE, bufsize=0)
|
|
|
|
self._stdout_queue = Queue()
|
|
self._stdout_thread = Thread(target=RealTimeSubprocess._enqueue_output, args=(self.stdout, self._stdout_queue))
|
|
self._stdout_thread.daemon = True
|
|
self._stdout_thread.start()
|
|
|
|
self._stderr_queue = Queue()
|
|
self._stderr_thread = Thread(target=RealTimeSubprocess._enqueue_output, args=(self.stderr, self._stderr_queue))
|
|
self._stderr_thread.daemon = True
|
|
self._stderr_thread.start()
|
|
|
|
@staticmethod
|
|
def _enqueue_output(stream, queue):
|
|
"""
|
|
Add chunks of data from a stream to a queue until the stream is empty.
|
|
"""
|
|
for line in iter(lambda: stream.read(4096), b''):
|
|
queue.put(line)
|
|
stream.close()
|
|
|
|
def write_contents(self):
|
|
"""
|
|
Write the available content from stdin and stderr where specified when the instance was created
|
|
:return:
|
|
"""
|
|
|
|
def read_all_from_queue(queue):
|
|
res = b''
|
|
size = queue.qsize()
|
|
while size != 0:
|
|
res += queue.get_nowait()
|
|
size -= 1
|
|
return res
|
|
|
|
stderr_contents = read_all_from_queue(self._stderr_queue)
|
|
if stderr_contents:
|
|
self._write_to_stderr(stderr_contents.decode())
|
|
|
|
stdout_contents = read_all_from_queue(self._stdout_queue)
|
|
if 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 = ""
|
|
while(len(readLine) == 0):
|
|
readLine = self._read_from_stdin()
|
|
# need to add newline since it is not captured by frontend
|
|
readLine += "\n"
|
|
self.stdin.write(readLine.encode())
|
|
else:
|
|
self._write_to_stdout(contents)
|
|
|
|
|
|
class CKernel(Kernel):
|
|
implementation = 'jupyter_c_kernel'
|
|
implementation_version = '1.0'
|
|
language = 'c'
|
|
language_version = 'C11'
|
|
language_info = {'name': 'c',
|
|
'mimetype': 'text/x-csrc',
|
|
'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.readOnlyFileSystem = False
|
|
self.bufferedOutput = True
|
|
self.linkMaths = True # always link math library
|
|
self.wAll = True # show all warnings by default
|
|
self.wError = False # but keep comipiling for warnings
|
|
self.standard = "c11" # default standard if none is specified
|
|
self.files = []
|
|
mastertemp = tempfile.mkstemp(suffix='.out')
|
|
os.close(mastertemp[0])
|
|
self.master_path = mastertemp[1]
|
|
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:
|
|
if(os.path.exists(file)):
|
|
os.remove(file)
|
|
os.remove(self.master_path)
|
|
|
|
def new_temp_file(self, **kwargs):
|
|
"""Create a new temp file to be deleted when the kernel shuts down"""
|
|
# We don't want the file to be deleted when closed, but only when the kernel stops
|
|
kwargs['delete'] = False
|
|
kwargs['mode'] = 'w'
|
|
file = tempfile.NamedTemporaryFile(**kwargs)
|
|
self.files.append(file.name)
|
|
return file
|
|
|
|
def _write_to_stdout(self, contents):
|
|
self.send_response(self.iopub_socket, 'stream', {'name': 'stdout', 'text': 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,
|
|
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 = ['-pedantic', '-fPIC', '-shared', '-rdynamic'] + cflags
|
|
if self.linkMaths:
|
|
cflags = cflags + ['-lm']
|
|
if self.wError:
|
|
cflags = cflags + ['-Werror']
|
|
if self.wAll:
|
|
cflags = cflags + ['-Wall']
|
|
if self.readOnlyFileSystem:
|
|
cflags = ['-DREAD_ONLY_FILE_SYSTEM'] + cflags
|
|
if self.bufferedOutput:
|
|
cflags = ['-DBUFFERED_OUTPUT'] + cflags
|
|
args = ['gcc', source_filename] + cflags + ['-o', binary_filename] + ldflags
|
|
return self.create_jupyter_subprocess(args)
|
|
|
|
def _filter_magics(self, code):
|
|
|
|
magics = {'cflags': [],
|
|
'ldflags': [],
|
|
'args': []}
|
|
|
|
actualCode = ''
|
|
|
|
for line in code.splitlines():
|
|
if line.startswith('//%'):
|
|
magicSplit = line[3:].split(":", 2)
|
|
if(len(magicSplit) < 2):
|
|
self._write_to_stderr("[C kernel] Magic line starting with '//%' is missing a semicolon, ignoring.")
|
|
continue
|
|
|
|
key, value = magicSplit
|
|
key = key.strip().lower()
|
|
|
|
if key in ['ldflags', 'cflags']:
|
|
for flag in value.split():
|
|
magics[key] += [flag]
|
|
elif key == "args":
|
|
# Split arguments respecting quotes
|
|
for argument in re.findall(r'(?:[^\s,"]|"(?:\\.|[^"])*")+', value):
|
|
magics['args'] += [argument.strip('"')]
|
|
|
|
# always add empty line, so line numbers don't change
|
|
actualCode += '\n'
|
|
|
|
# keep lines which did not contain magics
|
|
else:
|
|
actualCode += line + '\n'
|
|
|
|
# add default standard if cflags does not contain one
|
|
if not any(item.startswith('-std=') for item in magics["cflags"]):
|
|
magics["cflags"] += ["-std=" + self.standard]
|
|
|
|
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):
|
|
# remove comments
|
|
tmpCode = re.sub(r"//.*", "", code)
|
|
tmpCode = re.sub(r"/\*.*?\*/", "", tmpCode, flags=re.M|re.S)
|
|
|
|
x = re.search(r"int\s+main\s*\(", tmpCode)
|
|
|
|
if not x:
|
|
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=True):
|
|
|
|
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("<stdio.h>", headerDir)
|
|
code = code.replace("\"stdio.h\"", headerDir)
|
|
|
|
with self.new_temp_file(suffix='.c') as source_file:
|
|
source_file.write(code)
|
|
source_file.flush()
|
|
with self.new_temp_file(suffix='.out') as binary_file:
|
|
p = self.compile_with_gcc(source_file.name, binary_file.name, magics['cflags'], magics['ldflags'])
|
|
while p.poll() is None:
|
|
p.write_contents()
|
|
p.write_contents()
|
|
if p.returncode != 0: # Compilation failed
|
|
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': {}}
|
|
|
|
p = self.create_jupyter_subprocess([self.master_path, binary_file.name] + magics['args'])
|
|
while p.poll() is None:
|
|
p.write_contents()
|
|
|
|
# wait for threads to finish, so output is always shown
|
|
p._stdout_thread.join()
|
|
p._stderr_thread.join()
|
|
|
|
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': {}}
|
|
|
|
def do_shutdown(self, restart):
|
|
"""Cleanup the created source code files and executables when shutting down the kernel"""
|
|
self.cleanup_files()
|