diff --git a/jjava/assembly/zip/install.py b/jjava/assembly/zip/install.py deleted file mode 100644 index 80a8ae6..0000000 --- a/jjava/assembly/zip/install.py +++ /dev/null @@ -1,213 +0,0 @@ -import argparse -import json -import os -import sys - -from jupyter_client.kernelspec import KernelSpecManager - -ALIASES = { - 'JJAVA_CLASSPATH': { - }, - 'JJAVA_COMPILER_OPTS': { - }, - 'JJAVA_STARTUP_SCRIPTS_PATH': { - }, - 'JJAVA_STARTUP_SCRIPT': { - }, - 'JJAVA_TIMEOUT': { - 'NO_TIMEOUT': '-1', - } -} - -NAME_MAP = { - 'classpath': 'JJAVA_CLASSPATH', - 'comp-opts': 'JJAVA_COMPILER_OPTS', - 'startup-scripts-path': 'JJAVA_STARTUP_SCRIPTS_PATH', - 'startup-script': 'JJAVA_STARTUP_SCRIPT', - 'timeout': 'JJAVA_TIMEOUT' -} - - -def type_assertion(name, type_fn): - env = NAME_MAP[name] - aliases = ALIASES.get(env, {}) - - def checker(value): - alias = aliases.get(value, value) - type_fn(alias) - return alias - - setattr(checker, '__name__', getattr(type_fn, '__name__', 'type_fn')) - return checker - - -class EnvVar(argparse.Action): - def __init__(self, option_strings, dest, aliases=None, name_map=None, list_sep=None, **kwargs): - super(EnvVar, self).__init__(option_strings, dest, **kwargs) - - if aliases is None: - aliases = {} - if name_map is None: - name_map = {} - - self.aliases = aliases - self.name_map = name_map - self.list_sep = list_sep - - for name in self.option_strings: - if name.lstrip('-') not in name_map: - raise ValueError('Name "%s" is not mapped to an environment variable' % name.lstrip('-')) - - def __call__(self, arg_parser, namespace, value, option_string=None): - if option_string is None: - raise ValueError('option_string is required') - - env = getattr(namespace, self.dest, None) - if env is None: - env = {} - - name = option_string.lstrip('-') - env_var = self.name_map[name] - - if self.list_sep: - old = env.get(env_var) - value = old + self.list_sep + str(value) if old is not None else str(value) - - env[env_var] = value - - setattr(namespace, self.dest, env) - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Install the java kernel.', - formatter_class=lambda prog: argparse.HelpFormatter(prog, max_help_position=30)) - - install_location = parser.add_mutually_exclusive_group() - install_location.add_argument( - '--user', - help='Install to the per-user kernel registry.', - action='store_true' - ) - install_location.add_argument( - '--sys-prefix', - help="Install to Python's sys.prefix. Useful in conda/virtual environments.", - action='store_true' - ) - install_location.add_argument( - '--prefix', - help=''' - Specify a prefix to install to, e.g. an env. - The kernelspec will be installed in PREFIX/share/jupyter/kernels/ - ''', - default='' - ) - - parser.add_argument( - '--classpath', - dest='env', - action=EnvVar, - aliases=ALIASES, - name_map=NAME_MAP, - help=''' - A file path separator delimited list of classpath entries that should be available to the user code. - **Important:** no matter what OS, this should use forward slash \"/\" as the file separator. - Also each path may actually be a simple glob. - ''', - type=type_assertion('classpath', str), - list_sep=os.pathsep, - ) - parser.add_argument( - '--comp-opts', - dest='env', - action=EnvVar, - aliases=ALIASES, - name_map=NAME_MAP, - help=''' - A space delimited list of command line options that would be passed to the `javac` command - when compiling a project. For example `-parameters` to enable retaining parameter names for reflection. - ''', - type=type_assertion('comp-opts', str), - list_sep=' ', - ) - parser.add_argument( - '--startup-scripts-path', - dest='env', - action=EnvVar, - aliases=ALIASES, - name_map=NAME_MAP, - help=''' - A file path separator delimited list of `.jshell` scripts to run on startup.' - This includes jjava-jshell-init.jshell and jjava-display-init.jshell. - **Important:** no matter what OS, this should use forward slash \"/\" as the file separator. - Also each path may actually be a simple glob. - ''', - type=type_assertion('startup-scripts-path', str), - list_sep=os.pathsep, - ) - parser.add_argument( - '--startup-script', - dest='env', - action=EnvVar, - aliases=ALIASES, - name_map=NAME_MAP, - help=''' - A block of java code to run when the kernel starts up. - This may be something like `import my.utils;` to setup some default imports - or even `void sleep(long time) { try {Thread.sleep(time); } catch (InterruptedException e) - { throw new RuntimeException(e); }}` to declare a default utility method to use in the notebook. - ''', - type=type_assertion('startup-script', str), - ) - parser.add_argument( - '--timeout', - dest='env', - action=EnvVar, - aliases=ALIASES, - name_map=NAME_MAP, - help=''' - A duration specifying a timeout (in milliseconds by default) for a _single top level statement_. - If less than `1` then there is no timeout. - If desired a time may be specified with a `TimeUnit` may be given following the duration number - (ex `\"30 SECONDS\"`). - ''', - type=type_assertion('timeout', str), - ) - - args = parser.parse_args() - - if not hasattr(args, 'env') or getattr(args, 'env') is None: - setattr(args, 'env', {}) - - # Install the kernel - install_dest = KernelSpecManager().install_kernel_spec( - os.path.join(os.path.dirname(os.path.abspath(__file__)), 'java'), - kernel_name='java', - user=args.user, - prefix=sys.prefix if args.sys_prefix else args.prefix, - ) - - # Prepare token replacement strings which should be properly escaped for use in a JSON string - # The [1:-1] trims the first and last " json.dumps adds for strings. - executable_path = json.dumps(sys.executable)[1:-1] - launcher_path = json.dumps(os.path.join(install_dest, 'launcher.py'))[1:-1] - kernel_path = json.dumps(os.path.join(install_dest, '${project.build.finalName}.jar'))[1:-1] - - # Prepare the paths to the installed kernel.json and the one bundled with this installer. - local_kernel_json_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'java', 'kernel.json') - installed_kernel_json_path = os.path.join(install_dest, 'kernel.json') - - # Replace tokens in the installed kernel.json. - with open(local_kernel_json_path, 'r') as template_kernel_json_file: - template_kernel_json_contents = template_kernel_json_file.read() - kernel_json_contents = template_kernel_json_contents.replace('@PY_EXECUTABLE@', executable_path) - kernel_json_contents = kernel_json_contents.replace('@LAUNCHER_PATH@', launcher_path) - kernel_json_contents = kernel_json_contents.replace('@KERNEL_PATH@', kernel_path) - print(kernel_json_contents) - kernel_json_json_contents = json.loads(kernel_json_contents) - kernel_env = kernel_json_json_contents.setdefault('env', {}) - for k, v in args.env.items(): - kernel_env[k] = v - with open(installed_kernel_json_path, 'w') as installed_kernel_json_file: - json.dump(kernel_json_json_contents, installed_kernel_json_file, indent=4, sort_keys=True) - - print('Installed java kernel into "%s"' % install_dest) diff --git a/jjava/assembly/zip/kernel.json b/jjava/assembly/zip/kernel.json index 25a4843..ad8e91e 100644 --- a/jjava/assembly/zip/kernel.json +++ b/jjava/assembly/zip/kernel.json @@ -1,8 +1,9 @@ { "argv": [ - "@PY_EXECUTABLE@", - "@LAUNCHER_PATH@", - "@KERNEL_PATH@", + "java", + "-jar", + "{resource_dir}/launcher-${project.version}.jar", + "{resource_dir}/jjava-${project.version}.jar", "{connection_file}" ], "display_name": "Java", diff --git a/jjava/assembly/zip/launcher.py b/jjava/assembly/zip/launcher.py deleted file mode 100644 index 60c1415..0000000 --- a/jjava/assembly/zip/launcher.py +++ /dev/null @@ -1,21 +0,0 @@ -import os -import subprocess -import sys - - -def launch_kernel(): - kernel_path = sys.argv[1] - connection_file = sys.argv[2] - - args = ['java', '--add-opens', 'jdk.jshell/jdk.jshell=ALL-UNNAMED', '-jar', kernel_path, connection_file] - - jvm_options = os.getenv('JJAVA_JVM_OPTS', '') - if jvm_options: - args[1:1] = jvm_options.split(' ') - - print(f"Running JJava Kernel with args: {args}") - subprocess.run(args) - - -if __name__ == '__main__': - launch_kernel() diff --git a/jjava/assembly/zip/zip.xml b/jjava/assembly/zip/zip.xml index 7d95453..07801a5 100644 --- a/jjava/assembly/zip/zip.xml +++ b/jjava/assembly/zip/zip.xml @@ -8,20 +8,13 @@ false - ${project.build.directory}/${project.build.finalName}.jar - java + ${project.build.directory}/jjava-${project.version}.jar - assembly/zip/launcher.py - java + ../launcher/${project.build.directory}/launcher-${project.version}.jar assembly/zip/kernel.json - java - - - assembly/zip/install.py - / true diff --git a/launcher/pom.xml b/launcher/pom.xml new file mode 100644 index 0000000..b238882 --- /dev/null +++ b/launcher/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + + + org.dflib.jjava + jjava-parent + 1.0-SNAPSHOT + + + launcher + Java Jupyter Kernel launcher + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + org.dflib.jjava.launcher.LauncherMain + + + + jar-with-dependencies + + false + + + + make-assembly + package + + single + + + + + + + + diff --git a/launcher/src/main/java/org/dflib/jjava/launcher/KernelLauncher.java b/launcher/src/main/java/org/dflib/jjava/launcher/KernelLauncher.java new file mode 100644 index 0000000..9018c3d --- /dev/null +++ b/launcher/src/main/java/org/dflib/jjava/launcher/KernelLauncher.java @@ -0,0 +1,97 @@ +package org.dflib.jjava.launcher; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * The launcher responsible for creating a new process to run the JJava kernel. + *

