Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/main/java/org/perlonjava/core/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
48 changes: 43 additions & 5 deletions src/main/java/org/perlonjava/runtime/perlmodule/XSLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> NON_FUNCTIONAL_ISA = Set.of(
"Exporter",
"DynaLoader",
"AutoLoader",
"XSLoader"
);

/**
* Constructor for XSLoader.
* Initializes the module with the name "XSLoader".
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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).
* <p>
* 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;
Expand Down Expand Up @@ -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();
Expand Down
7 changes: 5 additions & 2 deletions src/main/perl/lib/TAP/Parser/Iterator/Process.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
Loading