diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..32f9fca --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,83 @@ +name: Release Build + +on: + push: + tags: + - 'v*' + +jobs: + build-release: + strategy: + fail-fast: false + matrix: + java: [11, 21] + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Extract version from tag + id: get_version + run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + + - name: Set up GraalVM (Java 21) + if: matrix.java == 21 + uses: graalvm/setup-graalvm@v1 + with: + java-version: ${{ matrix.java }} + distribution: 'graalvm' + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up JDK (Java 11) + if: matrix.java == 11 + uses: actions/setup-java@v4 + with: + java-version: ${{ matrix.java }} + distribution: 'temurin' + + - name: Set Java type variable + id: java_type + run: | + if [ ${{ matrix.java }} -eq 21 ]; then + echo "type=graalvm" >> $GITHUB_OUTPUT + else + echo "type=jdk" >> $GITHUB_OUTPUT + fi + + - name: Build regular JAR with Maven + run: mvn -B package -DskipTests=true --file pom.xml + + - name: Rename regular JAR with version and Java type + run: | + cp target/rsession.jar target/Rsession-${{ steps.get_version.outputs.VERSION }}-${{ steps.java_type.outputs.type }}.jar + + - name: Build lite JAR (without Rserve binaries) + run: | + # Create lite version by excluding Rserve binaries (tgz, zip, tar.gz) + mkdir -p target/lite-classes + cd target/classes + + # Copy all classes and resources EXCEPT Rserve binaries and test classes + find . -type f \( -name "*.class" -o -name "*.js" -o -name "*.form" \) \ + ! -name "*Test.class" \ + ! -name "*Test$*.class" \ + -exec cp --parents {} ../lite-classes/ \; + + cd ../lite-classes + jar cf ../Rsession-lite-${{ steps.get_version.outputs.VERSION }}-${{ steps.java_type.outputs.type }}.jar . + cd ../.. + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: rsession-jars-java${{ matrix.java }} + path: | + target/Rsession-*.jar + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + files: | + target/Rsession-*.jar + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/dist/rsession-lite.jar b/dist/rsession-lite.jar deleted file mode 100644 index 9f0c43b..0000000 Binary files a/dist/rsession-lite.jar and /dev/null differ diff --git a/dist/rsession.jar b/dist/rsession.jar deleted file mode 100644 index 9379d3e..0000000 Binary files a/dist/rsession.jar and /dev/null differ diff --git a/src/main/java/org/math/R/RserveSession.java b/src/main/java/org/math/R/RserveSession.java index 60b7561..21c9753 100644 --- a/src/main/java/org/math/R/RserveSession.java +++ b/src/main/java/org/math/R/RserveSession.java @@ -187,7 +187,16 @@ public RserveSession(final RLog console, Properties properties, RserverConf serv return; } - silentlyVoidEval("if (!any(file.access(.libPaths(),2)>=0)) .libPaths(new=tempdir())"); // ensure a writable directory for libPath + // Ensure a writable directory for libPath, especially for remote Rserve + // For remote connections, prioritize tempdir() to avoid client username path issues + if (RserveConf != null && !RserveConf.isLocal()) { + // Remote Rserve: always put tempdir first to avoid /home/ issues + silentlyVoidEval(".libPaths(c(tempdir(), .libPaths()[file.access(.libPaths(), 2) >= 0]))"); + log("Remote Rserve: prioritized tempdir() for package installation", Level.INFO); + } else { + // Local Rserve: only add tempdir if no writable path exists + silentlyVoidEval("if (!any(file.access(.libPaths(),2)>=0)) .libPaths(new=tempdir())"); + } setenv(properties); } @@ -218,7 +227,7 @@ void startup() throws Exception { status = STATUS_NOT_CONNECTED; if (RserveConf == null) {// no RserveConf given, so create one, and need to be started - RserveConf = new RserverConf(RserverConf.DEFAULT_RSERVE_HOST, -1, null, null, RserverConf.DEFAULT_RSERVE_WORKDIR); + RserveConf = new RserverConf(RserverConf.DEFAULT_RSERVE_HOST, -1, null, null, System.getProperty("user.home") + File.separator + RserverConf.DEFAULT_RSERVE_WORKDIR); log("No Rserve conf given. Trying to use " + RserveConf.toString(), Level.INFO); try { @@ -253,6 +262,20 @@ void startup() throws Exception { status = STATUS_ERROR; throw new RException("Rserve " + RserveConf + " version is too old."); } else { + // Ensure workdir exists on remote Rserve + if (RserveConf.workdir != null && !RserveConf.workdir.isEmpty()) { + try { + String checkCmd = "dir.exists('" + RserveConf.workdir.replace("\\", "/") + "')"; + REXP dirExists = R.eval(checkCmd); + if (dirExists != null && dirExists.asInteger() == 0) { + log("Creating workdir on remote Rserve: " + RserveConf.workdir, Level.INFO); + String createCmd = "dir.create('" + RserveConf.workdir.replace("\\", "/") + "', recursive=TRUE, showWarnings=FALSE)"; + R.eval(createCmd); + } + } catch (Exception ex) { + log(HEAD_ERROR + "Failed to check/create workdir on remote Rserve: " + ex.getMessage(), Level.WARNING); + } + } status = STATUS_READY; } } @@ -1252,6 +1275,14 @@ public void getFile(File localfile) { getFile(localfile, localfile.getName()); } + @Override + public boolean isLocal() { + if (RserveConf == null) { + return true; + } else { + return RserveConf.isLocal(); + } + } /** * Get file from R environment to user filesystem * diff --git a/src/main/java/org/math/R/RserverConf.java b/src/main/java/org/math/R/RserverConf.java index d23f2ab..97c31e8 100644 --- a/src/main/java/org/math/R/RserverConf.java +++ b/src/main/java/org/math/R/RserverConf.java @@ -6,9 +6,10 @@ public class RserverConf { - public static String DEFAULT_RSERVE_HOST = "localhost"; // InetAddress.getLocalHost().getHostName(); should not be used, as it seems an incoming connection, not authorized + public static String DEFAULT_RSERVE_HOST = "localhost"; // InetAddress.getLocalHost().getHostName(); should not be + // used, as it seems an incoming connection, not authorized public static int DEFAULT_RSERVE_PORT = 6311; - public static String DEFAULT_RSERVE_WORKDIR = System.getProperty("user.home") + "/" + ".Rserve"; + public static String DEFAULT_RSERVE_WORKDIR = "tmp" + "/" + ".Rserve"; RConnection connection; public String host; @@ -24,6 +25,7 @@ public RserverConf(String RserverHostName, int RserverPort, String login, String this.password = password; this.workdir = workdir != null ? workdir : DEFAULT_RSERVE_WORKDIR; } + public static long CONNECT_TIMEOUT = 5000; public static abstract class TimeOut { @@ -55,6 +57,7 @@ public void run() { } } } + private boolean timedOut = false; private Object result = null; @@ -91,7 +94,7 @@ public synchronized void execute(long timeout) throws TimeOutException { } public synchronized RConnection connect() { - //Logger.err.print("Connecting " + toString()+" ... "); + // Logger.err.print("Connecting " + toString()+" ... "); TimeOut t = new TimeOut() { @@ -124,7 +127,8 @@ protected Object command() { } return 0; } catch (RserveException ex) { - Log.Err.println("Failed to connect on host:" + host + " port:" + port + " login:" + login + "\n " + ex.getMessage()); + Log.Err.println("Failed to connect on host:" + host + " port:" + port + " login:" + login + + "\n " + ex.getMessage()); } } return -1; @@ -151,7 +155,9 @@ public boolean isLocal() { @Override public String toString() { - return RURL_START + (login != null ? (login + ":" + password + "@") : "") + (host == null ? DEFAULT_RSERVE_HOST : host) + (port > 0 ? ":" + port : "") + (workdir != null ? "/" + workdir : DEFAULT_RSERVE_WORKDIR); + return RURL_START + (login != null ? (login + ":" + password + "@") : "") + + (host == null ? DEFAULT_RSERVE_HOST : host) + (port > 0 ? ":" + port : "") + + (workdir != null ? "/" + workdir : DEFAULT_RSERVE_WORKDIR); } public final static String RURL_START = "R://"; @@ -162,18 +168,18 @@ public static RserverConf parse(String RURL) { String host = null; int port = -1; String workdir = null; - //Properties props = null; + // Properties props = null; try { String loginhostportpath = null; if (RURL.contains("?")) { loginhostportpath = beforeFirst(RURL, "?").substring((RURL_START).length()); -// String[] allprops = afterFirst(RURL, "?").split("&"); -// props = new Properties(); -// for (String prop : allprops) { -// if (prop.contains("=")) { -// props.put(beforeFirst(prop, "="), afterFirst(prop, "=")); -// } // else ignore -// } + // String[] allprops = afterFirst(RURL, "?").split("&"); + // props = new Properties(); + // for (String prop : allprops) { + // if (prop.contains("=")) { + // props.put(beforeFirst(prop, "="), afterFirst(prop, "=")); + // } // else ignore + // } } else { loginhostportpath = RURL.substring((RURL_START).length()); } @@ -203,9 +209,12 @@ public static RserverConf parse(String RURL) { hostportpath = beforeFirst(hostportpath, "/"); } - return new RserverConf(host, port, login, passwd, workdir != null ? workdir : DEFAULT_RSERVE_WORKDIR); + RserverConf rc = new RserverConf(host, port, login, passwd, + workdir != null ? workdir : DEFAULT_RSERVE_WORKDIR); + return rc; } catch (Exception e) { - throw new IllegalArgumentException("Impossible to parse " + RURL + ":\n host=" + host + "\n port=" + port + "\n login=" + login + "\n password=" + passwd + "\n workdir=" + workdir); + throw new IllegalArgumentException("Impossible to parse " + RURL + ":\n host=" + host + "\n port=" + port + + "\n login=" + login + "\n password=" + passwd + "\n workdir=" + workdir); } } @@ -235,5 +244,5 @@ static String afterFirst(String txt, String sep) { } else { return ""; } - } + } } diff --git a/src/main/java/org/math/R/Rsession.java b/src/main/java/org/math/R/Rsession.java index 2c3e272..e70223f 100644 --- a/src/main/java/org/math/R/Rsession.java +++ b/src/main/java/org/math/R/Rsession.java @@ -52,6 +52,10 @@ public RException(String cause, Rsession r, boolean details) { List loggers; public boolean debug; + public boolean isLocal() { + return true; + } + //** GLG HACK: Logging fix **// // No sink file (Passed to false) a lot faster not to sink the output boolean SINK_OUTPUT = true, SINK_MESSAGE = true; @@ -707,7 +711,8 @@ public String installPackages(String[] pack, boolean load) { public String installPackage(File pack, boolean load) { pack = putFileInWorkspace(pack); try { - rawEval("install.packages('" + pack.getPath().replace("\\", "/") + "',repos=NULL,quiet=T" + install_packages_moreargs + ")"); + // Explicitly specify lib to use first writable path (avoids client username path issues) + rawEval("install.packages('" + pack.getPath().replace("\\", "/") + "',repos=NULL,lib=.libPaths()[1],quiet=T" + install_packages_moreargs + ")"); } catch (Exception ex) { log(ex.getMessage(), Level.ERROR); } @@ -819,7 +824,8 @@ public String installPackage(String pack, boolean load) { log(" package " + pack + " not accessible on " + repos + ": CRAN unreachable."); return "Impossible to get package " + pack + " from " + repos; }*/ - rawEval("install.packages('" + pack + "',repos='" + repos + "',quiet=T" + install_packages_moreargs + ")", TRY_MODE); + // Explicitly specify lib to use first writable path (avoids client username path issues) + rawEval("install.packages('" + pack + "',repos='" + repos + "',lib=.libPaths()[1],quiet=T" + install_packages_moreargs + ")", TRY_MODE); log(" request if package " + pack + " is installed...", Level.INFO); if (isPackageInstalled(pack, null)) {