+ * The command to run the kernel is constructed by combining the JVM executable, + * the JJAVA_JVM_OPTS environment variable (if set), and the kernel arguments. + */ +public class KernelLauncher { + + private static final String JJAVA_JVM_OPTS = "JJAVA_JVM_OPTS"; + + private final List args; + + public KernelLauncher(List args) { + this.args = args; + } + + + /** + * Launches a new process to run the JJava kernel with the given arguments. + *

+ * The kernel inherits the standard input, output and error streams from the parent process. + * The method blocks until the kernel terminates. + * It also provides shutdown hooks to terminate the kernel when the JVM terminates. + * + * @return the exit code of the kernel + * @since 1.0-M4 + */ + public int launchKernel() { + List command = buildCommand(args); + System.out.println("Running JJava Kernel with args: " + command); + try { + ProcessBuilder pb = new ProcessBuilder(command); + pb.inheritIO(); + Process kernel = pb.start(); + addShutdownHook(kernel); + return kernel.waitFor(); + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + private void addShutdownHook(Process kernel) { + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + System.out.println("Launcher is shutting down, terminating the kernel..."); + kernel.destroy(); + try { + kernel.waitFor(); + System.out.println("Kernel terminated."); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + })); + } + + private List buildCommand(List args) { + if (args.size() < 2) { + throw new RuntimeException(buildErrorMessage(args)); + } + String kernelPath = args.get(0); + String connectionFile = args.get(1); + + List command = new ArrayList<>(); + command.add("java"); + + // Get JVM options from environment variable if provided + String jvmOptions = System.getenv(JJAVA_JVM_OPTS); + if (jvmOptions != null && !jvmOptions.isBlank()) { + String[] options = jvmOptions.split("\\s+"); + command.addAll(Arrays.asList(options)); + } + + // Add JVM option for JShell permissions + command.add("--add-opens"); + command.add("jdk.jshell/jdk.jshell=ALL-UNNAMED"); + command.add("-jar"); + command.add(kernelPath); + command.add(connectionFile); + return command; + } + + private String buildErrorMessage(List args) { + if (args.isEmpty()) { + return String.format("Missing arguments: %n"); + } + if (args.size() < 2) { + return String.format("Missing arguments: %s %n", args.get(0)); + } + return String.format("Arguments provided: %s%n", String.join(", ",args)); + } +} diff --git a/launcher/src/main/java/org/dflib/jjava/launcher/LauncherMain.java b/launcher/src/main/java/org/dflib/jjava/launcher/LauncherMain.java new file mode 100644 index 0000000..f2ffb26 --- /dev/null +++ b/launcher/src/main/java/org/dflib/jjava/launcher/LauncherMain.java @@ -0,0 +1,11 @@ +package org.dflib.jjava.launcher; + +import java.util.Arrays; + +public class LauncherMain { + + public static void main(String[] args) { + KernelLauncher launcher = new KernelLauncher(Arrays.asList(args)); + launcher.launchKernel(); + } +} diff --git a/pom.xml b/pom.xml index 6746b05..9dbbda8 100644 --- a/pom.xml +++ b/pom.xml @@ -11,8 +11,9 @@ Jupyter Kernel for JVM, including Java kernel - jjava + launcher jupyter-jvm-basekernel + jjava