diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 192f6369c..841a25795 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "5e818ead7"; + public static final String gitCommitId = "9ad8b915c"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). diff --git a/src/main/java/org/perlonjava/runtime/operators/IOOperator.java b/src/main/java/org/perlonjava/runtime/operators/IOOperator.java index d8d1bd140..36429f7de 100644 --- a/src/main/java/org/perlonjava/runtime/operators/IOOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/IOOperator.java @@ -613,6 +613,11 @@ else if (secondArg.type == RuntimeScalarType.GLOB || secondArg.type == RuntimeSc RuntimeIO.registerGlobForFdRecycling(anonGlob, fh); // Use set() to modify the lvalue in place fileHandle.set(newGlob); + // Mark this scalar as the IO owner so scopeExitCleanup will close + // the handle when the variable goes out of scope. Copies of this + // reference (via set()) won't have ioOwner=true, preventing + // premature close of shared handles (e.g., Test2's dup'd STDOUT). + fileHandle.ioOwner = true; } long pid = fh.getPid(); if (pid > 0) return new RuntimeScalar(pid); diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/XSLoader.java b/src/main/java/org/perlonjava/runtime/perlmodule/XSLoader.java index 54a8af225..22f250374 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/XSLoader.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/XSLoader.java @@ -8,12 +8,27 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; +import java.util.Set; import static org.perlonjava.runtime.runtimetypes.RuntimeContextType.SCALAR; import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.scalarTrue; public class XSLoader extends PerlModuleBase { + /** + * Non-functional base classes that should be ignored when checking @ISA + * for pure-Perl fallback parents. These classes provide infrastructure + * (exporting, autoloading) but not the module's actual functionality. + * A module with only these in @ISA should NOT get the silent success + * treatment — it needs XS or its own pure-Perl fallback code. + */ + private static final Set NON_FUNCTIONAL_ISA = Set.of( + "Exporter", + "DynaLoader", + "AutoLoader", + "XSLoader" + ); + /** * Constructor for XSLoader. * Initializes the module with the name "XSLoader". @@ -94,13 +109,18 @@ public static RuntimeList load(RuntimeArray args, int ctx) { return scalarTrue.getList(); } catch (Exception e) { - // No Java XS class found. If the module's @ISA already has a pure-Perl - // parent (set by the .pm file before calling XSLoader::load), the module - // can function through inheritance. Return success so the require doesn't - // fail — the pure-Perl methods from the parent will be used. + // No Java XS class found. If the module's @ISA already has a + // functional pure-Perl parent (set by the .pm file before calling + // XSLoader::load), the module can function through inheritance. + // Return success so the require doesn't fail. + // + // We skip non-functional base classes (Exporter, DynaLoader, etc.) + // because their presence in @ISA does NOT mean the module can work + // without its XS code — e.g. Clone has @ISA=(Exporter) but still + // needs its own pure-Perl fallback to define clone(). String isaKey = moduleName + "::ISA"; RuntimeArray isa = GlobalVariable.getGlobalArray(isaKey); - if (isa != null && !isa.isEmpty()) { + if (isa != null && hasFunctionalParent(isa)) { // @ISA fallback succeeded. Also try to load any jar: PERL5LIB shim // for this module, which may provide method overrides (e.g., bug fixes // for the pure-Perl parent that the XS version would normally handle). @@ -164,6 +184,24 @@ private static boolean versionsCompatible(String javaVersion, String requestedVe return javaMajor.equals(requestedMajor); } + /** + * Checks whether @ISA contains at least one functional parent class. + * Non-functional base classes (Exporter, DynaLoader, etc.) are skipped + * because they provide infrastructure, not the module's actual methods. + * + * @param isa The module's @ISA array + * @return true if at least one entry is a functional parent + */ + private static boolean hasFunctionalParent(RuntimeArray isa) { + for (int i = 0; i < isa.size(); i++) { + String parent = isa.get(i).toString(); + if (!NON_FUNCTIONAL_ISA.contains(parent)) { + return true; + } + } + return false; + } + /** * Stub implementation of bootstrap_inherit for compatibility. * In standard Perl, this is used for inheritance-aware XS loading. diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java index 56042809d..bf206e1cd 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java @@ -65,6 +65,19 @@ private static boolean mightBeInteger(String s) { public int type; public Object value; + /** + * True if this scalar was the direct target of an {@code open()} call that + * created a new anonymous filehandle glob. Used by {@link #scopeExitCleanup} + * to distinguish "owned" filehandles (should be closed at scope exit) from + * copies/aliases of shared handles (should NOT be closed, as other variables + * still reference the same glob). + *

+ * Set by {@link org.perlonjava.runtime.operators.IOOperator#open} after creating + * a new anonymous glob. NOT copied by {@link #set(RuntimeScalar)}, so copies + * like {@code my $io = $handles->[$hid]} remain {@code false}. + */ + public boolean ioOwner; + // Constructors public RuntimeScalar() { this.type = UNDEF; @@ -1798,7 +1811,7 @@ private void closeIOOnDrop() { * @param scalar the RuntimeScalar being cleaned up (may be null if slot was already nulled) */ public static void scopeExitCleanup(RuntimeScalar scalar) { - if (scalar != null && scalar.type == GLOBREFERENCE + if (scalar != null && scalar.ioOwner && scalar.type == GLOBREFERENCE && scalar.value instanceof RuntimeGlob glob && glob.globName == null) { RuntimeScalar ioSlot = glob.getIO(); diff --git a/src/main/perl/lib/TAP/Parser/Iterator/Process.pm b/src/main/perl/lib/TAP/Parser/Iterator/Process.pm index 59306251c..6e669ee73 100644 --- a/src/main/perl/lib/TAP/Parser/Iterator/Process.pm +++ b/src/main/perl/lib/TAP/Parser/Iterator/Process.pm @@ -254,11 +254,12 @@ sub _next { while ( my @ready = $sel->can_read ) { for my $fh (@ready) { my $got = sysread $fh, my ($chunk), $chunk_size; + $got = 0 if !defined $got || $got eq ''; if ( $got == 0 ) { $sel->remove($fh); } - elsif ( $fh == $err ) { + elsif ( ref $err && $fh == $err ) { print STDERR $chunk; # echo STDERR } else { @@ -340,7 +341,9 @@ sub _finish { # If we have an IO::Select we also have an error handle to close. if ( $self->{sel} ) { - ( delete $self->{err} )->close; + if ( $self->{err} && ref $self->{err} ) { + ( delete $self->{err} )->close; + } delete $self->{sel}; } else {