diff --git a/jjava-distro/pom.xml b/jjava-distro/pom.xml index 8ba7da4..419bfd1 100644 --- a/jjava-distro/pom.xml +++ b/jjava-distro/pom.xml @@ -132,6 +132,8 @@ org.dflib.jjava.distro.JJava + org.dflib.jjava.kernel.execution.HotswapAgent + true diff --git a/jjava-distro/src/test/java/org/dflib/jjava/distro/KernelExecutionIT.java b/jjava-distro/src/test/java/org/dflib/jjava/distro/KernelExecutionIT.java index 2768ba5..8ea204a 100644 --- a/jjava-distro/src/test/java/org/dflib/jjava/distro/KernelExecutionIT.java +++ b/jjava-distro/src/test/java/org/dflib/jjava/distro/KernelExecutionIT.java @@ -22,4 +22,23 @@ public void variableSurvivesLaterImports() throws Exception { assertEquals(0, snippetResult.getExitCode(), snippetResult.getStdout()); assertThat(snippetResult.getStdout(), not(containsString("|"))); } + + /** + * @see #119 + */ + @Test + public void variableRedeclarationUsesLatestValue() throws Exception { + String snippet = String.join("\n", + "var v = \"first\";", + "v", + "var v = \"second\";", + "v" + ); + + Container.ExecResult snippetResult = executeInKernel(snippet); + + assertEquals(0, snippetResult.getExitCode(), snippetResult.getStdout()); + assertThat(snippetResult.getStdout(), containsString("Out[2]: first")); + assertThat(snippetResult.getStdout(), containsString("Out[4]: second")); + } } diff --git a/jjava-kernel/src/main/java/org/dflib/jjava/kernel/execution/CodeEvaluator.java b/jjava-kernel/src/main/java/org/dflib/jjava/kernel/execution/CodeEvaluator.java index 9b93b85..7e0b6fd 100644 --- a/jjava-kernel/src/main/java/org/dflib/jjava/kernel/execution/CodeEvaluator.java +++ b/jjava-kernel/src/main/java/org/dflib/jjava/kernel/execution/CodeEvaluator.java @@ -169,9 +169,11 @@ private void dropSnippet(JShell shell, Snippet snippet) { // snippet.classFullName() returns name of a wrapper class created for a snippet String className = snippetClassName(snippet); // check that this class is not used by other snippets - if (shell.snippets() + boolean isUnused = shell.snippets() + .filter(s -> shell.status(s).isActive()) .map(this::snippetClassName) - .noneMatch(className::equals)) { + .noneMatch(className::equals); + if (isUnused) { execControl.unloadClass(className); } } diff --git a/jjava-kernel/src/main/java/org/dflib/jjava/kernel/execution/HotswapAgent.java b/jjava-kernel/src/main/java/org/dflib/jjava/kernel/execution/HotswapAgent.java new file mode 100644 index 0000000..da9852c --- /dev/null +++ b/jjava-kernel/src/main/java/org/dflib/jjava/kernel/execution/HotswapAgent.java @@ -0,0 +1,32 @@ +package org.dflib.jjava.kernel.execution; + +import java.lang.instrument.Instrumentation; + +/** + * Captures the JVM {@link Instrumentation} instance so that {@link JJavaLoaderDelegate} can perform + * in-place (HotSwap) redefinition of JShell snippet classes. + */ +public final class HotswapAgent { + + private static volatile Instrumentation instrumentation; + + private HotswapAgent() { + } + + public static void premain(String agentArgs, Instrumentation inst) { + instrumentation = inst; + } + + public static void agentmain(String agentArgs, Instrumentation inst) { + instrumentation = inst; + } + + public static Instrumentation instrumentation() { + return instrumentation; + } + + public static boolean canRedefine() { + Instrumentation inst = instrumentation; + return inst != null && inst.isRedefineClassesSupported(); + } +} diff --git a/jjava-kernel/src/main/java/org/dflib/jjava/kernel/execution/JJavaLoaderDelegate.java b/jjava-kernel/src/main/java/org/dflib/jjava/kernel/execution/JJavaLoaderDelegate.java index 8f7e39c..9d404fc 100644 --- a/jjava-kernel/src/main/java/org/dflib/jjava/kernel/execution/JJavaLoaderDelegate.java +++ b/jjava-kernel/src/main/java/org/dflib/jjava/kernel/execution/JJavaLoaderDelegate.java @@ -4,11 +4,16 @@ import jdk.jshell.spi.ExecutionControl; import org.dflib.jjava.jupyter.kernel.util.PathsHandler; +import java.lang.instrument.ClassDefinition; +import java.lang.instrument.Instrumentation; +import java.lang.instrument.UnmodifiableClassException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Path; import java.security.CodeSource; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -53,8 +58,41 @@ public void load(ExecutionControl.ClassBytecodes[] cbcs) throws ExecutionControl @Override public void classesRedefined(ExecutionControl.ClassBytecodes[] cbcs) { + List redefinitions = new ArrayList<>(cbcs.length); for (ExecutionControl.ClassBytecodes cbc : cbcs) { declaredClasses.put(cbc.name(), cbc.bytecodes()); + + Class loaded = loadedClasses.get(cbc.name()); + if (loaded != null) { + redefinitions.add(new ClassDefinition(loaded, cbc.bytecodes())); + } + } + redefineLoadedClasses(redefinitions); + } + + /** + * Perform in-place (HotSwap) redefinition of the given classes. This mirrors how the {@code jshell} tool + * redefines classes in its remote JVM via JDI, preserving {@link Class} identity and the values of + * static fields (i.e. JShell variable values). + */ + private void redefineLoadedClasses(List redefinitions) { + if (redefinitions.isEmpty() || !HotswapAgent.canRedefine()) { + return; + } + + Instrumentation instrumentation = HotswapAgent.instrumentation(); + try { + // Redefine all classes at once to handle interdependent changes to more than one class quickly. + instrumentation.redefineClasses(redefinitions.toArray(new ClassDefinition[0])); + } catch (UnsupportedOperationException | UnmodifiableClassException | ClassNotFoundException e) { + // Schema-changing redefinition in some class: try to redefine each class separately. + for (ClassDefinition redefinition : redefinitions) { + try { + instrumentation.redefineClasses(redefinition); + } catch (UnsupportedOperationException | UnmodifiableClassException | ClassNotFoundException ee) { + // Schema-changing redefinition: keep the new bytecode to declaredClasses. + } + } } } @@ -120,7 +158,9 @@ protected Class findClass(String name) throws ClassNotFoundException { return super.findClass(name); } try { - return super.defineClass(name, data, 0, data.length, (CodeSource) null); + Class klass = super.defineClass(name, data, 0, data.length, (CodeSource) null); + loadedClasses.put(name, klass); + return klass; } catch (LinkageError er) { // rethrow as ClassNotFoundException to let the caller properly handle this case // this error could be thrown in some cases (like static method signature change)