From d396e5fabaea4824f8547052db238a6e881ba4df Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Mon, 12 Oct 2026 14:15:21 +0300 Subject: [PATCH 01/11] Add shared Process API for desktop and backend --- .../impl/process/NativeProcessSupport.java | 289 +++++++++++++ .../impl/process/ProcessSupport.java | 36 ++ CodenameOne/src/com/codename1/io/Process.java | 132 ++++++ .../src/com/codename1/io/ProcessBuilder.java | 156 +++++++ .../codename1/impl/ImplementationFactory.java | 4 + .../codename1/impl/ImplementationFactory.java | 4 + .../codename1/impl/ImplementationFactory.java | 4 + .../impl/process/JavaSEProcessSupport.java | 76 ++++ .../codename1/impl/ImplementationFactory.java | 4 + .../codename1/impl/ImplementationFactory.java | 4 + .../codename1/impl/ImplementationFactory.java | 4 + .../codename1/impl/ImplementationFactory.java | 4 + .../codename1/impl/ImplementationFactory.java | 4 + .../codename1/impl/ImplementationFactory.java | 4 + .../Backend-Operations.asciidoc | 11 + docs/developer-guide/Processes.asciidoc | 140 +++++++ docs/developer-guide/developer-guide.asciidoc | 2 + maven/backend/pom.xml | 7 +- .../com/codename1/io/ProcessBuilderTest.java | 56 +++ scripts/process/check-process.sh | 40 ++ scripts/process/process_child.c | 56 +++ scripts/process/process_os_test.c | 157 +++++++ vm/ByteCodeTranslator/src/cn1_process.h | 382 ++++++++++++++++++ vm/ByteCodeTranslator/src/cn1_process_vm.h | 141 +++++++ .../tools/translator/ByteCodeTranslator.java | 4 + vm/ByteCodeTranslator/src/nativeMethods.m | 2 + vm/backend/build.sh | 1 + .../com/demo/ProcessVirtualConformance.java | 62 +++ .../process/com/demo/ProcessConformance.java | 110 +++++ .../codename1/impl/ImplementationFactory.java | 36 ++ .../codename1/impl/ImplementationFactory.java | 36 ++ vm/backend/run-javase.sh | 2 +- vm/backend/shared-sources.sh | 9 + .../src/com/codename1/backend/HttpServer.java | 5 +- vm/backend/ws-conformance.sh | 2 +- 35 files changed, 1981 insertions(+), 5 deletions(-) create mode 100644 CodenameOne/src/com/codename1/impl/process/NativeProcessSupport.java create mode 100644 CodenameOne/src/com/codename1/impl/process/ProcessSupport.java create mode 100644 CodenameOne/src/com/codename1/io/Process.java create mode 100644 CodenameOne/src/com/codename1/io/ProcessBuilder.java create mode 100644 Ports/JavaSE/src/com/codename1/impl/process/JavaSEProcessSupport.java create mode 100644 docs/developer-guide/Processes.asciidoc create mode 100644 maven/core-unittests/src/test/java/com/codename1/io/ProcessBuilderTest.java create mode 100755 scripts/process/check-process.sh create mode 100644 scripts/process/process_child.c create mode 100644 scripts/process/process_os_test.c create mode 100644 vm/ByteCodeTranslator/src/cn1_process.h create mode 100644 vm/ByteCodeTranslator/src/cn1_process_vm.h create mode 100644 vm/backend/demo/process-virtual/com/demo/ProcessVirtualConformance.java create mode 100644 vm/backend/demo/process/com/demo/ProcessConformance.java create mode 100644 vm/backend/impl/javase/com/codename1/impl/ImplementationFactory.java create mode 100644 vm/backend/impl/parparvm/com/codename1/impl/ImplementationFactory.java diff --git a/CodenameOne/src/com/codename1/impl/process/NativeProcessSupport.java b/CodenameOne/src/com/codename1/impl/process/NativeProcessSupport.java new file mode 100644 index 00000000000..8c143ee4551 --- /dev/null +++ b/CodenameOne/src/com/codename1/impl/process/NativeProcessSupport.java @@ -0,0 +1,289 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.process; + +import com.codename1.io.Process; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.InterruptedIOException; +import java.util.HashMap; +import java.util.Map; + +/// ParparVM adapter shared by native desktop and backend targets. +/// Native operations only poll; bounded waits park backend virtual threads. +/// @hidden +@com.codename1.impl.SharedWithBackend +public final class NativeProcessSupport extends ProcessSupport { + public Map environment() { + Map result = new HashMap(); + String[] entries = environmentImpl(); + if (entries == null) { + throw new IllegalStateException("Cannot read process environment"); + } + for (String entry : entries) { + int split = entry.indexOf('='); + // Windows' hidden =C: drive entries are inherited when no explicit + // environment is supplied, but are not mutable environment names. + if (split > 0) { + result.put(entry.substring(0, split), entry.substring(split + 1)); + } + } + return result; + } + public Process start(String[] command, String directory, Map environment, boolean mergeError) + throws IOException { + String[] env = null; + if (environment != null) { + env = new String[environment.size()]; + int i = 0; + for (Map.Entry entry : environment.entrySet()) { + env[i++] = entry.getKey() + "=" + entry.getValue(); + } + } + Child child = new Child(); + long handle = launch(command, directory, env, mergeError); + if (handle <= 0) { + throw new IOException("Cannot start " + command[0] + " (OS error " + (-handle) + ")"); + } + synchronized (child) { + child.handle = handle; + try { + child.startReaper(); + } catch (RuntimeException ex) { + abandon(handle); + child.handle = 0; + throw ex; + } catch (Error ex) { + abandon(handle); + child.handle = 0; + throw ex; + } + } + return child; + } + private static final class Child extends Process implements Runnable { + private long handle; + private boolean exited; + private int exitCode; + private final InputStream stdout = new PipeInput(1); + private final InputStream stderr = new PipeInput(2); + private final OutputStream stdin = new PipeOutput(); + private final boolean[] closed = new boolean[3]; + void startReaper() { + // Own the child until reaped even if the application drops Process. + // No pipe is closed on exit: unread output remains available. + Thread reaper = new Thread(this, "CN1 process reaper"); + reaper.setDaemon(true); + reaper.start(); + } + public void run() { + while (isAlive()) { + try { + Thread.sleep(10); + } catch (InterruptedException ignored) { + } + } + } + public InputStream getInputStream() { + return stdout; + } + public InputStream getErrorStream() { + return stderr; + } + public OutputStream getOutputStream() { + return stdin; + } + public synchronized int exitValue() { + if (!exited) { + long result = poll(handle); + if (result == Long.MIN_VALUE) { + throw new IllegalThreadStateException("Process is running"); + } + exited = true; + exitCode = (int) result; + releaseIfClosed(); + } + return exitCode; + } + public synchronized void destroy() { + if (handle != 0) { + terminate(handle, false); + } + } + public synchronized Process destroyForcibly() { + if (handle != 0) { + terminate(handle, true); + } + return this; + } + private synchronized void closePipe(int index) { + if (!closed[index]) { + closed[index] = true; + closePipeImpl(handle, index); + } + releaseIfClosed(); + } + private void releaseIfClosed() { + if (handle != 0 && exited && closed[0] && closed[1] && closed[2]) { + release(handle); + handle = 0; + } + } + private synchronized int transfer(int index, byte[] bytes, int offset, int length) throws IOException { + if (closed[index]) { + throw new IOException("Stream closed"); + } + int n = transferImpl(handle, index, bytes, offset, length); + if (n == -2) { + throw new IOException("Process pipe I/O failed"); + } + return n; + } + protected synchronized void finalize() { + // The reaper holds this object until the OS child is gone. Streams + // hold it too; finalization cannot race a poll, I/O or termination. + if (handle != 0) { + release(handle); + handle = 0; + } + } + private static void bounds(byte[] bytes, int offset, int length) { + if (bytes == null) { + throw new NullPointerException(); + } + if (offset < 0 || length < 0 || length > bytes.length - offset) { + throw new IndexOutOfBoundsException(); + } + } + protected void pause(long millis) throws InterruptedException { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + pauseImpl(millis); + if (Thread.interrupted()) { + throw new InterruptedException(); + } + } + private void pausePipe() throws InterruptedIOException { + try { + pause(5); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new InterruptedIOException("Interrupted process I/O"); + } + } + private final class PipeInput extends InputStream { + private final int index; + PipeInput(int index) { + this.index = index; + } + public int read() throws IOException { + byte[] one = new byte[1]; + return read(one, 0, 1) < 0 ? -1 : one[0] & 255; + } + private boolean reading; + public int read(byte[] bytes, int offset, int length) throws IOException { + bounds(bytes, offset, length); + if (length == 0) { + return 0; + } + synchronized (this) { + if (reading) { + throw new IOException("Concurrent reads on a process pipe"); + } + reading = true; + } + try { + while (true) { + int n = transfer(index, bytes, offset, length); + if (n != -3) { + return n; + } + try { + pausePipe(); + } catch (InterruptedIOException ex) { + closePipe(index); + throw ex; + } + } + } finally { + synchronized (this) { + reading = false; + } + } + } + public void close() { + closePipe(index); + } + } + private final class PipeOutput extends OutputStream { + public void write(int value) throws IOException { + write(new byte[] {(byte) value}, 0, 1); + } + private boolean writing; + public void write(byte[] bytes, int offset, int length) throws IOException { + bounds(bytes, offset, length); + synchronized (this) { + if (writing) { + throw new IOException("Concurrent writes on a process pipe"); + } + writing = true; + } + int sent = 0; + try { + while (sent < length) { + int n = transfer(0, bytes, offset + sent, length - sent); + if (n == -3) { + try { + pausePipe(); + } catch (InterruptedIOException ex) { + closePipe(0); + ex.bytesTransferred = sent; + throw ex; + } + } else { + sent += n; + } + } + } finally { + synchronized (this) { + writing = false; + } + } + } + public void close() { + closePipe(0); + } + } + } + private static native void pauseImpl(long millis); + private static native String[] environmentImpl(); + private static native long launch(String[] command, String directory, String[] environment, boolean mergeError); + private static native long poll(long handle); + private static native void terminate(long handle, boolean force); + private static native int transferImpl(long handle, int index, byte[] bytes, int offset, int length); + private static native void closePipeImpl(long handle, int index); + private static native void release(long handle); + private static native void abandon(long handle); +} diff --git a/CodenameOne/src/com/codename1/impl/process/ProcessSupport.java b/CodenameOne/src/com/codename1/impl/process/ProcessSupport.java new file mode 100644 index 00000000000..da16e9cba94 --- /dev/null +++ b/CodenameOne/src/com/codename1/impl/process/ProcessSupport.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.process; + +import com.codename1.io.Process; +import java.io.IOException; +import java.util.Map; + +/// Internal port adapter, independent of Display and backend initialization. +/// @hidden +@com.codename1.impl.SharedWithBackend +public abstract class ProcessSupport { + public abstract Map environment(); + public abstract Process start( + String[] command, String directory, Map environment, boolean mergeError) throws IOException; +} diff --git a/CodenameOne/src/com/codename1/io/Process.java b/CodenameOne/src/com/codename1/io/Process.java new file mode 100644 index 00000000000..4d1ce55c27e --- /dev/null +++ b/CodenameOne/src/com/codename1/io/Process.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.io; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.concurrent.TimeUnit; + +/// A child OS process. Stream directions and lifecycle methods follow JavaSE. +/// Read stdout and stderr concurrently to avoid filling a pipe. Never wait or do +/// blocking stream I/O on the EDT. Interrupting a wait does not kill the child. +/// Unlike JavaSE, this class is AutoCloseable: close forcibly terminates the direct +/// child and closes its three streams. Descendants are not terminated. +@com.codename1.impl.SharedWithBackend +public abstract class Process implements AutoCloseable { + /// The child's stdin (the parent writes to it). + public abstract OutputStream getOutputStream(); + /// The child's stdout (the parent reads from it). + public abstract InputStream getInputStream(); + /// The child's stderr; EOF when redirectErrorStream(true) was selected. + public abstract InputStream getErrorStream(); + /// Returns the exit code, or throws IllegalThreadStateException while alive. + public abstract int exitValue(); + /// Requests termination of the direct child. On Windows this is forcible. + public abstract void destroy(); + /// Forcibly terminates the direct child; completion is asynchronous. + public abstract Process destroyForcibly(); + /// Whether the direct child is still running. + public boolean isAlive() { + try { + exitValue(); + return false; + } catch (IllegalThreadStateException ex) { + return true; + } + } + /// Waits for exit. An interrupt cancels only this wait. + public int waitFor() throws InterruptedException { + while (true) { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + try { + return exitValue(); + } catch (IllegalThreadStateException ex) { + pause(10); + } + } + } + /// Waits at most timeout milliseconds. Zero/negative values only poll. + public boolean waitFor(long timeoutMillis) throws InterruptedException { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + if (!isAlive()) { + return true; + } + if (timeoutMillis <= 0) { + return false; + } + // Subtraction remains valid across nanoTime's signed wraparound. + long budget = timeoutMillis > Long.MAX_VALUE / 1000000L ? Long.MAX_VALUE : timeoutMillis * 1000000L; + long start = System.nanoTime(); + while (isAlive()) { + long remaining = budget - (System.nanoTime() - start); + if (remaining <= 0) { + return false; + } + pause(Math.min(10L, Math.max(1L, remaining / 1000000L))); + } + return true; + } + /// Port hook for a bounded wait between exit polls. + protected void pause(long millis) throws InterruptedException { + Thread.sleep(millis); + if (Thread.interrupted()) { + throw new InterruptedException(); + } + } + /// JavaSE-style timed wait; sub-millisecond positive durations round up. + public boolean waitFor(long timeout, TimeUnit unit) throws InterruptedException { + if (unit == null) { + throw new NullPointerException("unit"); + } + return waitFor(timeout > 0 ? Math.max(1L, unit.toMillis(timeout)) : 0L); + } + /// Forcibly terminates the child and releases all pipes, even if one close fails. + /// The runtime reaps it independently; this does not wait for termination. + public void close() throws IOException { + destroyForcibly(); + IOException failure = null; + try { + getOutputStream().close(); + } catch (IOException ex) { + failure = ex; + } + try { + getInputStream().close(); + } catch (IOException ex) { + failure = ex; + } + try { + getErrorStream().close(); + } catch (IOException ex) { + failure = ex; + } + if (failure != null) { + throw failure; + } + } +} diff --git a/CodenameOne/src/com/codename1/io/ProcessBuilder.java b/CodenameOne/src/com/codename1/io/ProcessBuilder.java new file mode 100644 index 00000000000..c6ae14b674d --- /dev/null +++ b/CodenameOne/src/com/codename1/io/ProcessBuilder.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.io; + +import com.codename1.impl.ImplementationFactory; +import com.codename1.impl.process.ProcessSupport; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/// Creates desktop/backend subprocesses using JavaSE-style command lists and pipes. +/// The working directory is an OS path String, not a Codename One file URL. +/// Shell parsing is never implicit: invoke a shell explicitly if needed. +/// Mobile/browser ports compile this API but start() throws +/// UnsupportedOperationException. Support does not imply permission to run every +/// executable. Builders are mutable and are not thread safe. +/// +/// File redirection and inheritIO are not provided in this initial subset. Use +/// the streams and drain stdout/stderr concurrently, or merge them first. +@com.codename1.impl.SharedWithBackend +public final class ProcessBuilder { + private List command; + private String directory; + private Map environment; + private boolean mergeError; + + public ProcessBuilder(String... command) { + command(command); + } + public ProcessBuilder(List command) { + command(command); + } + + /// Whether this port implements subprocess creation (no Display init required). + public static boolean isSupported() { + return support() != null; + } + private static ProcessSupport support() { + return (ProcessSupport) ImplementationFactory.getInstance().createProcessSupport(); + } + /// Returns the live command list, as in JavaSE. + public List command() { + return command; + } + /// Keeps the supplied list, as in JavaSE. Validated again at start(). + public ProcessBuilder command(List command) { + if (command == null) { + throw new NullPointerException("command"); + } + this.command = command; + return this; + } + public ProcessBuilder command(String... command) { + if (command == null) { + throw new NullPointerException("command"); + } + this.command = new ArrayList(); + for (String arg : command) { + this.command.add(arg); + } + return this; + } + /// Null inherits the parent's working directory. Paths are native OS paths. + public ProcessBuilder directory(String directory) { + if (directory != null) { + checkString(directory); + } + this.directory = directory; + return this; + } + public String directory() { + return directory; + } + /// A mutable copy of the inherited environment, initialized on first access. + /// Changes are validated at start(), including changes through entrySet(). + /// Throws UnsupportedOperationException on unsupported ports. + public Map environment() { + if (environment == null) { + ProcessSupport support = support(); + if (support == null) { + throw new UnsupportedOperationException("Processes are not supported on this port"); + } + environment = support.environment(); + } + return environment; + } + public ProcessBuilder redirectErrorStream(boolean merge) { + mergeError = merge; + return this; + } + public boolean redirectErrorStream() { + return mergeError; + } + + /// Launches a child. Invalid arguments throw IllegalArgumentException (or + /// NullPointerException); launch/permission errors throw IOException or + /// SecurityException. An unsupported port throws UnsupportedOperationException. + public Process start() throws IOException { + ProcessSupport support = support(); + if (support == null) { + throw new UnsupportedOperationException("Processes are not supported on this port"); + } + String[] args = command.toArray(new String[command.size()]); + if (args.length == 0) { + throw new IllegalArgumentException("Empty command"); + } + for (String arg : args) { + checkString(arg); + } + if (args[0].length() == 0) { + throw new IllegalArgumentException("Empty executable"); + } + Map env = null; + if (environment != null) { + env = new HashMap(environment); + for (Map.Entry entry : env.entrySet()) { + checkString(entry.getKey()); + checkString(entry.getValue()); + if (entry.getKey().length() == 0 || entry.getKey().indexOf('=') >= 0) { + throw new IllegalArgumentException("Invalid environment name"); + } + } + } + return support.start(args, directory, env, mergeError); + } + private static void checkString(String value) { + if (value == null) { + throw new NullPointerException(); + } + if (value.indexOf('\0') >= 0) { + throw new IllegalArgumentException("Embedded NUL"); + } + } +} diff --git a/Factory/src/com/codename1/impl/ImplementationFactory.java b/Factory/src/com/codename1/impl/ImplementationFactory.java index 15a82728c5a..ab1ec79988f 100644 --- a/Factory/src/com/codename1/impl/ImplementationFactory.java +++ b/Factory/src/com/codename1/impl/ImplementationFactory.java @@ -64,4 +64,8 @@ public static void setInstance(ImplementationFactory i) { public Object createImplementation() { throw new RuntimeException("You must include the platform port before the Codename One in the classpath"); } + /** Internal process adapter; null means this port does not support processes. */ + public Object createProcessSupport() { + return null; + } } diff --git a/Ports/Android/src/com/codename1/impl/ImplementationFactory.java b/Ports/Android/src/com/codename1/impl/ImplementationFactory.java index 8fdfffccaf9..49d75936d05 100644 --- a/Ports/Android/src/com/codename1/impl/ImplementationFactory.java +++ b/Ports/Android/src/com/codename1/impl/ImplementationFactory.java @@ -69,4 +69,8 @@ public Object createImplementation() { } return new AndroidImplementation(); } + /** Internal process adapter; null means this port does not support processes. */ + public Object createProcessSupport() { + return null; + } } diff --git a/Ports/JavaSE/src/com/codename1/impl/ImplementationFactory.java b/Ports/JavaSE/src/com/codename1/impl/ImplementationFactory.java index 0fed712869d..b79ac48d3df 100644 --- a/Ports/JavaSE/src/com/codename1/impl/ImplementationFactory.java +++ b/Ports/JavaSE/src/com/codename1/impl/ImplementationFactory.java @@ -103,4 +103,8 @@ public Object createImplementation() { return new JavaJMFSEPort(); } + /** Internal process adapter; null means this port does not support processes. */ + public Object createProcessSupport() { + return new com.codename1.impl.process.JavaSEProcessSupport(); + } } diff --git a/Ports/JavaSE/src/com/codename1/impl/process/JavaSEProcessSupport.java b/Ports/JavaSE/src/com/codename1/impl/process/JavaSEProcessSupport.java new file mode 100644 index 00000000000..e733e62bc0f --- /dev/null +++ b/Ports/JavaSE/src/com/codename1/impl/process/JavaSEProcessSupport.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.process; + +import com.codename1.io.Process; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; + +/// Host-JDK adapter, also compiled by the JVM backend. No UI dependencies. +/// @hidden +public final class JavaSEProcessSupport extends ProcessSupport { + public Map environment() { + return new HashMap(new java.lang.ProcessBuilder().environment()); + } + public Process start(String[] command, String directory, Map environment, boolean mergeError) + throws IOException { + java.lang.ProcessBuilder builder = new java.lang.ProcessBuilder(command); + if (directory != null) { + builder.directory(new java.io.File(directory)); + } + if (environment != null) { + builder.environment().clear(); + builder.environment().putAll(environment); + } + builder.redirectErrorStream(mergeError); + return new Child(builder.start()); + } + private static final class Child extends Process { + private final java.lang.Process child; + Child(java.lang.Process child) { + this.child = child; + } + public InputStream getInputStream() { + return child.getInputStream(); + } + public InputStream getErrorStream() { + return child.getErrorStream(); + } + public OutputStream getOutputStream() { + return child.getOutputStream(); + } + public int exitValue() { + return child.exitValue(); + } + public void destroy() { + child.destroy(); + } + public Process destroyForcibly() { + child.destroyForcibly(); + return this; + } + } +} diff --git a/Ports/JavaSEWithSVGSupport/src/com/codename1/impl/ImplementationFactory.java b/Ports/JavaSEWithSVGSupport/src/com/codename1/impl/ImplementationFactory.java index 5cd125cb3b6..7db34abbe61 100644 --- a/Ports/JavaSEWithSVGSupport/src/com/codename1/impl/ImplementationFactory.java +++ b/Ports/JavaSEWithSVGSupport/src/com/codename1/impl/ImplementationFactory.java @@ -106,4 +106,8 @@ public Object createImplementation() { return new JavaSEPortWithSVGSupport(); } + /** Internal process adapter; null means this port does not support processes. */ + public Object createProcessSupport() { + return new com.codename1.impl.process.JavaSEProcessSupport(); + } } diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/ImplementationFactory.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/ImplementationFactory.java index 01b809cbe6f..e2f8ac28846 100644 --- a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/ImplementationFactory.java +++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/ImplementationFactory.java @@ -51,4 +51,8 @@ public String proxifyURL(String url) { }); return implementation; } + /** Internal process adapter; null means this port does not support processes. */ + public Object createProcessSupport() { + return null; + } } diff --git a/Ports/LinuxPort/src/com/codename1/impl/ImplementationFactory.java b/Ports/LinuxPort/src/com/codename1/impl/ImplementationFactory.java index 8b229e8a1c6..8a611c45f5c 100644 --- a/Ports/LinuxPort/src/com/codename1/impl/ImplementationFactory.java +++ b/Ports/LinuxPort/src/com/codename1/impl/ImplementationFactory.java @@ -69,4 +69,8 @@ public Object createImplementation() { } return new LinuxImplementation(); } + /** Internal process adapter; null means this port does not support processes. */ + public Object createProcessSupport() { + return new com.codename1.impl.process.NativeProcessSupport(); + } } diff --git a/Ports/MacPort/src/com/codename1/impl/ImplementationFactory.java b/Ports/MacPort/src/com/codename1/impl/ImplementationFactory.java index 2761c97bf04..1b229bdfd35 100644 --- a/Ports/MacPort/src/com/codename1/impl/ImplementationFactory.java +++ b/Ports/MacPort/src/com/codename1/impl/ImplementationFactory.java @@ -66,4 +66,8 @@ public static void setInstance(ImplementationFactory i) { public Object createImplementation() { return new MacImplementation(); } + /** Internal process adapter; null means this port does not support processes. */ + public Object createProcessSupport() { + return new com.codename1.impl.process.NativeProcessSupport(); + } } diff --git a/Ports/WindowsPort/src/com/codename1/impl/ImplementationFactory.java b/Ports/WindowsPort/src/com/codename1/impl/ImplementationFactory.java index 39b7e021cb8..7bfef386f51 100644 --- a/Ports/WindowsPort/src/com/codename1/impl/ImplementationFactory.java +++ b/Ports/WindowsPort/src/com/codename1/impl/ImplementationFactory.java @@ -69,4 +69,8 @@ public Object createImplementation() { } return new WindowsImplementation(); } + /** Internal process adapter; null means this port does not support processes. */ + public Object createProcessSupport() { + return new com.codename1.impl.process.NativeProcessSupport(); + } } diff --git a/Ports/iOSPort/src/com/codename1/impl/ImplementationFactory.java b/Ports/iOSPort/src/com/codename1/impl/ImplementationFactory.java index 781883570a3..9d9f391a649 100644 --- a/Ports/iOSPort/src/com/codename1/impl/ImplementationFactory.java +++ b/Ports/iOSPort/src/com/codename1/impl/ImplementationFactory.java @@ -66,4 +66,8 @@ public static void setInstance(ImplementationFactory i) { public Object createImplementation() { return new IOSImplementation(); } + /** Internal process adapter; null means this port does not support processes. */ + public Object createProcessSupport() { + return null; + } } diff --git a/docs/developer-guide/Backend-Operations.asciidoc b/docs/developer-guide/Backend-Operations.asciidoc index 04f018e5c53..f7407330b81 100644 --- a/docs/developer-guide/Backend-Operations.asciidoc +++ b/docs/developer-guide/Backend-Operations.asciidoc @@ -187,3 +187,14 @@ no TLS, and what it does need is exactly what a translated binary is good at. The reasons to choose this are cold start, footprint, deployment shape, and one language across the app and its server. If none of those matter for the service in front of you, use a JVM framework. + + +=== External Programs + +Backend applications use the shared `com.codename1.io.ProcessBuilder` and +`com.codename1.io.Process` API described in <>. The same API serves +JavaSE and native desktop applications. Request-scoped children must be closed +on completion or cancellation; long-lived children belong to a managed bean +with a `@PreDestroy` cleanup method. A timed wait does not itself terminate a +child. See that chapter for pipe draining, virtual-thread behavior, platform +restrictions, and the differences from JavaSE. diff --git a/docs/developer-guide/Processes.asciidoc b/docs/developer-guide/Processes.asciidoc new file mode 100644 index 00000000000..9913bf6d205 --- /dev/null +++ b/docs/developer-guide/Processes.asciidoc @@ -0,0 +1,140 @@ +[[processes]] +== Desktop and Backend Processes + +`com.codename1.io.ProcessBuilder` launches operating-system programs from shared +application code. It returns `com.codename1.io.Process`, with stdin, stdout, +stderr, exit status, timed waits, and termination. Neither class needs a running +`Display` or backend server. Import `Process` explicitly: `java.lang.Process` has +the same simple name. + +[cols="1,2",options="header"] +|=== +|Target |Implementation +|JavaSE desktop and simulator on Windows, macOS, Linux |Host JDK process API +|Native Windows desktop |Win32 `CreateProcessW`, Unicode arguments, asynchronous pipes +|Native macOS and Linux desktop |POSIX child processes and nonblocking pipes +|JVM backend |The same adapter as JavaSE desktop +|Packaged ParparVM backend on Linux |The same native adapter as desktop +|Android, iOS, browser and ports without an adapter |Unsupported; shared code still compiles +|=== + +Call `ProcessBuilder.isSupported()` before offering process-dependent features. +On unsupported ports, `start()` and `environment()` throw +`UnsupportedOperationException`. A supported port can still deny a launch due +to permissions, sandbox rules, or a missing executable. Those failures surface +as `IOException` (or a host-JDK `SecurityException`), not a dummy process. Mobile +ports deliberately do not promise execution even where an OS exposes limited +process facilities. + +=== Compatibility with JavaSE + +[cols="1,2",options="header"] +|=== +|Operation |Codename One behavior +|Command |`ProcessBuilder(String...)`, `ProcessBuilder(List)`, and mutable `command()` follow the JavaSE shape. Each argument is passed literally. No shell is inserted, including for executable scripts without an interpreter header on POSIX. +|Working directory |`directory(String)` accepts a native OS path, not `java.io.File`, `com.codename1.io.File`, or a `file:` URL. Null inherits the parent directory. +|Environment |`environment()` returns a mutable copy on first access. Until accessed, the child inherits the environment at launch. Invalid names/values (null, NUL, empty names, or `=` in names) are rejected at `start()`, including mutations through map entries. This validation timing and lazy snapshot differ from JavaSE. +|Pipes |`getOutputStream()` writes child stdin; `getInputStream()` reads stdout; `getErrorStream()` reads stderr. `redirectErrorStream(true)` merges stderr into stdout and makes the stderr stream return EOF. +|Redirection |File redirection, `inheritIO()`, pipelines, and `ProcessHandle` are outside this subset. Copy through the streams instead. +|Waiting |`waitFor()`, `waitFor(long, TimeUnit)`, and the extra `waitFor(long timeoutMillis)` wait for the direct child. A timeout returns false and leaves it running. Interrupting a wait throws `InterruptedException` and also leaves it running. +|Termination |`destroy()` requests SIGTERM on POSIX and terminates forcibly on Windows. `destroyForcibly()` requests SIGKILL on POSIX or Windows termination. Both return before termination completes; use a wait to observe completion. Neither terminates descendants. +|Exit codes |Nonzero exit is a normal result. `exitValue()` throws `IllegalThreadStateException` while alive. POSIX signal termination yields `128 + signal`; Windows preserves the exit-code bits in a Java int. +|Cleanup |`Process` implements `AutoCloseable`. `close()` requests forcible termination and closes all three streams, even if one close fails. It does not wait. The runtime independently reaps children, including when callers never wait. Closing streams after exit releases their resources; unread output is retained until read or closed. +|Concurrency |Use one reader per output stream and one writer for stdin. The native adapter rejects overlapping reads or writes on the same stream. Closing a stream from another thread cancels native pipe I/O. Native interrupted pipe I/O throws `InterruptedIOException` and closes that pipe; JVM stream interruption follows the host JDK. +|=== + +Executable lookup follows the host OS. Prefer an absolute executable path for +consistent behavior across operating systems. POSIX uses the child environment's +`PATH` (or `/usr/bin:/bin` if absent); Windows uses `CreateProcessW` lookup rules, +which do not use a replacement child `PATH`. Windows argument quoting targets +the standard C runtime argument parser; programs with custom command-line parsers +can interpret their command line differently. Use the existing capitalization +of Windows environment names when editing the map, and avoid duplicate names +that differ only in case. + +=== A Desktop Example + +Run process I/O and waits off the EDT. This example invokes a small version +command, drains merged output concurrently, and enforces a wait timeout. Replace +`my-tool` with an installed executable or its absolute path. + +[source,java] +---- +import com.codename1.io.Process; +import com.codename1.io.ProcessBuilder; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +// Check isSupported() before enabling this action in your UI. +new Thread(() -> { + try (Process child = new ProcessBuilder("my-tool", "--version") + .redirectErrorStream(true).start()) { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + Thread reader = new Thread(() -> { + try { + byte[] buffer = new byte[4096]; + int count; + while ((count = child.getInputStream().read(buffer)) != -1) { + output.write(buffer, 0, count); + } + } catch (IOException error) { + // Closing the process also cancels an outstanding native read. + com.codename1.io.Log.e(error); + } + }); + reader.start(); + child.getOutputStream().close(); // No input; send EOF to the tool. + if (!child.waitFor(30, TimeUnit.SECONDS)) { + child.destroyForcibly(); + } + child.waitFor(); + reader.join(); + String version = new String(output.toByteArray(), "UTF-8"); + // Use Display.callSerially(...) for any UI update using version. + } catch (Exception error) { + com.codename1.io.Log.e(error); + } +}).start(); +---- + +For commands with substantial output, stream to a consumer or file rather than +retaining everything in memory. Without stderr merging, start independent +stdout and stderr readers. Waiting before draining can deadlock when a pipe +fills. A child can also spawn descendants that keep pipes open after it exits; +manage such tools explicitly rather than assuming child exit implies pipe EOF. + +=== Backend Ownership and Blocking + +A request owns the children it starts. Close them in `finally` or a +try-with-resources block, including on cancellation and timeout. Server-wide +children should belong to a managed bean whose `@PreDestroy` method closes them. +The process API does not attach arbitrary children to a request or server, and +server shutdown does not implicitly kill unrelated processes. Abrupt termination +of the parent can leave children running, as with JavaSE. + +Native pipe reads/writes and exit checks are nonblocking OS operations. Between +polls, unpinned backend virtual threads park through the backend scheduler; +platform threads sleep briefly. Do not hold application monitors around these +operations: a virtual thread holding a monitor is pinned to its host. Launch +itself crosses the OS process-creation boundary synchronously and can occupy the +calling host; use a platform task if executable lookup or launch can be slow. + +=== Local Validation + +`scripts/process/check-process.sh jvm` runs real-child tests against the shared +JavaSE adapter and OS layer. `scripts/process/check-process.sh native` builds and +executes the same Java conformance program with ParparVM. Set `JDK_8_HOME` to a +Java 8 installation. Native tests are local-only; the script rejects native +execution when `CI` is set. + +The portable `scripts/process/process_os_test.c` also builds on Windows alongside +`process_child.c`. Pass the child executable and an existing absolute working +directory to the test. With MinGW, compile both fixtures with `-municode` to +use their Unicode entry points. Tests cover literal/Unicode arguments, environment, +working directory, large simultaneous stdout/stderr, binary stdin round trips, +merged stderr, launch errors, termination, and repeated launches. The Java +suite additionally covers waits, interrupts, and `close()`. Native mode also +runs a backend with one virtual-thread host and verifies that a pending process +read or wait permits another task to run. The OS test checks descriptor/handle +counts across repeated launches, child reaping, and closed-stdin failure. diff --git a/docs/developer-guide/developer-guide.asciidoc b/docs/developer-guide/developer-guide.asciidoc index 1864cefc2dc..fd86060c6aa 100644 --- a/docs/developer-guide/developer-guide.asciidoc +++ b/docs/developer-guide/developer-guide.asciidoc @@ -103,6 +103,8 @@ include::Game-Assets.asciidoc[] include::io.asciidoc[] +include::Processes.asciidoc[] + include::Network-Connectivity.asciidoc[] include::Annotation-JSON-XML-Mapping.asciidoc[] diff --git a/maven/backend/pom.xml b/maven/backend/pom.xml index 56f2066f1ef..203c74ac4ae 100644 --- a/maven/backend/pom.xml +++ b/maven/backend/pom.xml @@ -173,6 +173,9 @@ + + + @@ -181,7 +184,7 @@ - + @@ -204,7 +207,7 @@ - + diff --git a/maven/core-unittests/src/test/java/com/codename1/io/ProcessBuilderTest.java b/maven/core-unittests/src/test/java/com/codename1/io/ProcessBuilderTest.java new file mode 100644 index 00000000000..8135eaab3b5 --- /dev/null +++ b/maven/core-unittests/src/test/java/com/codename1/io/ProcessBuilderTest.java @@ -0,0 +1,56 @@ +package com.codename1.io; + +import com.codename1.impl.ImplementationFactory; +import com.codename1.impl.process.ProcessSupport; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class ProcessBuilderTest { + private final ImplementationFactory previous = ImplementationFactory.getInstance(); + @AfterEach void restore() { ImplementationFactory.setInstance(previous); } + + @Test void unsupportedPortNeedsNoDisplayAndFailsExplicitly() { + ImplementationFactory.setInstance(new ImplementationFactory() { + public Object createProcessSupport() { return null; } + }); + assertFalse(ProcessBuilder.isSupported()); + ProcessBuilder builder = new ProcessBuilder("tool"); + assertThrows(UnsupportedOperationException.class, builder::start); + assertThrows(UnsupportedOperationException.class, builder::environment); + } + + @Test void validatesMutatedCommandAndEnvironmentBeforeNativeBoundary() { + final int[] calls = {0}; + ImplementationFactory.setInstance(new ImplementationFactory() { + public Object createProcessSupport() { + return new ProcessSupport() { + public Map environment() { return new HashMap(); } + public Process start(String[] args, String dir, Map env, boolean merge) throws IOException { + calls[0]++; throw new IOException("adapter reached"); + } + }; + } + }); + ProcessBuilder builder = new ProcessBuilder("tool"); + builder.command().add("bad\0arg"); + assertThrows(IllegalArgumentException.class, builder::start); + builder.command("tool"); + builder.environment().put("KEY", "value"); + builder.environment().entrySet().iterator().next().setValue("bad\0value"); + assertThrows(IllegalArgumentException.class, builder::start); + builder.environment().clear(); + builder.environment().put("KEY=bad", "value"); + assertThrows(IllegalArgumentException.class, builder::start); + builder.environment().clear(); + builder.environment().put("KEY", null); + assertThrows(NullPointerException.class, builder::start); + assertEquals(0, calls[0]); + builder.environment().clear(); + assertThrows(IOException.class, builder::start); + assertEquals(1, calls[0]); + } +} diff --git a/scripts/process/check-process.sh b/scripts/process/check-process.sh new file mode 100755 index 00000000000..fd292f27c8f --- /dev/null +++ b/scripts/process/check-process.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Local real-child conformance. Native builds must never run in CI. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +MODE="${1:-jvm}" +case "$MODE" in jvm|native) ;; *) echo "usage: $0 [jvm|native]" >&2; exit 2 ;; esac +if [[ "$MODE" == native && -n "${CI:-}" ]]; then + echo "Native process conformance is local-only." >&2; exit 2 +fi +J8="${JDK_8_HOME:-${JAVA_HOME:-}}" +if [[ -z "$J8" && -x /usr/libexec/java_home ]]; then J8="$(/usr/libexec/java_home -v 1.8)"; fi +: "${J8:?Set JDK_8_HOME to Java 8}" +export JAVA_HOME="$J8" PATH="$J8/bin:$PATH" JDK_8_HOME="$J8" +WORK="$(mktemp -d "${TMPDIR:-/tmp}/cn1-process.XXXXXX")" +WORK="$(cd "$WORK" && pwd -P)" +trap 'rm -rf "$WORK"' EXIT +# A space in the executable path is part of the contract. +"${CC:-cc}" -Wall -Wextra -Werror "$ROOT/scripts/process/process_child.c" -o "$WORK/process child" +"${CC:-cc}" -Wall -Wextra -Werror -I "$ROOT/vm/ByteCodeTranslator/src" \ + "$ROOT/scripts/process/process_os_test.c" -o "$WORK/process-os" +"$WORK/process-os" "$WORK/process child" "$WORK" +if [[ "$MODE" == jvm ]]; then + mkdir -p "$WORK/classes" + "$J8/bin/javac" -encoding UTF-8 -d "$WORK/classes" \ + "$ROOT/CodenameOne/src/com/codename1/impl/SharedWithBackend.java" \ + "$ROOT/CodenameOne/src/com/codename1/io/Process.java" \ + "$ROOT/CodenameOne/src/com/codename1/io/ProcessBuilder.java" \ + "$ROOT/CodenameOne/src/com/codename1/impl/process/ProcessSupport.java" \ + "$ROOT/Ports/JavaSE/src/com/codename1/impl/process/JavaSEProcessSupport.java" \ + "$ROOT/vm/backend/impl/javase/com/codename1/impl/ImplementationFactory.java" \ + "$ROOT/vm/backend/demo/process/com/demo/ProcessConformance.java" + "$J8/bin/java" -cp "$WORK/classes" com.demo.ProcessConformance "$WORK/process child" "$WORK" +else + CN1_BACKEND_DEMO=demo/process CN1_BACKEND_STANDALONE_DEMO=1 \ + "$ROOT/vm/backend/build.sh" ProcessConformance com.demo "$WORK/process-native" + "$WORK/process-native" "$WORK/process child" "$WORK" + CN1_BACKEND_DEMO=demo/process-virtual CN1_BACKEND_STANDALONE_DEMO=1 \ + "$ROOT/vm/backend/build.sh" ProcessVirtualConformance com.demo "$WORK/process-virtual" + CN1_HTTP_POLL_MODE=3 "$WORK/process-virtual" "$WORK/process child" +fi diff --git a/scripts/process/process_child.c b/scripts/process/process_child.c new file mode 100644 index 00000000000..decb09c1719 --- /dev/null +++ b/scripts/process/process_child.c @@ -0,0 +1,56 @@ +/* Portable child fixture for the JavaSE/native process conformance test. */ +#include +#include +#include +#ifdef _WIN32 +#include +#include +#include +#include +#define getcwd _getcwd +#define nap() Sleep(100) +#else +#include +#define nap() usleep(100000) +#endif +static int run(int argc, char **argv) { + int i; + if (argc < 2) return 99; + if (!strcmp(argv[1], "args")) { + for (i = 2; i < argc; ++i) { fwrite(argv[i], 1, strlen(argv[i]), stdout); fputc(0, stdout); } + } else if (!strcmp(argv[1], "env")) { + const char *value = getenv("CN1_PROCESS_TEST"); + printf("%s", value ? value : ""); + } else if (!strcmp(argv[1], "cwd")) { + char path[4096]; + if (!getcwd(path, sizeof(path))) return 98; + printf("%s", path); + } else if (!strcmp(argv[1], "duplex")) { + unsigned char data[4096]; size_t n; + memset(data, 37, sizeof(data)); + for (i = 0; i < 128; ++i) { fwrite(data, 1, sizeof(data), stdout); fwrite(data, 1, sizeof(data), stderr); } + while ((n = fread(data, 1, sizeof(data), stdin)) != 0) fwrite(data, 1, n, stdout); + } else if (!strcmp(argv[1], "sleep")) { + for (;;) nap(); + } else if (!strcmp(argv[1], "exit")) { + return argc > 2 ? atoi(argv[2]) : 7; + } else return 97; + return 0; +} +#ifdef _WIN32 +int wmain(int argc, wchar_t **wide) { + int i, code; + char **argv = (char **)calloc((size_t)argc + 1, sizeof(char *)); + _setmode(_fileno(stdin), _O_BINARY); _setmode(_fileno(stdout), _O_BINARY); _setmode(_fileno(stderr), _O_BINARY); + for (i = 0; i < argc; ++i) { + int n = WideCharToMultiByte(CP_UTF8, 0, wide[i], -1, NULL, 0, NULL, NULL); + argv[i] = (char *)malloc((size_t)n); + WideCharToMultiByte(CP_UTF8, 0, wide[i], -1, argv[i], n, NULL, NULL); + } + code = run(argc, argv); + for (i = 0; i < argc; ++i) free(argv[i]); + free(argv); return code; +} +#else +int main(int argc, char **argv) { return run(argc, argv); } +#endif diff --git a/scripts/process/process_os_test.c b/scripts/process/process_os_test.c new file mode 100644 index 00000000000..ebbc8678a22 --- /dev/null +++ b/scripts/process/process_os_test.c @@ -0,0 +1,157 @@ +/* Runs unchanged on POSIX and Windows (including Wine). Tests the exact OS layer + * used by the translated adapter, without requiring a desktop window system. */ +#include "cn1_process.h" +#include +#ifdef _WIN32 +#define pause_test() Sleep(1) +#else +#define pause_test() usleep(1000) +#endif +#define CHECK(value, message) do { if (!(value)) { fprintf(stderr, "FAIL: %s at %d\n", message, __LINE__); exit(1); } } while (0) +static cn1p_child *start(char **args, const char *cwd, char **env, int merge) { + int error = 0; + cn1p_child *p = cn1p_launch(args, cwd, env, merge, &error); + if (!p) fprintf(stderr, "launch OS error %d\n", error); + CHECK(p != NULL, "launch"); return p; +} +static int collect(cn1p_child *p, int pipe, unsigned char *out, int capacity) { + int total = 0, attempts; + for (attempts = 0; attempts < 30000; ++attempts) { + int n = cn1p_transfer(p, pipe, out + total, capacity - total); + if (n == -1) return total; + CHECK(n != -2, "read"); + if (n >= 0) { total += n; CHECK(total < capacity, "buffer size"); } + else pause_test(); + } + CHECK(0, "read timeout"); return 0; +} +static void reap(cn1p_child *p, int code) { + int i; + for (i = 0; i < 30000 && !cn1p_poll(p); ++i) pause_test(); + CHECK(cn1p_poll(p), "reap timeout"); + if (code >= 0) CHECK(p->code == code, "exit status"); + cn1p_release(p); +} +static int resource_count(void) { +#ifdef _WIN32 + DWORD count = 0; + CHECK(GetProcessHandleCount(GetCurrentProcess(), &count), "handle count"); + return (int)count; +#else + int count = 0, fd; + for (fd = 0; fd < 1024; ++fd) if (fcntl(fd, F_GETFD) >= 0) ++count; + return count; +#endif +} +static int run(int argc, char **argv) { + unsigned char buffer[8192]; + char *literals[] = {NULL, "args", "", "two words", "quote\"here", "\\\\trailing\\", "a\\\"b", "שלום-🙂", "$HOME;|&<>*?", NULL}; + char *command[] = {NULL, "env", NULL, NULL}; + char *env[] = {"CN1_PROCESS_TEST=changed value", NULL}; + cn1p_child *p; + int length, i, offset = 0, error, merged; + CHECK(argc == 3, "fixture and cwd required"); + literals[0] = command[0] = argv[1]; + p = start(literals, NULL, NULL, 0); + length = collect(p, 1, buffer, sizeof(buffer)); + for (i = 2; literals[i]; ++i) { + size_t n = strlen(literals[i]) + 1; + CHECK(offset + (int)n <= length && !memcmp(buffer + offset, literals[i], n), "literal arguments"); + offset += (int)n; + } + CHECK(offset == length, "argument byte count"); reap(p, 0); + p = start(command, NULL, env, 0); + length = collect(p, 1, buffer, sizeof(buffer)); + CHECK(length == 13 && !memcmp(buffer, "changed value", 13), "environment"); reap(p, 0); + env[0] = NULL; + p = start(command, NULL, env, 0); + length = collect(p, 1, buffer, sizeof(buffer)); + CHECK(length == 8 && !memcmp(buffer, "", 8), "empty environment"); reap(p, 0); + command[1] = "cwd"; + p = start(command, argv[2], NULL, 0); + length = collect(p, 1, buffer, sizeof(buffer)); buffer[length] = 0; + CHECK(!strcmp((char *)buffer, argv[2]), "working directory"); reap(p, 0); + CHECK(!cn1p_launch(command, "cn1-no-such-directory", NULL, 0, &error) && error, "missing cwd"); + command[0] = "cn1-no-such-executable"; + CHECK(!cn1p_launch(command, NULL, NULL, 0, &error) && error, "missing executable"); + command[0] = argv[1]; + for (merged = 0; merged < 2; ++merged) { + int sent = 0, out = 0, err = 0, eofOut = 0, eofErr = 0, iterations, stdinClosed = 0; + unsigned char input[4096]; + command[1] = "duplex"; + for (i = 0; i < (int)sizeof(input); ++i) input[i] = (unsigned char)i; + p = start(command, NULL, NULL, merged); + for (iterations = 0; iterations < 30000 && !(eofOut && eofErr); ++iterations) { + int n, j; + if (sent < 262144) { + n = cn1p_transfer(p, 0, input, sizeof(input)); CHECK(n > 0 || n == -3, "stdin write"); + if (n > 0) sent += n; + } else if (!stdinClosed) { cn1p_close_pipe(p, 0); stdinClosed = 1; } + if (!eofOut) { + n = cn1p_transfer(p, 1, buffer, 4096); CHECK(n != -2, "stdout read"); + if (n == -1) eofOut = 1; + if (n > 0) for (j = 0; j < n; ++j, ++out) { + int prefix = 524288 * (merged + 1); + CHECK(buffer[j] == (out < prefix ? 37 : (unsigned char)(out - prefix)), "binary output"); + } + } + if (!eofErr) { + n = cn1p_transfer(p, 2, buffer, 4096); CHECK(n != -2, "stderr read"); + if (n == -1) eofErr = 1; + if (n > 0) { for (j = 0; j < n; ++j) CHECK(buffer[j] == 37, "stderr bytes"); err += n; } + } + pause_test(); + } + CHECK(eofOut && eofErr && out == 524288 * (merged + 1) + 262144 && err == (merged ? 0 : 524288), "duplex length/timeout"); + reap(p, 0); + } + command[1] = "sleep"; + p = start(command, NULL, NULL, 0); + CHECK(!cn1p_poll(p), "sleep alive"); + CHECK(cn1p_transfer(p, 1, buffer, 4096) == -3, "pending read"); + cn1p_close_pipe(p, 1); cn1p_close_pipe(p, 1); + cn1p_terminate(p, 1); reap(p, -1); + command[1] = "exit"; command[2] = "7"; + { int before = resource_count(); + for (i = 0; i < 40; ++i) { p = start(command, NULL, NULL, 0); reap(p, 7); } + CHECK(resource_count() == before, "descriptor/handle leak across repeated launches"); + } + p = start(command, NULL, NULL, 0); + for (i = 0; i < 30000 && !cn1p_poll(p); ++i) pause_test(); + CHECK(cn1p_poll(p), "exit before broken-pipe test"); + { int n = -3; + // Windows may accept a buffered write while it finishes closing the + // exited child's handles. Observe the eventual pipe error. + for (i = 0; i < 30000 && n != -2; ++i) { + n = cn1p_transfer(p, 0, buffer, 1); + if (n != -2) pause_test(); + } + CHECK(n == -2, "closed stdin must fail without SIGPIPE"); + } +#ifndef _WIN32 + { int status; + CHECK(waitpid(p->pid, &status, WNOHANG) == -1 && errno == ECHILD, "child was not reaped"); + } +#endif + cn1p_release(p); + puts("PROCESS_OS_CONFORMANCE_OK"); return 0; +} + +#ifdef _WIN32 +int wmain(int argc, wchar_t **wide) { + int i, code; + char **argv = (char **)calloc((size_t)argc + 1, sizeof(char *)); + CHECK(argv != NULL, "argument allocation"); + for (i = 0; i < argc; ++i) { + int n = WideCharToMultiByte(CP_UTF8, 0, wide[i], -1, NULL, 0, NULL, NULL); + argv[i] = (char *)malloc((size_t)n); + CHECK(argv[i] != NULL, "argument allocation"); + WideCharToMultiByte(CP_UTF8, 0, wide[i], -1, argv[i], n, NULL, NULL); + } + code = run(argc, argv); + for (i = 0; i < argc; ++i) free(argv[i]); + free(argv); return code; +} +#else +int main(int argc, char **argv) { return run(argc, argv); } +#endif diff --git a/vm/ByteCodeTranslator/src/cn1_process.h b/vm/ByteCodeTranslator/src/cn1_process.h new file mode 100644 index 00000000000..f2894130b02 --- /dev/null +++ b/vm/ByteCodeTranslator/src/cn1_process.h @@ -0,0 +1,382 @@ +/* Subprocess OS layer shared by ParparVM desktop and backend targets. + * No VM pointers cross an OS call. All pipe transfers and exit checks are polls. + * SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0 */ +#ifndef CN1_PROCESS_H +#define CN1_PROCESS_H +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#include + +typedef struct { + HANDLE handle; + OVERLAPPED op; + unsigned char *buffer; + DWORD size; + int pending; +} cn1p_pipe; +typedef struct { + HANDLE process; + cn1p_pipe pipes[3]; + int done; + int code; +} cn1p_child; + +static wchar_t *cn1p_wide(const char *s) { + int n = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, s, -1, NULL, 0); + wchar_t *out = n ? (wchar_t *)malloc((size_t)n * sizeof(wchar_t)) : NULL; + if (out) MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, s, -1, out, n); + return out; +} +/* Quote every argument using the Windows CRT parsing rules, including empty + * arguments, embedded quotes and trailing backslashes. No cmd.exe is involved. */ +static wchar_t *cn1p_command(char **args) { + size_t size = 1; + int i; + for (i = 0; args[i]; ++i) size += 2 * strlen(args[i]) + 3; + wchar_t *result = (wchar_t *)calloc(size, sizeof(wchar_t)), *out = result; + if (!out) return NULL; + for (i = 0; args[i]; ++i) { + wchar_t *arg = cn1p_wide(args[i]), *p; + if (!arg) { free(result); return NULL; } + if (i) *out++ = L' '; + *out++ = L'"'; + p = arg; + while (*p) { + size_t slashes = 0; + while (*p == L'\\') { ++slashes; ++p; } + if (*p == L'"' || !*p) slashes *= 2; + while (slashes--) *out++ = L'\\'; + if (*p == L'"') *out++ = L'\\'; + if (*p) *out++ = *p++; + } + *out++ = L'"'; + free(arg); + } + *out = 0; + return result; +} +static int cn1p_envcmp(const void *a, const void *b) { + return _wcsicmp(*(wchar_t *const *)a, *(wchar_t *const *)b); +} +static wchar_t *cn1p_environment(char **env) { + int count = 0, i; + size_t size = 2; + while (env[count]) ++count; + wchar_t **entries = (wchar_t **)calloc((size_t)count + 1, sizeof(wchar_t *)); + if (!entries) return NULL; + for (i = 0; i < count; ++i) { + entries[i] = cn1p_wide(env[i]); + if (!entries[i]) { while (i--) free(entries[i]); free(entries); return NULL; } + size += wcslen(entries[i]) + 1; + } + qsort(entries, (size_t)count, sizeof(wchar_t *), cn1p_envcmp); + wchar_t *block = (wchar_t *)calloc(size, sizeof(wchar_t)), *p = block; + for (i = 0; i < count; ++i) { + if (p) { size_t n = wcslen(entries[i]) + 1; memcpy(p, entries[i], n * sizeof(wchar_t)); p += n; } + free(entries[i]); + } + free(entries); + return block; +} +static void cn1p_close_pipe(cn1p_child *child, int index) { + cn1p_pipe *p = &child->pipes[index]; + if (p->handle && p->handle != INVALID_HANDLE_VALUE) { + if (p->pending) { + DWORD ignored; + CancelIoEx(p->handle, &p->op); + GetOverlappedResult(p->handle, &p->op, &ignored, TRUE); + } + CloseHandle(p->handle); + } + if (p->op.hEvent) CloseHandle(p->op.hEvent); + free(p->buffer); + memset(p, 0, sizeof(*p)); +} +static void cn1p_release(cn1p_child *child) { + int i; + if (!child) return; + for (i = 0; i < 3; ++i) cn1p_close_pipe(child, i); + if (child->process) CloseHandle(child->process); + free(child); +} +static int cn1p_make_pipe(cn1p_pipe *parent, HANDLE *child, int input) { + static volatile LONG serial; + wchar_t name[100]; + SECURITY_ATTRIBUTES security = {sizeof(security), NULL, TRUE}; + swprintf(name, 100, L"\\\\.\\pipe\\cn1-process-%lu-%lu", (unsigned long)GetCurrentProcessId(), + (unsigned long)InterlockedIncrement(&serial)); + parent->handle = CreateNamedPipeW(name, (input ? PIPE_ACCESS_OUTBOUND : PIPE_ACCESS_INBOUND) + | FILE_FLAG_OVERLAPPED | FILE_FLAG_FIRST_PIPE_INSTANCE, + PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS, + 1, 65536, 65536, 0, NULL); + if (parent->handle == INVALID_HANDLE_VALUE) return 0; + *child = CreateFileW(name, input ? GENERIC_READ : GENERIC_WRITE, 0, &security, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (*child == INVALID_HANDLE_VALUE) return 0; + parent->op.hEvent = CreateEventW(NULL, TRUE, FALSE, NULL); + return parent->op.hEvent != NULL; +} +static cn1p_child *cn1p_launch(char **args, const char *directory, char **env, int merge, int *error) { + cn1p_child *child = (cn1p_child *)calloc(1, sizeof(*child)); + HANDLE inherited[3] = {NULL, NULL, NULL}; + STARTUPINFOEXW startup; + PROCESS_INFORMATION info; + SIZE_T attrSize = 0; + wchar_t *command = cn1p_command(args), *cwd = directory ? cn1p_wide(directory) : NULL; + wchar_t *environment = env ? cn1p_environment(env) : NULL; + int ok = 0, i, attrReady = 0; + memset(&startup, 0, sizeof(startup)); memset(&info, 0, sizeof(info)); + startup.StartupInfo.cb = sizeof(startup); + *error = ERROR_NOT_ENOUGH_MEMORY; + if (!child || !command || (directory && !cwd) || (env && !environment)) goto cleanup; + for (i = 0; i < (merge ? 2 : 3); ++i) { + if (!cn1p_make_pipe(&child->pipes[i], &inherited[i], i == 0)) { *error = (int)GetLastError(); goto cleanup; } + } + startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES; + startup.StartupInfo.hStdInput = inherited[0]; + startup.StartupInfo.hStdOutput = inherited[1]; + startup.StartupInfo.hStdError = merge ? inherited[1] : inherited[2]; + InitializeProcThreadAttributeList(NULL, 1, 0, &attrSize); + startup.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)malloc(attrSize); + if (!startup.lpAttributeList) goto cleanup; + if (!InitializeProcThreadAttributeList(startup.lpAttributeList, 1, 0, &attrSize)) { *error = (int)GetLastError(); goto cleanup; } + attrReady = 1; + if (!UpdateProcThreadAttribute(startup.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, + inherited, (merge ? 2 : 3) * sizeof(HANDLE), NULL, NULL)) { *error = (int)GetLastError(); goto cleanup; } + ok = CreateProcessW(NULL, command, NULL, NULL, TRUE, + EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT | CREATE_NO_WINDOW, + environment, cwd, &startup.StartupInfo, &info); + if (!ok) { *error = (int)GetLastError(); goto cleanup; } + child->process = info.hProcess; + CloseHandle(info.hThread); +cleanup: + if (attrReady) DeleteProcThreadAttributeList(startup.lpAttributeList); + free(startup.lpAttributeList); + for (i = 0; i < 3; ++i) if (inherited[i] && inherited[i] != INVALID_HANDLE_VALUE) CloseHandle(inherited[i]); + free(command); free(cwd); free(environment); + if (!ok) { cn1p_release(child); return NULL; } + return child; +} +static int cn1p_poll(cn1p_child *child) { + DWORD code; + if (!child->done && WaitForSingleObject(child->process, 0) == WAIT_OBJECT_0) { + if (GetExitCodeProcess(child->process, &code)) { child->code = (int)code; child->done = 1; } + } + return child->done; +} +static void cn1p_terminate(cn1p_child *child, int force) { + (void)force; + if (!cn1p_poll(child)) TerminateProcess(child->process, 1); +} +static int cn1p_transfer(cn1p_child *child, int index, unsigned char *bytes, int length) { + cn1p_pipe *p = &child->pipes[index]; + DWORD done = 0, error; + BOOL ok; + if (!p->handle) return index ? -1 : -2; + if (p->pending) { + ok = GetOverlappedResult(p->handle, &p->op, &done, FALSE); + error = ok ? ERROR_SUCCESS : GetLastError(); + if (error == ERROR_IO_INCOMPLETE) return -3; + p->pending = 0; + } else { + DWORD size = (DWORD)(length > 65536 ? 65536 : length); + if (size > p->size) { + unsigned char *buffer = (unsigned char *)realloc(p->buffer, size); + if (!buffer) return -2; + p->buffer = buffer; p->size = size; + } + if (!index) memcpy(p->buffer, bytes, size); + ResetEvent(p->op.hEvent); + ok = index ? ReadFile(p->handle, p->buffer, size, &done, &p->op) + : WriteFile(p->handle, p->buffer, size, &done, &p->op); + error = ok ? ERROR_SUCCESS : GetLastError(); + if (error == ERROR_IO_PENDING) { p->pending = 1; return -3; } + } + if (error != ERROR_SUCCESS) return index && error == ERROR_BROKEN_PIPE ? -1 : -2; + if (index && done) memcpy(bytes, p->buffer, done); + return done ? (int)done : (index ? -1 : -2); +} +#else +#include +#include +#include +#include +#include +#include +#if defined(__APPLE__) +#include +#define CN1P_ENVIRON (*_NSGetEnviron()) +#else +extern char **environ; +#define CN1P_ENVIRON environ +#endif + +typedef struct { pid_t pid; int pipes[3]; int done; int code; } cn1p_child; +static void cn1p_close_pipe(cn1p_child *child, int index) { + if (child->pipes[index] >= 0) { close(child->pipes[index]); child->pipes[index] = -1; } +} +static void cn1p_release(cn1p_child *child) { + int i; + if (!child) return; + for (i = 0; i < 3; ++i) cn1p_close_pipe(child, i); + free(child); +} +static int cn1p_pipe(int *fds) { + int i; + if (pipe(fds)) return -1; + for (i = 0; i < 2; ++i) { + if (fds[i] < 3) { + int fd = fcntl(fds[i], F_DUPFD, 3); + if (fd < 0) goto fail; + close(fds[i]); fds[i] = fd; + } + if (fcntl(fds[i], F_SETFD, FD_CLOEXEC) < 0) goto fail; + } + return 0; +fail: + { int saved = errno; close(fds[0]); close(fds[1]); fds[0] = fds[1] = -1; errno = saved; } + return -1; +} +/* Prepare PATH candidates before fork. The child does only async-signal-safe + * syscalls, never allocation, Java, stdio, or setenv in a multithreaded VM. */ +static char **cn1p_paths(const char *command, char **env) { + const char *path = "/usr/bin:/bin", *start, *end; + size_t count = 1, i = 0; + char **paths; + if (strchr(command, '/')) { + paths = (char **)calloc(2, sizeof(char *)); + if (paths) { paths[0] = strdup(command); if (!paths[0]) { free(paths); return NULL; } } + return paths; + } + for (i = 0; env[i]; ++i) if (!strncmp(env[i], "PATH=", 5)) { path = env[i] + 5; break; } + for (start = path; *start; ++start) if (*start == ':') ++count; + paths = (char **)calloc(count + 1, sizeof(char *)); + if (!paths) return NULL; + i = 0; start = path; + do { + end = strchr(start, ':'); + size_t len = end ? (size_t)(end - start) : strlen(start); + paths[i] = (char *)malloc(len + strlen(command) + 2); + if (!paths[i]) { while (i) free(paths[--i]); free(paths); return NULL; } + if (len) { memcpy(paths[i], start, len); paths[i][len] = '/'; strcpy(paths[i] + len + 1, command); } + else strcpy(paths[i], command); + ++i; start = end ? end + 1 : NULL; + } while (start); + return paths; +} +static cn1p_child *cn1p_launch(char **args, const char *directory, char **environment, int merge, int *error) { + int pipes[4][2] = {{-1,-1},{-1,-1},{-1,-1},{-1,-1}}; + int i, j, err = 0, status; + long maxfd = sysconf(_SC_OPEN_MAX); + char **env = environment ? environment : CN1P_ENVIRON; + char **paths = cn1p_paths(args[0], env); + cn1p_child *child = (cn1p_child *)calloc(1, sizeof(*child)); + pid_t pid = -1; + *error = ENOMEM; + if (maxfd < 0 || maxfd > INT_MAX) maxfd = INT_MAX; + if (child) for (i = 0; i < 3; ++i) child->pipes[i] = -1; + if (!paths || !child) goto cleanup; + for (i = 0; i < 4; ++i) { + if (i == 2 && merge) continue; + if (cn1p_pipe(pipes[i])) { *error = errno; goto cleanup; } + } + pid = fork(); + if (pid < 0) { *error = errno; goto cleanup; } + if (pid == 0) { + int failure = 0; + if (directory && chdir(directory)) failure = errno; + if (!failure && (dup2(pipes[0][0], 0) < 0 || dup2(pipes[1][1], 1) < 0 + || dup2(pipes[merge ? 1 : 2][1], 2) < 0)) failure = errno; + for (i = 3; i < maxfd; ++i) if (i != pipes[3][1]) close(i); + // The VM may ignore SIGPIPE. Give the child normal shell-independent + // process signal semantics and an unblocked signal mask. + struct sigaction action; + sigset_t mask; + memset(&action, 0, sizeof(action)); action.sa_handler = SIG_DFL; + sigemptyset(&action.sa_mask); sigaction(SIGPIPE, &action, NULL); + sigemptyset(&mask); sigprocmask(SIG_SETMASK, &mask, NULL); + if (!failure) { + int denied = 0; + for (i = 0; paths[i]; ++i) { + execve(paths[i], args, env); + failure = errno; + if (failure == EACCES) denied = 1; + else if (failure != ENOENT && failure != ENOTDIR) break; + } + if (!paths[i] && denied) failure = EACCES; + } + while (write(pipes[3][1], &failure, sizeof(failure)) < 0 && errno == EINTR) { } + _exit(127); + } + close(pipes[3][1]); pipes[3][1] = -1; + { ssize_t n; + do { n = read(pipes[3][0], &err, sizeof(err)); } while (n < 0 && errno == EINTR); + if (n != 0) { + *error = n < 0 ? errno : err; + kill(pid, SIGKILL); + while (waitpid(pid, &status, 0) < 0 && errno == EINTR) { } + pid = -1; goto cleanup; + } + } + child->pid = pid; + for (i = 0; i < 3; ++i) { + if (i == 2 && merge) continue; + j = i == 0 ? 1 : 0; + child->pipes[i] = pipes[i][j]; pipes[i][j] = -1; + if (fcntl(child->pipes[i], F_SETFL, O_NONBLOCK) < 0) { + *error = errno; kill(pid, SIGKILL); + while (waitpid(pid, &status, 0) < 0 && errno == EINTR) { } + pid = -1; goto cleanup; + } + } +cleanup: + for (i = 0; i < 4; ++i) for (j = 0; j < 2; ++j) if (pipes[i][j] >= 0) close(pipes[i][j]); + if (paths) { for (i = 0; paths[i]; ++i) free(paths[i]); free(paths); } + if (pid < 0) { cn1p_release(child); return NULL; } + return child; +} +/* Called under the Java child's lock, as is terminate: a reaped PID can never + * be reused between the running check and kill. */ +static int cn1p_poll(cn1p_child *child) { + int status; + pid_t result; + if (child->done) return 1; + do { result = waitpid(child->pid, &status, WNOHANG); } while (result < 0 && errno == EINTR); + if (result == child->pid) { + child->code = WIFEXITED(status) ? WEXITSTATUS(status) : 128 + WTERMSIG(status); + child->done = 1; + } else if (result < 0 && errno == ECHILD) { child->done = 1; child->code = -1; } + return child->done; +} +static void cn1p_terminate(cn1p_child *child, int force) { + if (!cn1p_poll(child)) kill(child->pid, force ? SIGKILL : SIGTERM); +} +static int cn1p_transfer(cn1p_child *child, int index, unsigned char *bytes, int length) { + ssize_t n; + if (child->pipes[index] < 0) return index ? -1 : -2; + if (index) n = read(child->pipes[index], bytes, (size_t)length); + else { + // Suppress only this write's SIGPIPE, without changing the application's + // process-wide signal disposition or consuming a previously pending one. + sigset_t block, old, pending; + int wasPending, saved; + sigemptyset(&block); sigaddset(&block, SIGPIPE); + if (pthread_sigmask(SIG_BLOCK, &block, &old)) return -2; + sigpending(&pending); wasPending = sigismember(&pending, SIGPIPE); + n = write(child->pipes[index], bytes, (size_t)length); saved = errno; + if (n < 0 && saved == EPIPE && !wasPending) { + sigpending(&pending); + if (sigismember(&pending, SIGPIPE)) { int sig; sigwait(&block, &sig); } + } + pthread_sigmask(SIG_SETMASK, &old, NULL); errno = saved; + } + if (n < 0) return errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR ? -3 : -2; + return n ? (int)n : (index ? -1 : -2); +} +#endif +#endif diff --git a/vm/ByteCodeTranslator/src/cn1_process_vm.h b/vm/ByteCodeTranslator/src/cn1_process_vm.h new file mode 100644 index 00000000000..34f38cbafd5 --- /dev/null +++ b/vm/ByteCodeTranslator/src/cn1_process_vm.h @@ -0,0 +1,141 @@ +/* ParparVM bindings for the shared Codename One process adapter. + * SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0 */ +#ifndef CN1_PROCESS_VM_H +#define CN1_PROCESS_VM_H +#if __has_include("com_codename1_impl_process_NativeProcessSupport.h") +#include "com_codename1_impl_process_NativeProcessSupport.h" +#include "java_lang_String.h" +#if defined(__APPLE__) +#include +#endif +#if !defined(__APPLE__) || !TARGET_OS_IPHONE +#include "cn1_process.h" + +JAVA_VOID com_codename1_impl_process_NativeProcessSupport_pauseImpl___long(CODENAME_ONE_THREAD_STATE, JAVA_LONG millis) { +#if defined(CN1_VIRTUAL_THREADS) && CN1_VIRTUAL_THREADS + /* The backend owns the timer/poller registration. No Java monitor is held + here, so it can park this task and run another on the same host. */ + extern int cn1BackendVtWait(int, const int *, const int *, long long); + CN1_YIELD_THREAD; + int parked = cn1BackendVtWait(0, NULL, NULL, millis); + CN1_RESUME_THREAD; + if (parked) return; +#endif + CN1_YIELD_THREAD; +#ifdef _WIN32 + Sleep((DWORD)millis); +#else + usleep((unsigned int)millis * 1000); +#endif + CN1_RESUME_THREAD; +} + +static void cn1p_free_strings(char **strings) { + int i; + if (!strings) return; + for (i = 0; strings[i]; ++i) free(strings[i]); + free(strings); +} +static char **cn1p_strings(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT array) { + int i, size = ((JAVA_ARRAY)array)->length; + char **out = (char **)calloc((size_t)size + 1, sizeof(char *)); + if (!out) return NULL; + for (i = 0; i < size; ++i) { + JAVA_OBJECT value = ((JAVA_ARRAY_OBJECT *)CN1_ARRAY_DATA((JAVA_ARRAY)array))[i]; + out[i] = cn1_strdup(stringToUTF8(threadStateData, value)); + if (!out[i]) { cn1p_free_strings(out); return NULL; } + } + return out; +} +JAVA_OBJECT com_codename1_impl_process_NativeProcessSupport_environmentImpl___R_java_lang_String_1ARRAY(CODENAME_ONE_THREAD_STATE) { + int count = 0, i; + JAVA_OBJECT result; +#ifdef _WIN32 + wchar_t *block = GetEnvironmentStringsW(), *p; + if (!block) return JAVA_NULL; + for (p = block; *p; p += wcslen(p) + 1) ++count; + result = allocArray(threadStateData, count, &class_array1__java_lang_String, sizeof(JAVA_OBJECT), 1); + for (p = block, i = 0; *p; p += wcslen(p) + 1, ++i) { + int size = WideCharToMultiByte(CP_UTF8, 0, p, -1, NULL, 0, NULL, NULL); + char *utf8 = (char *)malloc((size_t)size); + if (!utf8) { FreeEnvironmentStringsW(block); return JAVA_NULL; } + WideCharToMultiByte(CP_UTF8, 0, p, -1, utf8, size, NULL, NULL); + JAVA_OBJECT entry = newStringFromNative(threadStateData, utf8); + free(utf8); CN1_SET_ARRAY_ELEMENT_OBJECT(result, i, entry); + } + FreeEnvironmentStringsW(block); +#else + char **env = CN1P_ENVIRON; + while (env[count]) ++count; + result = allocArray(threadStateData, count, &class_array1__java_lang_String, sizeof(JAVA_OBJECT), 1); + for (i = 0; i < count; ++i) { + JAVA_OBJECT entry = newStringFromNative(threadStateData, env[i]); + CN1_SET_ARRAY_ELEMENT_OBJECT(result, i, entry); + } +#endif + return result; +} +JAVA_LONG com_codename1_impl_process_NativeProcessSupport_launch___java_lang_String_1ARRAY_java_lang_String_java_lang_String_1ARRAY_boolean_R_long(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT command, JAVA_OBJECT directory, JAVA_OBJECT environment, JAVA_BOOLEAN merge) { + char **args = cn1p_strings(threadStateData, command); + char **env = environment == JAVA_NULL ? NULL : cn1p_strings(threadStateData, environment); + char *cwd = directory == JAVA_NULL ? NULL : cn1_strdup(stringToUTF8(threadStateData, directory)); + cn1p_child *child = NULL; + int error = ENOMEM; + if (args && (environment == JAVA_NULL || env) && (directory == JAVA_NULL || cwd)) { + CN1_YIELD_THREAD; + child = cn1p_launch(args, cwd, env, merge, &error); + CN1_RESUME_THREAD; + } + cn1p_free_strings(args); cn1p_free_strings(env); free(cwd); + return child ? (JAVA_LONG)(intptr_t)child : -(JAVA_LONG)error; +} +JAVA_LONG com_codename1_impl_process_NativeProcessSupport_poll___long_R_long(CODENAME_ONE_THREAD_STATE, JAVA_LONG handle) { + cn1p_child *child = (cn1p_child *)(intptr_t)handle; + return cn1p_poll(child) ? (JAVA_LONG)child->code : INT64_MIN; +} +JAVA_VOID com_codename1_impl_process_NativeProcessSupport_terminate___long_boolean(CODENAME_ONE_THREAD_STATE, JAVA_LONG handle, JAVA_BOOLEAN force) { + cn1p_terminate((cn1p_child *)(intptr_t)handle, force); +} +JAVA_INT com_codename1_impl_process_NativeProcessSupport_transferImpl___long_int_byte_1ARRAY_int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG handle, JAVA_INT index, JAVA_OBJECT bytes, JAVA_INT offset, JAVA_INT length) { + return cn1p_transfer((cn1p_child *)(intptr_t)handle, index, + (unsigned char *)CN1_ARRAY_DATA((JAVA_ARRAY)bytes) + offset, length); +} +JAVA_VOID com_codename1_impl_process_NativeProcessSupport_closePipeImpl___long_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG handle, JAVA_INT index) { + cn1p_close_pipe((cn1p_child *)(intptr_t)handle, index); +} +JAVA_VOID com_codename1_impl_process_NativeProcessSupport_release___long(CODENAME_ONE_THREAD_STATE, JAVA_LONG handle) { + cn1p_release((cn1p_child *)(intptr_t)handle); +} +JAVA_VOID com_codename1_impl_process_NativeProcessSupport_abandon___long(CODENAME_ONE_THREAD_STATE, JAVA_LONG handle) { + cn1p_child *child = (cn1p_child *)(intptr_t)handle; + CN1_YIELD_THREAD; + cn1p_terminate(child, 1); + while (!cn1p_poll(child)) { +#ifdef _WIN32 + Sleep(1); +#else + usleep(1000); +#endif + } + cn1p_release(child); + CN1_RESUME_THREAD; +} +#else /* iOS: keep the shared class linkable without referencing forbidden APIs. */ +JAVA_OBJECT com_codename1_impl_process_NativeProcessSupport_environmentImpl___R_java_lang_String_1ARRAY(CODENAME_ONE_THREAD_STATE) { + return JAVA_NULL; +} +JAVA_LONG com_codename1_impl_process_NativeProcessSupport_launch___java_lang_String_1ARRAY_java_lang_String_java_lang_String_1ARRAY_boolean_R_long(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT command, JAVA_OBJECT directory, JAVA_OBJECT environment, JAVA_BOOLEAN merge) { + (void)command; (void)directory; (void)environment; (void)merge; return -ENOSYS; +} +JAVA_VOID com_codename1_impl_process_NativeProcessSupport_pauseImpl___long(CODENAME_ONE_THREAD_STATE, JAVA_LONG millis) { (void)millis; } +JAVA_LONG com_codename1_impl_process_NativeProcessSupport_poll___long_R_long(CODENAME_ONE_THREAD_STATE, JAVA_LONG handle) { (void)handle; return 0; } +JAVA_VOID com_codename1_impl_process_NativeProcessSupport_terminate___long_boolean(CODENAME_ONE_THREAD_STATE, JAVA_LONG handle, JAVA_BOOLEAN force) { (void)handle; (void)force; } +JAVA_INT com_codename1_impl_process_NativeProcessSupport_transferImpl___long_int_byte_1ARRAY_int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG handle, JAVA_INT index, JAVA_OBJECT bytes, JAVA_INT offset, JAVA_INT length) { + (void)handle; (void)index; (void)bytes; (void)offset; (void)length; return -2; +} +JAVA_VOID com_codename1_impl_process_NativeProcessSupport_closePipeImpl___long_int(CODENAME_ONE_THREAD_STATE, JAVA_LONG handle, JAVA_INT index) { (void)handle; (void)index; } +JAVA_VOID com_codename1_impl_process_NativeProcessSupport_release___long(CODENAME_ONE_THREAD_STATE, JAVA_LONG handle) { (void)handle; } +JAVA_VOID com_codename1_impl_process_NativeProcessSupport_abandon___long(CODENAME_ONE_THREAD_STATE, JAVA_LONG handle) { (void)handle; } +#endif +#endif +#endif diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java index 746150c4687..a7586d65188 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java @@ -521,6 +521,8 @@ private static void handleCleanOutput(ByteCodeTranslator b, File[] sources, File File cn1Globals = copyRuntimeResource(srcRoot, "cn1_globals.h"); copyRuntimeResource(srcRoot, "cn1_intrinsics.h"); copyRuntimeResource(srcRoot, "cn1_collections.h"); + copyRuntimeResource(srcRoot, "cn1_process.h"); + copyRuntimeResource(srcRoot, "cn1_process_vm.h"); // Virtual threads: the switch is a few instructions of assembly per // architecture, so the .S travels with the runtime rather than being // generated. A project that gets the C and not the .S links against a @@ -903,6 +905,8 @@ private static void handleAppleOutput(ByteCodeTranslator b, File[] sources, File File cn1Globals = copyRuntimeResource(srcRoot, "cn1_globals.h"); copyRuntimeResource(srcRoot, "cn1_intrinsics.h"); copyRuntimeResource(srcRoot, "cn1_collections.h"); + copyRuntimeResource(srcRoot, "cn1_process.h"); + copyRuntimeResource(srcRoot, "cn1_process_vm.h"); // Virtual threads: the switch is a few instructions of assembly per // architecture, so the .S travels with the runtime rather than being // generated. A project that gets the C and not the .S links against a diff --git a/vm/ByteCodeTranslator/src/nativeMethods.m b/vm/ByteCodeTranslator/src/nativeMethods.m index 5f9a97bcf9a..c114d5b5f9e 100644 --- a/vm/ByteCodeTranslator/src/nativeMethods.m +++ b/vm/ByteCodeTranslator/src/nativeMethods.m @@ -5895,3 +5895,5 @@ JAVA_OBJECT java_lang_String_toLowerCase___R_java_lang_String(CODENAME_ONE_THREA return cn1StringConvertCase(threadStateData, __cn1ThisObject, 0); #endif } + +#include "cn1_process_vm.h" diff --git a/vm/backend/build.sh b/vm/backend/build.sh index dbcefd06341..b9343205ac7 100755 --- a/vm/backend/build.sh +++ b/vm/backend/build.sh @@ -76,6 +76,7 @@ fi # cn1_sqlite3.c is synced too: it carries our build options for the bundled # engine, and a stale copy in target/classes silently builds the old ones. for f in cn1_globals.h cn1_globals.m nativeMethods.m cn1_intrinsics.h cn1_sqlite3.c \ + cn1_process.h cn1_process_vm.h \ cn1_virtual_thread.h cn1_virtual_thread.c cn1_virtual_thread_asm.S; do cp "$REPO/vm/ByteCodeTranslator/src/$f" "$TRANSLATOR/$f" done diff --git a/vm/backend/demo/process-virtual/com/demo/ProcessVirtualConformance.java b/vm/backend/demo/process-virtual/com/demo/ProcessVirtualConformance.java new file mode 100644 index 00000000000..37780e4afe3 --- /dev/null +++ b/vm/backend/demo/process-virtual/com/demo/ProcessVirtualConformance.java @@ -0,0 +1,62 @@ +package com.demo; + +import com.codename1.backend.Backend; +import com.codename1.backend.Config; +import com.codename1.backend.HttpServer; +import com.codename1.backend.Tasks; +import com.codename1.backend.VirtualThread; +import com.codename1.io.Process; +import com.codename1.io.ProcessBuilder; +import java.util.Properties; + +/** A blocked process pipe/wait must let another task run on the sole host. */ +public final class ProcessVirtualConformance { + private static volatile boolean entered; + private static volatile boolean marker; + private static volatile boolean finished; + private static volatile boolean virtual; + public static void main(String[] args) throws Exception { + Backend backend = Backend.builder(Config.of(new Properties(), "test")) + .quiet().host("127.0.0.1").port(0).workers(1) + .handler(new HttpServer.Handler() { + public HttpServer.Response handle(HttpServer.Request request) { + return HttpServer.Response.text(200, "ok"); + } + }).start(); + try { + for (int mode = 0; mode < 2; ++mode) { + entered = marker = finished = virtual = false; + System.out.println("process virtual mode=" + mode); + final boolean pipe = mode == 0; + final Process child = new ProcessBuilder(args[0], "sleep").start(); + try { + Tasks.virtual(new Runnable() { + public void run() { + virtual = VirtualThread.isVirtual(); + entered = true; + try { + if (pipe) child.getInputStream().read(); + else child.waitFor(); + } catch (Exception expectedOnClose) { System.out.println("process task closed: " + expectedOnClose); } + finally { finished = true; System.out.println("process task finished"); } + } + }); + long deadline = System.currentTimeMillis() + 5000; + while (!entered && System.currentTimeMillis() < deadline) Thread.sleep(1); + if (!entered || !virtual) throw new AssertionError("not running on a virtual thread"); + Tasks.virtual(new Runnable() { public void run() { marker = true; } }); + deadline = System.currentTimeMillis() + 2000; + while (!marker && System.currentTimeMillis() < deadline) Thread.sleep(1); + System.out.println("process marker=" + marker); + if (!marker) throw new AssertionError("process operation pinned the sole host"); + } finally { + child.close(); + long deadline = System.currentTimeMillis() + 5000; + while (!finished && System.currentTimeMillis() < deadline) Thread.sleep(1); + } + if (!finished) throw new AssertionError("task did not finish after close"); + } + } finally { backend.stop(); } + System.out.println("PROCESS_VIRTUAL_CONFORMANCE_OK"); + } +} diff --git a/vm/backend/demo/process/com/demo/ProcessConformance.java b/vm/backend/demo/process/com/demo/ProcessConformance.java new file mode 100644 index 00000000000..64766bbbc84 --- /dev/null +++ b/vm/backend/demo/process/com/demo/ProcessConformance.java @@ -0,0 +1,110 @@ +package com.demo; + +import com.codename1.io.Process; +import com.codename1.io.ProcessBuilder; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.TimeUnit; + +/** Same real-child contract on the JVM and translated desktop/backend runtime. */ +public final class ProcessConformance { + private static String fixture; + private static void check(boolean ok, String message) { + if (!ok) throw new AssertionError(message); + } + private static byte[] read(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] bytes = new byte[4096]; int n; + while ((n = in.read(bytes)) != -1) out.write(bytes, 0, n); + return out.toByteArray(); + } + private static String output(ProcessBuilder builder) throws Exception { + Process p = builder.start(); + try { + p.getOutputStream().close(); + String text = new String(read(p.getInputStream()), "UTF-8"); + check(p.waitFor(10000), "exit timeout"); + check(p.exitValue() == 0, "exit status"); + return text; + } finally { p.close(); } + } + private static final class Drain extends Thread { + final InputStream in; + byte[] bytes; + Throwable error; + Drain(InputStream in) { this.in = in; } + public void run() { try { bytes = read(in); } catch (Throwable ex) { error = ex; } } + void finish() throws Exception { join(15000); check(!isAlive() && error == null, "drain failed: " + error); } + } + public static void main(String[] args) throws Exception { + fixture = args[0]; + check(ProcessBuilder.isSupported(), "support"); + String[] literals = {"", "two words", "quote\"here", "\\\\trailing\\", "a\\\"b", "שלום-🙂", "$HOME;|&<>*?"}; + String[] command = new String[literals.length + 2]; command[0] = fixture; command[1] = "args"; + String expected = ""; + for (int i = 0; i < literals.length; ++i) { command[i + 2] = literals[i]; expected += literals[i] + '\0'; } + check(expected.equals(output(new ProcessBuilder(command))), "literal/Unicode arguments"); + ProcessBuilder env = new ProcessBuilder(fixture, "env"); + env.environment().put("CN1_PROCESS_TEST", "value with spaces"); + check("value with spaces".equals(output(env)), "environment override"); + env.environment().remove("CN1_PROCESS_TEST"); + check("".equals(output(env)), "environment removal"); + env.environment().clear(); + check("".equals(output(env)), "empty environment"); + check(output(new ProcessBuilder(fixture, "cwd").directory(args[1])).equals(args[1]), "working directory"); + try { new ProcessBuilder(fixture + "-missing").start(); throw new AssertionError("missing executable accepted"); } + catch (IOException expectedFailure) { } + try { new ProcessBuilder(fixture, "bad\0argument").start(); throw new AssertionError("NUL accepted"); } + catch (IllegalArgumentException expectedFailure) { } + try { new ProcessBuilder(fixture, "exit").directory(args[1] + "/missing-dir").start(); throw new AssertionError("bad cwd accepted"); } + catch (IOException expectedFailure) { } + for (int merged = 0; merged < 2; ++merged) { + Process p = new ProcessBuilder(fixture, "duplex").redirectErrorStream(merged == 1).start(); + try { + Drain out = new Drain(p.getInputStream()), err = new Drain(p.getErrorStream()); + out.start(); err.start(); + byte[] payload = new byte[262144]; + for (int i = 0; i < payload.length; ++i) payload[i] = (byte)i; + p.getOutputStream().write(payload); p.getOutputStream().close(); + check(p.waitFor(15, TimeUnit.SECONDS), "duplex timeout"); + out.finish(); err.finish(); + int prefix = 524288 * (merged + 1); + check(out.bytes.length == prefix + payload.length, "stdout length"); + check(err.bytes.length == (merged == 1 ? 0 : 524288), "stderr length"); + for (int i = 0; i < prefix; ++i) check(out.bytes[i] == 37, "output corruption"); + for (int i = 0; i < payload.length; ++i) check(out.bytes[prefix + i] == payload[i], "binary roundtrip"); + } finally { p.close(); } + } + Process sleeping = new ProcessBuilder(fixture, "sleep").start(); + try { + check(!sleeping.waitFor(30), "timed wait"); + check(sleeping.isAlive(), "timeout killed child"); + Thread.currentThread().interrupt(); + try { sleeping.waitFor(); throw new AssertionError("interrupt ignored"); } + catch (InterruptedException expectedInterrupt) { } + check(sleeping.isAlive(), "interrupt killed child"); + final Process interruptTarget = sleeping; + final boolean[] interrupted = {false}; + Thread waiter = new Thread(new Runnable() { + public void run() { + try { interruptTarget.waitFor(10000); } + catch (InterruptedException expectedInterrupt) { interrupted[0] = true; } + } + }); + waiter.start(); Thread.sleep(30); waiter.interrupt(); waiter.join(1000); + check(!waiter.isAlive() && interrupted[0], "interrupt during timed wait"); + check(sleeping.isAlive(), "delayed interrupt killed child"); + sleeping.destroy(); check(sleeping.waitFor(5000), "destroy"); + } finally { sleeping.close(); } + sleeping = new ProcessBuilder(fixture, "sleep").start(); + sleeping.close(); sleeping.close(); + check(sleeping.waitFor(5000), "close did not terminate"); + for (int i = 0; i < 40; ++i) { + Process p = new ProcessBuilder(fixture, "exit", "7").start(); + try { check(p.waitFor(5000) && p.exitValue() == 7, "nonzero/repeated launch"); } + finally { p.close(); } + } + System.out.println("PROCESS_CONFORMANCE_OK"); + } +} diff --git a/vm/backend/impl/javase/com/codename1/impl/ImplementationFactory.java b/vm/backend/impl/javase/com/codename1/impl/ImplementationFactory.java new file mode 100644 index 00000000000..24694f9695f --- /dev/null +++ b/vm/backend/impl/javase/com/codename1/impl/ImplementationFactory.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl; + +/// Headless factory for shared APIs. Does not initialize a server or a UI. +public final class ImplementationFactory { + private static final ImplementationFactory INSTANCE = new ImplementationFactory(); + private ImplementationFactory() { + } + public static ImplementationFactory getInstance() { + return INSTANCE; + } + public Object createProcessSupport() { + return new com.codename1.impl.process.JavaSEProcessSupport(); + } +} diff --git a/vm/backend/impl/parparvm/com/codename1/impl/ImplementationFactory.java b/vm/backend/impl/parparvm/com/codename1/impl/ImplementationFactory.java new file mode 100644 index 00000000000..26b0eebd20d --- /dev/null +++ b/vm/backend/impl/parparvm/com/codename1/impl/ImplementationFactory.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl; + +/// Headless factory for shared APIs. Does not initialize a server or a UI. +public final class ImplementationFactory { + private static final ImplementationFactory INSTANCE = new ImplementationFactory(); + private ImplementationFactory() { + } + public static ImplementationFactory getInstance() { + return INSTANCE; + } + public Object createProcessSupport() { + return new com.codename1.impl.process.NativeProcessSupport(); + } +} diff --git a/vm/backend/run-javase.sh b/vm/backend/run-javase.sh index 868be234689..88f982fb8da 100755 --- a/vm/backend/run-javase.sh +++ b/vm/backend/run-javase.sh @@ -82,7 +82,7 @@ if [ -n "$DRIVERS" ]; then BUILD_CP="$BUILD_CP:$DRIVERS"; fi # The core classes marked @SharedWithBackend come from CodenameOne/src directly. # Read into an array: these are ABSOLUTE paths, so an unquoted expansion would # split a checkout that lives under a directory with a space in its name. -SHARED_LIST="$(./shared-sources.sh)" +SHARED_LIST="$(./shared-sources.sh --javase)" SHARED=() while IFS= read -r f; do SHARED+=("$f"); done <<< "$SHARED_LIST" "$JAVAC" -nowarn -encoding UTF-8 -cp "$BUILD_CP" -d "$OUT" \ diff --git a/vm/backend/shared-sources.sh b/vm/backend/shared-sources.sh index e25547739ff..0473c0f3d7a 100755 --- a/vm/backend/shared-sources.sh +++ b/vm/backend/shared-sources.sh @@ -41,3 +41,12 @@ done # [[:space:]]* tolerates a CR, so a CRLF source is found as well. The Ant selector # in maven/backend/pom.xml uses the equivalent \s*. grep -rlE --include='*.java' '^@(com\.codename1\.impl\.)?SharedWithBackend[[:space:]]*$' "$CORE" | LC_ALL=C sort + +# Runtime-specific sources shared with the JavaSE desktop port. Keep the native +# selection free of host-JDK references. +if [ "${1:-}" = "--javase" ]; then + printf '%s\n' "$BACKEND/../../Ports/JavaSE/src/com/codename1/impl/process/JavaSEProcessSupport.java" +elif [ -n "${1:-}" ]; then + echo "usage: shared-sources.sh [--javase]" >&2 + exit 2 +fi diff --git a/vm/backend/src/com/codename1/backend/HttpServer.java b/vm/backend/src/com/codename1/backend/HttpServer.java index f85cc4ececd..ff9255bd89b 100644 --- a/vm/backend/src/com/codename1/backend/HttpServer.java +++ b/vm/backend/src/com/codename1/backend/HttpServer.java @@ -3356,8 +3356,11 @@ private static long wakeNappers(VtHost me, long now) { Map.Entry e = (Map.Entry) it.next(); long until = ((Long) e.getValue()).longValue(); if (until <= now) { + // An entry is no longer valid after iterator.remove(); the + // native HashMap clears its key when the slot is removed. + long handle = ((Long) e.getKey()).longValue(); it.remove(); - me.ringAdd(((Long) e.getKey()).longValue()); + me.ringAdd(handle); } else if (until < earliest) { earliest = until; } diff --git a/vm/backend/ws-conformance.sh b/vm/backend/ws-conformance.sh index 64945e60ec5..89ec5cc9341 100755 --- a/vm/backend/ws-conformance.sh +++ b/vm/backend/ws-conformance.sh @@ -108,7 +108,7 @@ case "$ARM" in fi OUT="$(pwd)/target/wsecho-classes" rm -rf "$OUT"; mkdir -p "$OUT" - { find src impl/javase demo/wsecho -name '*.java'; ./shared-sources.sh; } \ + { find src impl/javase demo/wsecho -name '*.java'; ./shared-sources.sh --javase; } \ | tr '\n' '\0' | xargs -0 "$JAVAC" -nowarn -d "$OUT" "$JAVA" -cp "$OUT" com.demo.WsEcho --port "$PORT" --host "$BIND_HOST" \ --workers "$WORKERS" & From 2f3785fbf72b44b61acccef83211dfb5f4cf01f7 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Mon, 12 Oct 2026 15:02:17 +0300 Subject: [PATCH 02/11] Fix Process API integration checks and Windows monotonic timing --- .../impl/android/AndroidImplementation.java | 2 +- .../processes/ProcessSnippets.java | 63 +++++++++++++ docs/developer-guide/Processes.asciidoc | 40 +-------- .../com/codename1/io/ProcessBuilderTest.java | 22 +++++ scripts/process/check-process.sh | 1 + scripts/process/process_child.c | 22 +++++ scripts/process/process_os_test.c | 22 +++++ scripts/process/test_windows_clock.py | 88 +++++++++++++++++++ vm/ByteCodeTranslator/src/cn1_process.h | 22 +++++ vm/ByteCodeTranslator/src/cn1_process_vm.h | 22 +++++ vm/ByteCodeTranslator/src/nativeMethods.m | 10 +-- .../com/demo/ProcessVirtualConformance.java | 22 +++++ .../process/com/demo/ProcessConformance.java | 22 +++++ vm/selfhost/perf-baseline/pr/6010.json | 27 ++++++ 14 files changed, 341 insertions(+), 44 deletions(-) create mode 100644 docs/demos/common/src/main/java/com/codenameone/developerguide/processes/ProcessSnippets.java create mode 100644 scripts/process/test_windows_clock.py create mode 100644 vm/selfhost/perf-baseline/pr/6010.json diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java index 5f2fa550db5..cf3c70e7115 100644 --- a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java +++ b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java @@ -10696,7 +10696,7 @@ public String getPlatformName() { @Override public String getNativeLogSnapshot() { java.io.BufferedReader reader = null; - Process proc = null; + java.lang.Process proc = null; try { proc = Runtime.getRuntime().exec(new String[]{ "logcat", "-d", "-t", "200", "-v", "threadtime"}); diff --git a/docs/demos/common/src/main/java/com/codenameone/developerguide/processes/ProcessSnippets.java b/docs/demos/common/src/main/java/com/codenameone/developerguide/processes/ProcessSnippets.java new file mode 100644 index 00000000000..1b0d5ff9acc --- /dev/null +++ b/docs/demos/common/src/main/java/com/codenameone/developerguide/processes/ProcessSnippets.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codenameone.developerguide.processes; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +public class ProcessSnippets { + // tag::process-version[] + public void readToolVersion() { + // Check isSupported() before enabling this action in your UI. + new Thread(() -> { + try (com.codename1.io.Process child = new com.codename1.io.ProcessBuilder("my-tool", "--version") + .redirectErrorStream(true).start()) { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + Thread reader = new Thread(() -> { + try { + byte[] buffer = new byte[4096]; + int count; + while ((count = child.getInputStream().read(buffer)) != -1) { + output.write(buffer, 0, count); + } + } catch (IOException error) { + // Closing the process also cancels an outstanding native read. + com.codename1.io.Log.e(error); + } + }); + reader.start(); + child.getOutputStream().close(); // No input; send EOF to the tool. + if (!child.waitFor(30, java.util.concurrent.TimeUnit.SECONDS)) { + child.destroyForcibly(); + } + child.waitFor(); + reader.join(); + String version = new String(output.toByteArray(), "UTF-8"); + // Use Display.callSerially(...) for any UI update using version. + } catch (Exception error) { + com.codename1.io.Log.e(error); + } + }).start(); + } + // end::process-version[] +} diff --git a/docs/developer-guide/Processes.asciidoc b/docs/developer-guide/Processes.asciidoc index 9913bf6d205..3cd365370ac 100644 --- a/docs/developer-guide/Processes.asciidoc +++ b/docs/developer-guide/Processes.asciidoc @@ -56,46 +56,12 @@ that differ only in case. Run process I/O and waits off the EDT. This example invokes a small version command, drains merged output concurrently, and enforces a wait timeout. Replace -`my-tool` with an installed executable or its absolute path. +`my-tool` with an installed executable or its absolute path. Import +`java.io.ByteArrayOutputStream` and `java.io.IOException`. [source,java] ---- -import com.codename1.io.Process; -import com.codename1.io.ProcessBuilder; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.concurrent.TimeUnit; - -// Check isSupported() before enabling this action in your UI. -new Thread(() -> { - try (Process child = new ProcessBuilder("my-tool", "--version") - .redirectErrorStream(true).start()) { - ByteArrayOutputStream output = new ByteArrayOutputStream(); - Thread reader = new Thread(() -> { - try { - byte[] buffer = new byte[4096]; - int count; - while ((count = child.getInputStream().read(buffer)) != -1) { - output.write(buffer, 0, count); - } - } catch (IOException error) { - // Closing the process also cancels an outstanding native read. - com.codename1.io.Log.e(error); - } - }); - reader.start(); - child.getOutputStream().close(); // No input; send EOF to the tool. - if (!child.waitFor(30, TimeUnit.SECONDS)) { - child.destroyForcibly(); - } - child.waitFor(); - reader.join(); - String version = new String(output.toByteArray(), "UTF-8"); - // Use Display.callSerially(...) for any UI update using version. - } catch (Exception error) { - com.codename1.io.Log.e(error); - } -}).start(); +include::../demos/common/src/main/java/com/codenameone/developerguide/processes/ProcessSnippets.java[tag=process-version,indent=0] ---- For commands with substantial output, stream to a consumer or file rather than diff --git a/maven/core-unittests/src/test/java/com/codename1/io/ProcessBuilderTest.java b/maven/core-unittests/src/test/java/com/codename1/io/ProcessBuilderTest.java index 8135eaab3b5..041d1061813 100644 --- a/maven/core-unittests/src/test/java/com/codename1/io/ProcessBuilderTest.java +++ b/maven/core-unittests/src/test/java/com/codename1/io/ProcessBuilderTest.java @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package com.codename1.io; import com.codename1.impl.ImplementationFactory; diff --git a/scripts/process/check-process.sh b/scripts/process/check-process.sh index fd292f27c8f..5999187ed1f 100755 --- a/scripts/process/check-process.sh +++ b/scripts/process/check-process.sh @@ -7,6 +7,7 @@ case "$MODE" in jvm|native) ;; *) echo "usage: $0 [jvm|native]" >&2; exit 2 ;; e if [[ "$MODE" == native && -n "${CI:-}" ]]; then echo "Native process conformance is local-only." >&2; exit 2 fi +python3 "$ROOT/scripts/process/test_windows_clock.py" J8="${JDK_8_HOME:-${JAVA_HOME:-}}" if [[ -z "$J8" && -x /usr/libexec/java_home ]]; then J8="$(/usr/libexec/java_home -v 1.8)"; fi : "${J8:?Set JDK_8_HOME to Java 8}" diff --git a/scripts/process/process_child.c b/scripts/process/process_child.c index decb09c1719..2b1f6725ea1 100644 --- a/scripts/process/process_child.c +++ b/scripts/process/process_child.c @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ /* Portable child fixture for the JavaSE/native process conformance test. */ #include #include diff --git a/scripts/process/process_os_test.c b/scripts/process/process_os_test.c index ebbc8678a22..1e6eb270845 100644 --- a/scripts/process/process_os_test.c +++ b/scripts/process/process_os_test.c @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ /* Runs unchanged on POSIX and Windows (including Wine). Tests the exact OS layer * used by the translated adapter, without requiring a desktop window system. */ #include "cn1_process.h" diff --git a/scripts/process/test_windows_clock.py b/scripts/process/test_windows_clock.py new file mode 100644 index 00000000000..99ae1f6818c --- /dev/null +++ b/scripts/process/test_windows_clock.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Exercise the Windows nanoTime binding with controlled QPC and wall clocks.""" +import os +from pathlib import Path +import shlex +import subprocess +import tempfile +import unittest + + +ROOT = Path(__file__).resolve().parents[2] +RUNTIME = ROOT / "vm/ByteCodeTranslator/src" + + +def function(path, signature): + source = path.read_text() + start = source.index("\n" + signature) + 1 + return source[start:source.index("\n}", start) + 2] + + +class WindowsClockTest(unittest.TestCase): + def test_wall_clock_steps_do_not_change_elapsed_time(self): + source = r''' +#include +#include +#include +#define _WIN32 1 +#define JAVA_LONG int64_t +#define CODENAME_ONE_THREAD_STATE void +#define __STATIC_INITIALIZER_java_lang_System(ignored) ((void)0) +typedef struct { int64_t QuadPart; } LARGE_INTEGER; +struct timeval { long tv_sec; long tv_usec; }; +static int64_t ticks, frequency = 10000000; +static long wall_seconds = 1800000000; +static int QueryPerformanceFrequency(LARGE_INTEGER *out) { + out->QuadPart = frequency; return 1; +} +static int QueryPerformanceCounter(LARGE_INTEGER *out) { + out->QuadPart = ticks; return 1; +} +static int gettimeofday(struct timeval *out, void *unused) { + (void)unused; + out->tv_sec = wall_seconds; out->tv_usec = 0; return 0; +} +''' + source += function(RUNTIME / "cn1_win_compat.c", "long long cn1_monotonic_micros(void)") + for name in ("currentTimeMillis", "nanoTime"): + source += "\n" + function(RUNTIME / "nativeMethods.m", + "JAVA_LONG java_lang_System_" + name + "___R_long(") + source += r''' +int main(void) { + /* Long uptime exercises the split conversion, not just tiny counters. */ + ticks = frequency * 86400LL * 365 * 10; + int64_t start = java_lang_System_nanoTime___R_long(); + int64_t wall_start = java_lang_System_currentTimeMillis___R_long(); + wall_seconds -= 3600; + ticks += frequency / 40; + assert(java_lang_System_currentTimeMillis___R_long() == wall_start - 3600000); + assert(java_lang_System_nanoTime___R_long() - start == 25000000); + wall_seconds += 7200; + ticks += frequency / 40; + assert(java_lang_System_currentTimeMillis___R_long() == wall_start + 3600000); + assert(java_lang_System_nanoTime___R_long() - start == 50000000); + /* A non-integral tick/microsecond ratio still rounds monotonically. */ + frequency = 3579545; + ticks = frequency * 100000LL; + start = java_lang_System_nanoTime___R_long(); + for (int i = 0; i < 10000; ++i) { + ++ticks; + int64_t next = java_lang_System_nanoTime___R_long(); + assert(next >= start); + start = next; + } + return 0; +} +''' + with tempfile.TemporaryDirectory(prefix="cn1-windows-clock-") as work: + test = Path(work) / "clock.c" + binary = Path(work) / "clock" + test.write_text(source) + subprocess.run(shlex.split(os.environ.get("CC", "cc")) + + ["-std=c99", "-Wall", "-Wextra", "-Werror", str(test), "-o", str(binary)], + check=True) + subprocess.run([str(binary)], check=True, cwd=work) + + +if __name__ == "__main__": + unittest.main() diff --git a/vm/ByteCodeTranslator/src/cn1_process.h b/vm/ByteCodeTranslator/src/cn1_process.h index f2894130b02..e9625480f10 100644 --- a/vm/ByteCodeTranslator/src/cn1_process.h +++ b/vm/ByteCodeTranslator/src/cn1_process.h @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ /* Subprocess OS layer shared by ParparVM desktop and backend targets. * No VM pointers cross an OS call. All pipe transfers and exit checks are polls. * SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0 */ diff --git a/vm/ByteCodeTranslator/src/cn1_process_vm.h b/vm/ByteCodeTranslator/src/cn1_process_vm.h index 34f38cbafd5..0e06b1482eb 100644 --- a/vm/ByteCodeTranslator/src/cn1_process_vm.h +++ b/vm/ByteCodeTranslator/src/cn1_process_vm.h @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ /* ParparVM bindings for the shared Codename One process adapter. * SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0 */ #ifndef CN1_PROCESS_VM_H diff --git a/vm/ByteCodeTranslator/src/nativeMethods.m b/vm/ByteCodeTranslator/src/nativeMethods.m index c114d5b5f9e..12840bba8df 100644 --- a/vm/ByteCodeTranslator/src/nativeMethods.m +++ b/vm/ByteCodeTranslator/src/nativeMethods.m @@ -1561,12 +1561,10 @@ JAVA_LONG java_lang_System_currentTimeMillis___R_long(CODENAME_ONE_THREAD_STATE) JAVA_LONG java_lang_System_nanoTime___R_long(CODENAME_ONE_THREAD_STATE) { __STATIC_INITIALIZER_java_lang_System(threadStateData); #ifdef _WIN32 - /* clock_gettime / CLOCK_MONOTONIC are absent from the MSVC / clang-cl - target, so fall back to the microsecond wall clock that already backs - currentTimeMillis on Windows (gettimeofday is supplied by cn1_win_compat). */ - struct timeval time; - gettimeofday(&time, NULL); - return (((JAVA_LONG)time.tv_sec) * 1000000000LL) + (((JAVA_LONG)time.tv_usec) * 1000LL); + /* The QPC-backed clock also used by Thread.sleep is independent of wall + clock adjustments. Its microsecond resolution is sufficient for nanoTime + (which specifies a unit, not nanosecond clock precision). */ + return (JAVA_LONG)(cn1_monotonic_micros() * 1000LL); #else struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); diff --git a/vm/backend/demo/process-virtual/com/demo/ProcessVirtualConformance.java b/vm/backend/demo/process-virtual/com/demo/ProcessVirtualConformance.java index 37780e4afe3..a00f6ca3609 100644 --- a/vm/backend/demo/process-virtual/com/demo/ProcessVirtualConformance.java +++ b/vm/backend/demo/process-virtual/com/demo/ProcessVirtualConformance.java @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package com.demo; import com.codename1.backend.Backend; diff --git a/vm/backend/demo/process/com/demo/ProcessConformance.java b/vm/backend/demo/process/com/demo/ProcessConformance.java index 64766bbbc84..12f64b61725 100644 --- a/vm/backend/demo/process/com/demo/ProcessConformance.java +++ b/vm/backend/demo/process/com/demo/ProcessConformance.java @@ -1,3 +1,25 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ package com.demo; import com.codename1.io.Process; diff --git a/vm/selfhost/perf-baseline/pr/6010.json b/vm/selfhost/perf-baseline/pr/6010.json new file mode 100644 index 00000000000..b27a784b99e --- /dev/null +++ b/vm/selfhost/perf-baseline/pr/6010.json @@ -0,0 +1,27 @@ +{ + "pr": 6010, + "rebaseline": { + "windows-x64@intel64-family-6-model-173-genuineintel": { + "hello": { + "all": { + "from": { + "memory": 0.94, + "time": 1.212, + "tolerance": { + "memory": 0.2, + "time": 0.5 + } + }, + "memory": 0.747, + "reason": "Windows x64 run 38241524753 measured WinHelloMain all-core RAM at 0.75x versus the 0.94x baseline; record the measured improvement. No causal attribution to the Process API; retain the existing memory tolerance.", + "runs": 1, + "time": 1.212, + "tolerance": { + "memory": 0.2, + "time": 0.5 + } + } + } + } + } +} From 26bd8cc40817410114a84761bfa8098fb506e8e6 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Mon, 12 Oct 2026 16:56:06 +0300 Subject: [PATCH 03/11] Fix Process API runtime surface, PMD findings and Windows environment decoding --- .../impl/process/NativeProcessSupport.java | 21 ++++- CodenameOne/src/com/codename1/io/Process.java | 1 + Ports/CLDC11/src/java/lang/Object.java | 4 + Ports/CLDC11/src/java/lang/Thread.java | 20 +++++ .../src/java/util/concurrent/TimeUnit.java | 41 ++++++++++ .../impl/process/JavaSEProcessSupport.java | 8 ++ docs/developer-guide/Processes.asciidoc | 6 ++ .../process/build-windows-environment-test.py | 79 +++++++++++++++++++ vm/ByteCodeTranslator/src/cn1_process_vm.h | 3 +- 9 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 Ports/CLDC11/src/java/util/concurrent/TimeUnit.java create mode 100644 scripts/process/build-windows-environment-test.py diff --git a/CodenameOne/src/com/codename1/impl/process/NativeProcessSupport.java b/CodenameOne/src/com/codename1/impl/process/NativeProcessSupport.java index 8c143ee4551..3c967b09181 100644 --- a/CodenameOne/src/com/codename1/impl/process/NativeProcessSupport.java +++ b/CodenameOne/src/com/codename1/impl/process/NativeProcessSupport.java @@ -35,6 +35,7 @@ /// @hidden @com.codename1.impl.SharedWithBackend public final class NativeProcessSupport extends ProcessSupport { + @Override public Map environment() { Map result = new HashMap(); String[] entries = environmentImpl(); @@ -51,6 +52,7 @@ public Map environment() { } return result; } + @Override public Process start(String[] command, String directory, Map environment, boolean mergeError) throws IOException { String[] env = null; @@ -97,6 +99,7 @@ void startReaper() { reaper.setDaemon(true); reaper.start(); } + @Override public void run() { while (isAlive()) { try { @@ -105,15 +108,19 @@ public void run() { } } } + @Override public InputStream getInputStream() { return stdout; } + @Override public InputStream getErrorStream() { return stderr; } + @Override public OutputStream getOutputStream() { return stdin; } + @Override public synchronized int exitValue() { if (!exited) { long result = poll(handle); @@ -126,11 +133,13 @@ public synchronized int exitValue() { } return exitCode; } + @Override public synchronized void destroy() { if (handle != 0) { terminate(handle, false); } } + @Override public synchronized Process destroyForcibly() { if (handle != 0) { terminate(handle, true); @@ -160,6 +169,7 @@ private synchronized int transfer(int index, byte[] bytes, int offset, int lengt } return n; } + @Override protected synchronized void finalize() { // The reaper holds this object until the OS child is gone. Streams // hold it too; finalization cannot race a poll, I/O or termination. @@ -176,6 +186,7 @@ private static void bounds(byte[] bytes, int offset, int length) { throw new IndexOutOfBoundsException(); } } + @Override protected void pause(long millis) throws InterruptedException { if (Thread.interrupted()) { throw new InterruptedException(); @@ -190,7 +201,9 @@ private void pausePipe() throws InterruptedIOException { pause(5); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); - throw new InterruptedIOException("Interrupted process I/O"); + InterruptedIOException error = new InterruptedIOException("Interrupted process I/O"); + error.initCause(ex); + throw error; } } private final class PipeInput extends InputStream { @@ -198,11 +211,13 @@ private final class PipeInput extends InputStream { PipeInput(int index) { this.index = index; } + @Override public int read() throws IOException { byte[] one = new byte[1]; return read(one, 0, 1) < 0 ? -1 : one[0] & 255; } private boolean reading; + @Override public int read(byte[] bytes, int offset, int length) throws IOException { bounds(bytes, offset, length); if (length == 0) { @@ -233,15 +248,18 @@ public int read(byte[] bytes, int offset, int length) throws IOException { } } } + @Override public void close() { closePipe(index); } } private final class PipeOutput extends OutputStream { + @Override public void write(int value) throws IOException { write(new byte[] {(byte) value}, 0, 1); } private boolean writing; + @Override public void write(byte[] bytes, int offset, int length) throws IOException { bounds(bytes, offset, length); synchronized (this) { @@ -272,6 +290,7 @@ public void write(byte[] bytes, int offset, int length) throws IOException { } } } + @Override public void close() { closePipe(0); } diff --git a/CodenameOne/src/com/codename1/io/Process.java b/CodenameOne/src/com/codename1/io/Process.java index 4d1ce55c27e..59e26c53795 100644 --- a/CodenameOne/src/com/codename1/io/Process.java +++ b/CodenameOne/src/com/codename1/io/Process.java @@ -107,6 +107,7 @@ public boolean waitFor(long timeout, TimeUnit unit) throws InterruptedException } /// Forcibly terminates the child and releases all pipes, even if one close fails. /// The runtime reaps it independently; this does not wait for termination. + @Override public void close() throws IOException { destroyForcibly(); IOException failure = null; diff --git a/Ports/CLDC11/src/java/lang/Object.java b/Ports/CLDC11/src/java/lang/Object.java index 32317dd8ac0..d8dc18fd65c 100644 --- a/Ports/CLDC11/src/java/lang/Object.java +++ b/Ports/CLDC11/src/java/lang/Object.java @@ -109,4 +109,8 @@ protected Object clone() { return null; } + /// Compile-time declaration of the runtime finalization hook. + /// Collection and finalization behavior depends on the target runtime. + protected void finalize() throws Throwable { + } } diff --git a/Ports/CLDC11/src/java/lang/Thread.java b/Ports/CLDC11/src/java/lang/Thread.java index e288871cab8..d3eb877d8d7 100644 --- a/Ports/CLDC11/src/java/lang/Thread.java +++ b/Ports/CLDC11/src/java/lang/Thread.java @@ -98,6 +98,26 @@ public void interrupt(){ return; //TODO codavaj!! } + /// Tests and clears the current thread's interrupt status. + /// Compile-time declaration; each platform supplies the runtime implementation. + public static boolean interrupted() { + return false; + } + + /// Tests this thread's interrupt status without clearing it. + public boolean isInterrupted() { + return false; + } + + /// Marks this thread as a daemon before it is started. + public final void setDaemon(boolean on) { + } + + /// Returns whether this thread is a daemon. + public final boolean isDaemon() { + return false; + } + /// Tests if this thread is alive. A thread is alive if it has been started and has not yet died. public final boolean isAlive(){ return false; //TODO codavaj!! diff --git a/Ports/CLDC11/src/java/util/concurrent/TimeUnit.java b/Ports/CLDC11/src/java/util/concurrent/TimeUnit.java new file mode 100644 index 00000000000..50e8e3a40f9 --- /dev/null +++ b/Ports/CLDC11/src/java/util/concurrent/TimeUnit.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package java.util.concurrent; + +/// Compile-time API for time units. Runtime implementations come from the host +/// JDK, Android, TeaVM, or vm/JavaAPI on ParparVM, as with the other CLDC stubs. +public enum TimeUnit { + NANOSECONDS, MICROSECONDS, MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS; + + public long convert(long duration, TimeUnit sourceUnit) { return 0; } + public long toNanos(long duration) { return 0; } + public long toMicros(long duration) { return 0; } + public long toMillis(long duration) { return 0; } + public long toSeconds(long duration) { return 0; } + public long toMinutes(long duration) { return 0; } + public long toHours(long duration) { return 0; } + public long toDays(long duration) { return 0; } + public void timedWait(Object object, long timeout) throws InterruptedException { } + public void timedJoin(Thread thread, long timeout) throws InterruptedException { } + public void sleep(long timeout) throws InterruptedException { } +} diff --git a/Ports/JavaSE/src/com/codename1/impl/process/JavaSEProcessSupport.java b/Ports/JavaSE/src/com/codename1/impl/process/JavaSEProcessSupport.java index e733e62bc0f..46b53629fab 100644 --- a/Ports/JavaSE/src/com/codename1/impl/process/JavaSEProcessSupport.java +++ b/Ports/JavaSE/src/com/codename1/impl/process/JavaSEProcessSupport.java @@ -32,9 +32,11 @@ /// Host-JDK adapter, also compiled by the JVM backend. No UI dependencies. /// @hidden public final class JavaSEProcessSupport extends ProcessSupport { + @Override public Map environment() { return new HashMap(new java.lang.ProcessBuilder().environment()); } + @Override public Process start(String[] command, String directory, Map environment, boolean mergeError) throws IOException { java.lang.ProcessBuilder builder = new java.lang.ProcessBuilder(command); @@ -53,21 +55,27 @@ private static final class Child extends Process { Child(java.lang.Process child) { this.child = child; } + @Override public InputStream getInputStream() { return child.getInputStream(); } + @Override public InputStream getErrorStream() { return child.getErrorStream(); } + @Override public OutputStream getOutputStream() { return child.getOutputStream(); } + @Override public int exitValue() { return child.exitValue(); } + @Override public void destroy() { child.destroy(); } + @Override public Process destroyForcibly() { child.destroyForcibly(); return this; diff --git a/docs/developer-guide/Processes.asciidoc b/docs/developer-guide/Processes.asciidoc index 3cd365370ac..971aad5ec71 100644 --- a/docs/developer-guide/Processes.asciidoc +++ b/docs/developer-guide/Processes.asciidoc @@ -104,3 +104,9 @@ suite additionally covers waits, interrupts, and `close()`. Native mode also runs a backend with one virtual-thread host and verifies that a pending process read or wait permits another task to run. The OS test checks descriptor/handle counts across repeated launches, child reaping, and closed-stdin failure. + +To test Windows environment decoding independently of the active code page, run +`python3 scripts/process/build-windows-environment-test.py OUTPUT.exe` with `CC` set to +a Windows C compiler. Run the executable on Windows or Wine with a non-UTF-8 +active code page. It exercises the native environment binding and string decoder +with Unicode names and values, including a supplementary character. diff --git a/scripts/process/build-windows-environment-test.py b/scripts/process/build-windows-environment-test.py new file mode 100644 index 00000000000..fb2569951a5 --- /dev/null +++ b/scripts/process/build-windows-environment-test.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Build a Win32 regression for the actual environment and string native bindings. + +Run the resulting executable on Windows (or Wine with a non-UTF-8 active code page). +Usage: python3 scripts/process/build-windows-environment-test.py OUTPUT.exe +Set CC to a Windows C compiler, e.g. x86_64-w64-mingw32-gcc. +""" +import os +from pathlib import Path +import shlex +import subprocess +import sys +import tempfile + +from test_windows_clock import function, RUNTIME + +source = r''' +#include +#include +#include +#include +#include +#include +#include +#define JAVA_OBJECT void * +#define JAVA_ARRAY_CHAR uint16_t +#define JAVA_NULL NULL +#define CODENAME_ONE_THREAD_STATE void *threadStateData +#define enteringNativeAllocations() ((void)0) +#define finishedNativeAllocations() ((void)0) +struct array { int length; void **items; }; +static int class_array1__java_lang_String; +#define CN1_SET_ARRAY_ELEMENT_OBJECT(a, i, v) (((struct array *)(a))->items[i] = (v)) +static JAVA_OBJECT allocArray(CODENAME_ONE_THREAD_STATE, int count, void *clazz, size_t size, int dimensions) { + (void)threadStateData; (void)clazz; (void)size; (void)dimensions; + struct array *a = calloc(1, sizeof(*a)); + a->length = count; a->items = calloc((size_t)count, sizeof(void *)); + return a; +} +static JAVA_OBJECT cn1StringFromUnits(CODENAME_ONE_THREAD_STATE, const JAVA_ARRAY_CHAR *units, int length) { + (void)threadStateData; + JAVA_ARRAY_CHAR *result = calloc((size_t)length + 1, sizeof(*result)); + memcpy(result, units, (size_t)length * sizeof(*result)); + return result; +} +JAVA_OBJECT newStringFromUtf8Len(CODENAME_ONE_THREAD_STATE, const char *str, int length); +''' +for signature in ("JAVA_OBJECT newStringFromUtf8Len(", "JAVA_OBJECT newStringFromNative("): + source += "\n" + function(RUNTIME / "cn1_globals.m", signature) +source += "\n" + function(RUNTIME / "cn1_process_vm.h", + "JAVA_OBJECT com_codename1_impl_process_NativeProcessSupport_environmentImpl_") +source += r''' +int main(void) { + /* Include non-ASCII names, BMP values, and a surrogate pair. */ + const wchar_t *name = L"CN1_INHERITED_\u05e9"; + const wchar_t *value = L"caf\u00e9-\u20ac-\U0001f642"; + const wchar_t *entry = L"CN1_INHERITED_\u05e9=caf\u00e9-\u20ac-\U0001f642"; + int found = 0; + /* Ensure the regression actually distinguishes UTF-8 from the active code page. */ + assert(GetACP() != CP_UTF8); + assert(SetEnvironmentVariableW(name, value)); + struct array *env = com_codename1_impl_process_NativeProcessSupport_environmentImpl___R_java_lang_String_1ARRAY(NULL); + assert(env != NULL); + for (int i = 0; i < env->length; ++i) { + if (!wcscmp((wchar_t *)env->items[i], entry)) found = 1; + free(env->items[i]); + } + free(env->items); free(env); + assert(SetEnvironmentVariableW(name, NULL)); + if (!found) { fputs("Unicode environment entry was corrupted\n", stderr); return 1; } + puts("WINDOWS_ENVIRONMENT_UTF8_OK"); + return 0; +} +''' +with tempfile.TemporaryDirectory(prefix="cn1-windows-environment-") as directory: + c_file = Path(directory) / "environment.c" + c_file.write_text(source) + subprocess.run(shlex.split(os.environ.get("CC", "x86_64-w64-mingw32-gcc")) + + ["-std=c99", "-Wall", "-Wextra", "-Werror", str(c_file), "-o", sys.argv[1]], check=True) diff --git a/vm/ByteCodeTranslator/src/cn1_process_vm.h b/vm/ByteCodeTranslator/src/cn1_process_vm.h index 0e06b1482eb..aa2bb98b684 100644 --- a/vm/ByteCodeTranslator/src/cn1_process_vm.h +++ b/vm/ByteCodeTranslator/src/cn1_process_vm.h @@ -82,7 +82,8 @@ JAVA_OBJECT com_codename1_impl_process_NativeProcessSupport_environmentImpl___R_ char *utf8 = (char *)malloc((size_t)size); if (!utf8) { FreeEnvironmentStringsW(block); return JAVA_NULL; } WideCharToMultiByte(CP_UTF8, 0, p, -1, utf8, size, NULL, NULL); - JAVA_OBJECT entry = newStringFromNative(threadStateData, utf8); + // The buffer is UTF-8, independent of the Windows active code page. + JAVA_OBJECT entry = newStringFromUtf8Len(threadStateData, utf8, size - 1); free(utf8); CN1_SET_ARRAY_ELEMENT_OBJECT(result, i, entry); } FreeEnvironmentStringsW(block); From 2fac0a411ffbe57c393ac869953b41469dc55c36 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:25:58 +0300 Subject: [PATCH 04/11] Fix monotonic process parks and native environment allocation guards --- .../process/build-windows-environment-test.py | 1 + scripts/process/check-process.sh | 2 + .../process/test_environment_allocations.py | 144 ++++++++++++++++++ scripts/process/test_scheduler_clock.py | 126 +++++++++++++++ vm/ByteCodeTranslator/src/cn1_process_vm.h | 23 ++- .../src/com/codename1/backend/HttpServer.java | 35 ++++- 6 files changed, 320 insertions(+), 11 deletions(-) create mode 100644 scripts/process/test_environment_allocations.py create mode 100644 scripts/process/test_scheduler_clock.py diff --git a/scripts/process/build-windows-environment-test.py b/scripts/process/build-windows-environment-test.py index fb2569951a5..4b3fc7aafc3 100644 --- a/scripts/process/build-windows-environment-test.py +++ b/scripts/process/build-windows-environment-test.py @@ -22,6 +22,7 @@ #include #include #include +#define CN1_CONSERVATIVE_GC_ROOTS 1 #define JAVA_OBJECT void * #define JAVA_ARRAY_CHAR uint16_t #define JAVA_NULL NULL diff --git a/scripts/process/check-process.sh b/scripts/process/check-process.sh index 5999187ed1f..0b0c75c0c53 100755 --- a/scripts/process/check-process.sh +++ b/scripts/process/check-process.sh @@ -12,6 +12,8 @@ J8="${JDK_8_HOME:-${JAVA_HOME:-}}" if [[ -z "$J8" && -x /usr/libexec/java_home ]]; then J8="$(/usr/libexec/java_home -v 1.8)"; fi : "${J8:?Set JDK_8_HOME to Java 8}" export JAVA_HOME="$J8" PATH="$J8/bin:$PATH" JDK_8_HOME="$J8" +python3 "$ROOT/scripts/process/test_scheduler_clock.py" +python3 "$ROOT/scripts/process/test_environment_allocations.py" WORK="$(mktemp -d "${TMPDIR:-/tmp}/cn1-process.XXXXXX")" WORK="$(cd "$WORK" && pwd -P)" trap 'rm -rf "$WORK"' EXIT diff --git a/scripts/process/test_environment_allocations.py b/scripts/process/test_environment_allocations.py new file mode 100644 index 00000000000..00b1ad747da --- /dev/null +++ b/scripts/process/test_environment_allocations.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +"""Check native environment allocation guards with the actual binding/decoders. + +Model a precise-GC allocation boundary and array publication, including nested +string-helper guards and Windows allocation failures after partial population. +""" +import os +from pathlib import Path +import shlex +import subprocess +import tempfile +import unittest + +from test_windows_clock import function, RUNTIME + + +class EnvironmentAllocationsTest(unittest.TestCase): + def test_guards_and_cleanup(self): + source = r''' +#include +#include +#include +#include +#include +#define JAVA_OBJECT void * +#define JAVA_ARRAY_CHAR uint16_t +#define JAVA_BOOLEAN int +#define JAVA_TRUE 1 +#define JAVA_FALSE 0 +#define JAVA_NULL NULL +struct state { int nativeAllocationMode; }; +#define CODENAME_ONE_THREAD_STATE struct state *threadStateData +static const char *cn1LastNamSetter; +''' + header = (RUNTIME / "cn1_globals.h").read_text() + for macro in ("enteringNativeAllocations", "finishedNativeAllocations"): + prefix = "#define " + macro + "() do { threadStateData" + start = header.index(prefix) + source += header[start:header.index("\n", start)] + "\n" + source += r''' +struct array { int length; void **items; }; +static struct array *allocated; +static int class_array1__java_lang_String; +static JAVA_OBJECT allocArray(CODENAME_ONE_THREAD_STATE, int count, void *clazz, size_t size, int dimensions) { + assert(threadStateData->nativeAllocationMode); + (void)clazz; (void)size; (void)dimensions; + allocated = calloc(1, sizeof(*allocated)); + allocated->length = count; allocated->items = calloc((size_t)count + 1, sizeof(void *)); + return allocated; +} +static void setEntry(CODENAME_ONE_THREAD_STATE, struct array *a, int i, void *value) { + assert(threadStateData->nativeAllocationMode); + a->items[i] = value; +} +#define CN1_SET_ARRAY_ELEMENT_OBJECT(a, i, v) setEntry(threadStateData, a, i, v) +static JAVA_OBJECT cn1StringFromUnits(CODENAME_ONE_THREAD_STATE, const JAVA_ARRAY_CHAR *units, int length) { + assert(threadStateData->nativeAllocationMode); + JAVA_ARRAY_CHAR *result = calloc((size_t)length + 1, sizeof(*result)); + memcpy(result, units, (size_t)length * sizeof(*result)); + return result; +} +JAVA_OBJECT newStringFromUtf8Len(CODENAME_ONE_THREAD_STATE, const char *str, int length); +''' + for signature in ("JAVA_OBJECT newStringFromUtf8Len(", "JAVA_OBJECT newStringFromNative("): + source += "\n" + function(RUNTIME / "cn1_globals.m", signature) + source += r''' +#ifdef TEST_WINDOWS +#define _WIN32 1 +#define CP_UTF8 65001 +static int empty, failBlock, blocks, failAfter = -1; +static wchar_t *GetEnvironmentStringsW(void) { + if (failBlock) return NULL; + ++blocks; + return empty ? L"\0" : L"FIRST=one\0SECOND=two\0"; +} +static void FreeEnvironmentStringsW(wchar_t *block) { (void)block; --blocks; } +static int WideCharToMultiByte(int cp, int flags, const wchar_t *input, int length, + char *out, int size, void *fallback, void *used) { + (void)flags; (void)length; (void)fallback; (void)used; assert(cp == CP_UTF8); + int n = (int)wcslen(input) + 1; + if (out) { assert(size == n); for (int i = 0; i < n; ++i) out[i] = (char)input[i]; } + return n; +} +static void *temporaryAllocation(size_t size) { + if (failAfter == 0) return NULL; + if (failAfter > 0) --failAfter; + return malloc(size); +} +#define malloc temporaryAllocation +#else +static char *entries[] = { "FIRST=one", "SECOND=two", NULL }; +static char **CN1P_ENVIRON = entries; +#endif +''' + source += "\n" + function(RUNTIME / "cn1_process_vm.h", + "JAVA_OBJECT com_codename1_impl_process_NativeProcessSupport_environmentImpl_") + source += r''' +static void run(int initialMode, int expectNull, int count) { + struct state state = { initialMode }; + struct array *result = com_codename1_impl_process_NativeProcessSupport_environmentImpl___R_java_lang_String_1ARRAY(&state); + assert(state.nativeAllocationMode == initialMode); + assert((result == NULL) == expectNull); + if (result) { + assert(result->length == count); + for (int i = 0; i < count; ++i) assert(result->items[i] != NULL); + } + if (allocated) { + for (int i = 0; i < allocated->length; ++i) free(allocated->items[i]); + free(allocated->items); free(allocated); allocated = NULL; + } +#ifdef TEST_WINDOWS + assert(blocks == 0); +#endif +} +int main(void) { + for (int initial = 0; initial < 2; ++initial) { + run(initial, 0, 2); +#ifdef TEST_WINDOWS + failAfter = 0; run(initial, 1, 0); + failAfter = 1; run(initial, 1, 0); + failAfter = -1; + failBlock = 1; run(initial, 1, 0); failBlock = 0; + empty = 1; run(initial, 0, 0); empty = 0; +#else + CN1P_ENVIRON = entries + 2; run(initial, 0, 0); CN1P_ENVIRON = entries; +#endif + } + return 0; +} +''' + with tempfile.TemporaryDirectory(prefix="cn1-environment-guards-") as directory: + test = Path(directory) / "environment.c" + test.write_text(source) + for platform in ("posix", "windows"): + with self.subTest(platform=platform): + binary = Path(directory) / platform + flags = ["-DTEST_WINDOWS"] if platform == "windows" else [] + subprocess.run(shlex.split(os.environ.get("CC", "cc")) + flags + + ["-std=c99", "-Wall", "-Wextra", "-Werror", str(test), "-o", str(binary)], check=True) + subprocess.run([str(binary)], check=True) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/process/test_scheduler_clock.py b/scripts/process/test_scheduler_clock.py new file mode 100644 index 00000000000..b3da7238782 --- /dev/null +++ b/scripts/process/test_scheduler_clock.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +"""Run the scheduler's actual park/wake methods with independently stepped clocks. + +The JVM VirtualThread implementation is a stub. Substitute its native wait +metadata and the clocks here; native conformance covers the real host loop. +""" +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + +from test_windows_clock import ROOT + + +class SchedulerClockTest(unittest.TestCase): + def test_relative_parks_ignore_wall_clock_steps(self): + server = (ROOT / "vm/backend/src/com/codename1/backend/HttpServer.java").read_text() + source = '''import java.util.*; +import java.io.IOException; +public class SchedulerClockTest { + static class Clock { + static long wall = 1800000000000L, nanos = Long.MAX_VALUE - 2000000L; + static long currentTimeMillis() { return wall; } + static long nanoTime() { return nanos; } + static void advance(long millis) { nanos += millis * 1000000L; } + } + static class VirtualThread { + static int count; + static long timeout; + static int waitCount(long handle) { return count; } + static long waitTimeout(long handle) { return timeout; } + static int waitDescriptor(long handle, int index) { return index + 10; } + static int waitEvents(long handle, int index) { return 1; } + static long current() { return 7; } + static void yieldNow() { } + } + static class Poller { + void add(int fd, int events) throws IOException { } + void remove(int fd) { } + } + static class VtHost { + Poller poller = new Poller(); + HashMap napping = new HashMap(); + ArrayList ring = new ArrayList(); + long[] waiters = new long[16], waiterDeadlines = new long[16]; + int waiterCount; + void ringAdd(long handle) { ring.add(handle); } + void setWaiter(int fd, long handle) { } + void addWaiter(long handle, long deadline, boolean task) { + waiters[waiterCount] = handle; waiterDeadlines[waiterCount++] = deadline; + } + void removeWaiterAt(int index) { + --waiterCount; + waiters[index] = waiters[waiterCount]; + waiterDeadlines[index] = waiterDeadlines[waiterCount]; + } + } +''' + for declaration in ("private static final long SCHEDULER_START_NANOS", "private static final java.util.HashMap NAP_REQUESTS"): + start = server.index(" " + declaration) + source += server[start:server.index(";", start) + 1] + "\n" + for method in ("private static long schedulerMillis(", "private static long schedulerDeadline(", + "private static void parkWaiter(", "private static void wakeWaiterAt(", + "private static long wakeExpiredWaiters(", "static void napUntil(", + "private void ringOrNap(", "private static long wakeNappers("): + start = server.index(" " + method) + source += server[start:server.index("\n }", start) + 6] + "\n" + source = source.replace("System.", "Clock.") + source += ''' + static void check(boolean value) { if (!value) throw new AssertionError(); } + public static void main(String[] args) { + SchedulerClockTest server = new SchedulerClockTest(); + // No-descriptor waits are the path used by NativeProcessSupport.pauseImpl. + for (int descriptors = 0; descriptors <= 2; descriptors += 2) { + for (long wallStep : new long[] {-3600000, 7200000}) { + VtHost host = new VtHost(); + VirtualThread.count = descriptors; VirtualThread.timeout = 5; + parkWaiter(host, 42, true); + Clock.wall += wallStep; + Clock.advance(4); + wakeNappers(host, schedulerMillis()); + wakeExpiredWaiters(host, schedulerMillis()); + check(host.ring.isEmpty()); + Clock.advance(1); + wakeNappers(host, schedulerMillis()); + wakeExpiredWaiters(host, schedulerMillis()); + check(host.ring.equals(Arrays.asList(42L))); + } + } + // Existing absolute-wall-time callers enter the same monotonic nap table. + VtHost host = new VtHost(); + napUntil(Clock.wall + 10); + Clock.wall -= 3600000; + server.ringOrNap(host, 7); + check(host.ring.isEmpty()); + Clock.advance(10); + wakeNappers(host, schedulerMillis()); + check(host.ring.equals(Arrays.asList(7L))); + check(schedulerDeadline(-1) == 0); + check(schedulerDeadline(0) != 0); + check(schedulerDeadline(Long.MAX_VALUE) == Long.MAX_VALUE); + host = new VtHost(); + VirtualThread.count = 2; VirtualThread.timeout = -1; + parkWaiter(host, 43, true); + Clock.advance(100000); + wakeExpiredWaiters(host, schedulerMillis()); + check(host.ring.isEmpty()); + host.napping.put(44L, Long.MAX_VALUE); + wakeNappers(host, Long.MAX_VALUE); + check(host.ring.equals(Arrays.asList(44L))); + } +} +''' + java_home = os.environ.get("JDK_8_HOME") or os.environ.get("JAVA_HOME") + javac = str(Path(java_home) / "bin/javac") if java_home else "javac" + java = str(Path(java_home) / "bin/java") if java_home else "java" + with tempfile.TemporaryDirectory(prefix="cn1-scheduler-clock-") as directory: + test = Path(directory) / "SchedulerClockTest.java" + test.write_text(source) + subprocess.run([javac, str(test)], check=True) + subprocess.run([java, "-cp", directory, "SchedulerClockTest"], check=True) + + +if __name__ == "__main__": + unittest.main() diff --git a/vm/ByteCodeTranslator/src/cn1_process_vm.h b/vm/ByteCodeTranslator/src/cn1_process_vm.h index aa2bb98b684..3b9362c490c 100644 --- a/vm/ByteCodeTranslator/src/cn1_process_vm.h +++ b/vm/ByteCodeTranslator/src/cn1_process_vm.h @@ -72,29 +72,44 @@ static char **cn1p_strings(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT array) { JAVA_OBJECT com_codename1_impl_process_NativeProcessSupport_environmentImpl___R_java_lang_String_1ARRAY(CODENAME_ONE_THREAD_STATE) { int count = 0, i; JAVA_OBJECT result; +#ifndef CN1_CONSERVATIVE_GC_ROOTS + JAVA_BOOLEAN previousNativeAllocations = threadStateData->nativeAllocationMode; +#endif #ifdef _WIN32 wchar_t *block = GetEnvironmentStringsW(), *p; if (!block) return JAVA_NULL; for (p = block; *p; p += wcslen(p) + 1) ++count; +#else + char **env = CN1P_ENVIRON; + while (env[count]) ++count; +#endif + /* The array and its entries live only in C locals until this call returns. */ + enteringNativeAllocations(); result = allocArray(threadStateData, count, &class_array1__java_lang_String, sizeof(JAVA_OBJECT), 1); +#ifdef _WIN32 for (p = block, i = 0; *p; p += wcslen(p) + 1, ++i) { int size = WideCharToMultiByte(CP_UTF8, 0, p, -1, NULL, 0, NULL, NULL); char *utf8 = (char *)malloc((size_t)size); - if (!utf8) { FreeEnvironmentStringsW(block); return JAVA_NULL; } + if (!utf8) { result = JAVA_NULL; break; } WideCharToMultiByte(CP_UTF8, 0, p, -1, utf8, size, NULL, NULL); // The buffer is UTF-8, independent of the Windows active code page. JAVA_OBJECT entry = newStringFromUtf8Len(threadStateData, utf8, size - 1); + /* String helpers end their own non-nesting allocation bracket. Restore + ours before publishing the entry or making another VM call. */ + enteringNativeAllocations(); free(utf8); CN1_SET_ARRAY_ELEMENT_OBJECT(result, i, entry); } FreeEnvironmentStringsW(block); #else - char **env = CN1P_ENVIRON; - while (env[count]) ++count; - result = allocArray(threadStateData, count, &class_array1__java_lang_String, sizeof(JAVA_OBJECT), 1); for (i = 0; i < count; ++i) { JAVA_OBJECT entry = newStringFromNative(threadStateData, env[i]); + enteringNativeAllocations(); CN1_SET_ARRAY_ELEMENT_OBJECT(result, i, entry); } +#endif + finishedNativeAllocations(); +#ifndef CN1_CONSERVATIVE_GC_ROOTS + if (previousNativeAllocations) enteringNativeAllocations(); #endif return result; } diff --git a/vm/backend/src/com/codename1/backend/HttpServer.java b/vm/backend/src/com/codename1/backend/HttpServer.java index 2f895e4a5a2..4ccac999a20 100644 --- a/vm/backend/src/com/codename1/backend/HttpServer.java +++ b/vm/backend/src/com/codename1/backend/HttpServer.java @@ -3213,6 +3213,23 @@ private void advanceTask(VtHost me, long handle) { ringOrNap(me, handle); } + // Scheduler deadlines use elapsed time, including naps requested by native + // process polling. A wall-clock correction must not extend a relative wait. + private static final long SCHEDULER_START_NANOS = System.nanoTime(); + + private static long schedulerMillis() { + // Reserve zero for an unlimited descriptor wait, even at startup. + return (System.nanoTime() - SCHEDULER_START_NANOS) / 1000000L + 1; + } + + private static long schedulerDeadline(long timeout) { + if (timeout < 0) { + return 0; + } + long now = schedulerMillis(); + return timeout > Long.MAX_VALUE - now ? Long.MAX_VALUE : now + timeout; + } + /// Parks a virtual thread that answered WAITING: its outbound descriptors go /// on this host's poller, and it comes back to the ring when one is ready or /// its timeout runs out. Until then this host runs everybody else -- which is @@ -3224,7 +3241,7 @@ private void advanceTask(VtHost me, long handle) { private static void parkWaiter(VtHost me, long handle, boolean task) { int count = VirtualThread.waitCount(handle); long timeout = VirtualThread.waitTimeout(handle); - long deadline = timeout < 0 ? 0 : System.currentTimeMillis() + timeout; + long deadline = schedulerDeadline(timeout); if (count == 0) { if (deadline == 0) { me.ringAdd(handle); @@ -3328,7 +3345,7 @@ private static void pumpTaskWaiters(VtHost me, int timeoutMillis) { me.setArmed(fd, false); } } - long now = System.currentTimeMillis(); + long now = schedulerMillis(); int iter = 0; while (iter < me.waiterCount) { long at = me.waiterDeadlines[iter]; @@ -3353,7 +3370,11 @@ static void napUntil(long untilMillis) { long self = VirtualThread.current(); if (self != 0) { synchronized (NAP_REQUESTS) { - NAP_REQUESTS.put(Long.valueOf(self), Long.valueOf(untilMillis)); + // Convert the wall-clock API once, before yielding. Both sources + // of naps then use the same monotonic scheduler clock. + long wallNow = System.currentTimeMillis(); + long remaining = untilMillis <= wallNow ? 0 : untilMillis - wallNow; + NAP_REQUESTS.put(Long.valueOf(self), Long.valueOf(schedulerDeadline(Math.max(0, remaining)))); } } VirtualThread.yieldNow(); @@ -3366,7 +3387,7 @@ private void ringOrNap(VtHost me, long handle) { synchronized (NAP_REQUESTS) { until = (Long) NAP_REQUESTS.remove(Long.valueOf(handle)); } - if (until != null && until.longValue() > System.currentTimeMillis()) { + if (until != null && until.longValue() > schedulerMillis()) { me.napping.put(Long.valueOf(handle), until); return; } @@ -4019,17 +4040,17 @@ private void runVirtualThreadHostLoop(VtHost me, int index) { // network, so polling before running them would delay them by the // whole poll timeout. boolean ranSome = drainRunnable(me); - long nextNap = wakeNappers(me, System.currentTimeMillis()); + long nextNap = wakeNappers(me, schedulerMillis()); // Outbound waits whose timeout ran out go back to the ring, and the // poll below sleeps no longer than the next one is due. - nextNap = Math.min(nextNap, wakeExpiredWaiters(me, System.currentTimeMillis())); + nextNap = Math.min(nextNap, wakeExpiredWaiters(me, schedulerMillis())); int timeout = 250; if (ranSome || !me.ringEmpty()) { timeout = 0; } else if (nextNap != Long.MAX_VALUE) { // Sleep in the poll until the earliest nap is due, not the full // idle interval: the napper wakes on time, and nothing spins. - timeout = (int) Math.max(0, Math.min(250, nextNap - System.currentTimeMillis())); + timeout = (int) Math.max(0, Math.min(250, nextNap - schedulerMillis())); } int n; try { From 3353b7262a8a0e53f7494b040f30bf710abce4a2 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 13 Oct 2026 03:46:41 +0300 Subject: [PATCH 05/11] Fix backend ZIP fixture sources and record Windows memory baseline --- vm/selfhost/perf-baseline/pr/6010.json | 22 +++++++++++++++++++ .../translator/BackendZipStreamingTest.java | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/vm/selfhost/perf-baseline/pr/6010.json b/vm/selfhost/perf-baseline/pr/6010.json index b27a784b99e..cdea92ad19a 100644 --- a/vm/selfhost/perf-baseline/pr/6010.json +++ b/vm/selfhost/perf-baseline/pr/6010.json @@ -1,6 +1,28 @@ { "pr": 6010, "rebaseline": { + "windows-x64@amd64-family-26-model-2-authenticamd": { + "stringBuilding": { + "all": { + "from": { + "memory": 0.297, + "time": 1.386, + "tolerance": { + "memory": 0.35, + "time": 0.2 + } + }, + "memory": 0.175, + "reason": "Windows x64 run 38290890489 on AMD Family 26 Model 2 measured all-core stringBuilding RAM at 0.175x versus the 0.297x baseline across five rounds. Record the measured improvement without attributing it to the Process API; retain the existing memory tolerance and time baseline.", + "runs": 1, + "time": 1.386, + "tolerance": { + "memory": 0.35, + "time": 0.2 + } + } + } + }, "windows-x64@intel64-family-6-model-173-genuineintel": { "hello": { "all": { diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/BackendZipStreamingTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/BackendZipStreamingTest.java index 61d8b01acb4..81fd3c76ca7 100644 --- a/vm/tests/src/test/java/com/codename1/tools/translator/BackendZipStreamingTest.java +++ b/vm/tests/src/test/java/com/codename1/tools/translator/BackendZipStreamingTest.java @@ -54,7 +54,7 @@ void boundedDownloadsAndCancellationCloseSources() throws Exception { paths.filter(p -> p.toString().endsWith(".java")).forEach(p -> compile.add(p.toString())); } } - String shared = run(backend, work, Arrays.asList("./shared-sources.sh")); + String shared = run(backend, work, Arrays.asList("./shared-sources.sh", "--javase")); for (String file : shared.split("\\r?\\n")) { if (!file.isEmpty()) compile.add(file); } From 7f7f7e22ddacfe2bbf4b18b6a651ecf42b7d81fa Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 13 Oct 2026 05:43:21 +0300 Subject: [PATCH 06/11] Record Windows Intel 207 recursion benchmark improvement --- vm/selfhost/perf-baseline/pr/6010.json | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/vm/selfhost/perf-baseline/pr/6010.json b/vm/selfhost/perf-baseline/pr/6010.json index cdea92ad19a..c8fd1fa64c9 100644 --- a/vm/selfhost/perf-baseline/pr/6010.json +++ b/vm/selfhost/perf-baseline/pr/6010.json @@ -44,6 +44,26 @@ } } } + }, + "windows-x64@intel64-family-6-model-207-genuineintel": { + "recursion": { + "all": { + "from": { + "memory": 0.059, + "time": 1.212, + "tolerance": { + "time": 0.2 + } + }, + "memory": 0.059, + "reason": "Windows x64 run 38328925998 on Intel Family 6 Model 207 measured all-core recursion time at 0.945x versus the 1.212x baseline across five rounds (0.931x to 0.955x). Record the measured improvement without attributing it to the Process API; retain the existing timing tolerance and memory baseline.", + "runs": 1, + "time": 0.945, + "tolerance": { + "time": 0.2 + } + } + } } } } From dfe88211f9e9db2f3839a6801e1294cffe99cbd2 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 13 Oct 2026 07:35:33 +0300 Subject: [PATCH 07/11] Fix Android editor completion race and calibrate Linux memory variation --- .../impl/android/AndroidImplementation.java | 14 ++- scripts/run-android-instrumentation-tests.sh | 4 + scripts/test_android_editor_wait.py | 108 ++++++++++++++++++ vm/selfhost/perf-baseline/pr/6010.json | 20 ++++ 4 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 scripts/test_android_editor_wait.py diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java index cf3c70e7115..adafaae440f 100644 --- a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java +++ b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java @@ -2544,16 +2544,22 @@ public void run() { } }); - if (!flag[0]) { - // Wait (if necessary) for the asynchronous runOnUiThread to do its work - synchronized (flag) { - + boolean interrupted = false; + synchronized (flag) { + // Check under the same monitor as the UI callback. Otherwise it can + // notify between the check and wait, leaving the EDT blocked forever. + while (!flag[0]) { try { flag.wait(); } catch (InterruptedException e) { + // Teardown must finish before the caller can paint another form. + interrupted = true; } } } + if (interrupted) { + Thread.currentThread().interrupt(); + } } @Override diff --git a/scripts/run-android-instrumentation-tests.sh b/scripts/run-android-instrumentation-tests.sh index e942d7cef46..2ec55539892 100755 --- a/scripts/run-android-instrumentation-tests.sh +++ b/scripts/run-android-instrumentation-tests.sh @@ -82,6 +82,10 @@ if [ ! -x "$TARGET_JAVA_BIN" ]; then exit 3 fi +# Exercise editor completion interleavings before a lost wakeup can stall the +# device suite. This runs the production handoff on the host JVM. +JAVA_HOME="$TARGET_JAVA_HOME" python3 "$SCRIPT_DIR/test_android_editor_wait.py" + cn1ss_setup "$TARGET_JAVA_BIN" "$CN1SS_HELPER_SOURCE_DIR" # Start the host-side WebSocket screenshot server on the fixed standard port. diff --git a/scripts/test_android_editor_wait.py b/scripts/test_android_editor_wait.py new file mode 100644 index 00000000000..b2236ee7b75 --- /dev/null +++ b/scripts/test_android_editor_wait.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +"""Compile the real Android editor handoff and control its thread interleavings. + +Only the Activity and editor are stubs. A hook immediately before the caller's +monitor acquisition schedules completion in the old check-before-wait gap. +""" +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] + + +class AndroidEditorWaitTest(unittest.TestCase): + def test_completion_handoff(self): + source = (ROOT / "Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java").read_text() + start = source.index(" public static void stopEditing(final boolean forceVKBClose)") + method = source[start:source.index("\n @Override", start)] + position = method.rindex("synchronized (flag)") + method = method[:position] + "beforeWaitMonitor(flag);\n " + method[position:] + harness = r''' +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +public class EditorWaitTest { + static String mode; + static Activity activity; + static volatile Runnable callback; + static volatile boolean[] completion; + static volatile boolean stopped; + static CountDownLatch acquired; + static class Activity { + void runOnUiThread(Runnable action) { + callback = action; + if (mode.equals("inline")) action.run(); + } + } + static Activity getActivity() { return activity; } + static class InPlaceEditView { + static void stopEdit(boolean close) { check(close, "close flag lost"); stopped = true; } + } + static void beforeWaitMonitor(boolean[] flag) { + completion = flag; + if (mode.equals("gap")) callback.run(); + acquired.countDown(); + } + static void check(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } + static void awaitWaiting(Thread caller) throws Exception { + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2); + while (caller.isAlive() && caller.getState() != Thread.State.WAITING + && System.nanoTime() < deadline) Thread.sleep(1); + check(caller.getState() == Thread.State.WAITING, "caller did not await completion"); + } +METHOD + public static void main(String[] args) throws Exception { + mode = args[0]; + activity = mode.equals("no-activity") ? null : new Activity(); + acquired = new CountDownLatch(1); + AtomicReference failure = new AtomicReference(); + Thread caller = new Thread(() -> { + try { + stopEditing(true); + check(activity == null || stopped, "returned before editor stopped"); + if (mode.equals("interrupt")) check(Thread.currentThread().isInterrupted(), "interrupt status lost"); + } catch (Throwable error) { failure.set(error); } + }); + caller.setDaemon(true); // A broken handoff must fail, not hang the test JVM. + caller.start(); + if (mode.equals("delayed") || mode.equals("spurious") || mode.equals("interrupt")) { + check(acquired.await(2, TimeUnit.SECONDS), "caller did not reach handoff"); + awaitWaiting(caller); + if (mode.equals("spurious")) { + synchronized (completion) { completion.notifyAll(); } + } else if (mode.equals("interrupt")) { + caller.interrupt(); + } + if (!mode.equals("delayed")) { + Thread.sleep(50); + check(caller.isAlive(), "wake without completion released caller"); + awaitWaiting(caller); + } + callback.run(); + } + caller.join(2000); + check(!caller.isAlive(), "lost editor completion notification"); + if (failure.get() != null) throw new AssertionError(failure.get()); + } +} +'''.replace("METHOD", method) + java_home = os.environ.get("JAVA_HOME") + javac = str(Path(java_home) / "bin/javac") if java_home else "javac" + java = str(Path(java_home) / "bin/java") if java_home else "java" + with tempfile.TemporaryDirectory(prefix="cn1-editor-wait-") as directory: + test = Path(directory) / "EditorWaitTest.java" + test.write_text(harness) + subprocess.run([javac, "-encoding", "UTF-8", str(test)], check=True) + for mode in ("gap", "delayed", "inline", "spurious", "interrupt", "no-activity"): + with self.subTest(mode=mode): + subprocess.run([java, "-cp", directory, "EditorWaitTest", mode], + check=True, timeout=10) + + +if __name__ == "__main__": + unittest.main() diff --git a/vm/selfhost/perf-baseline/pr/6010.json b/vm/selfhost/perf-baseline/pr/6010.json index c8fd1fa64c9..d6c61c64cff 100644 --- a/vm/selfhost/perf-baseline/pr/6010.json +++ b/vm/selfhost/perf-baseline/pr/6010.json @@ -1,6 +1,26 @@ { "pr": 6010, "rebaseline": { + "linux-x64@amd-epyc-9v74-80-core-processor": { + "translator": { + "all": { + "from": { + "memory": 0.43, + "time": 0.628, + "tolerance": { + "time": 0.2 + } + }, + "memory": 0.463, + "reason": "Recalibrate translator RAM from the two documented EPYC 9V74 runs: original baseline run 37661853941 (0.430 median, preserved in cf048eaaca^:vm/selfhost/perf-baseline/pr/5941.json) and run 38338248984 (0.497 median). Their median is 0.463. The earlier native peaks were 238-243 MB versus 249-253 MB now; JDK 25 peaks varied from 529-581 MB to 497-570 MB. Pool the run medians instead of treating the reference-JVM variation as an intentional native memory cost. Keep the existing memory tolerance and time baseline.", + "runs": 2, + "time": 0.628, + "tolerance": { + "time": 0.2 + } + } + } + }, "windows-x64@amd64-family-26-model-2-authenticamd": { "stringBuilding": { "all": { From 6a4ba1527d4668e2c4c9b0dc174d5b251428145e Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 13 Oct 2026 09:08:39 +0300 Subject: [PATCH 08/11] Bound clean-target subprocesses and record ARM64 translator memory improvement --- vm/selfhost/perf-baseline/pr/6010.json | 20 ++++ .../translator/CleanTargetCommandTest.java | 103 ++++++++++++++++++ .../CleanTargetIntegrationTest.java | 52 ++++++--- 3 files changed, 160 insertions(+), 15 deletions(-) create mode 100644 vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetCommandTest.java diff --git a/vm/selfhost/perf-baseline/pr/6010.json b/vm/selfhost/perf-baseline/pr/6010.json index d6c61c64cff..a41e3bf2d26 100644 --- a/vm/selfhost/perf-baseline/pr/6010.json +++ b/vm/selfhost/perf-baseline/pr/6010.json @@ -21,6 +21,26 @@ } } }, + "windows-arm64@armv8-64-bit-family-8-model-d84-microsoft-corporation": { + "translator": { + "all": { + "from": { + "memory": 0.57, + "time": 0.775, + "tolerance": { + "time": 0.2 + } + }, + "memory": 0.467, + "reason": "Windows ARM64 run 38347196405 on D84 measured translator RAM at 0.467x against the 0.570x reference across five rounds (0.424x to 0.491x). Record the measured improvement without attributing it to the Process API; retain the existing memory tolerance and timing baseline.", + "runs": 1, + "time": 0.775, + "tolerance": { + "time": 0.2 + } + } + } + }, "windows-x64@amd64-family-26-model-2-authenticamd": { "stringBuilding": { "all": { diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetCommandTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetCommandTest.java new file mode 100644 index 00000000000..f2156562c80 --- /dev/null +++ b/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetCommandTest.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +package com.codename1.tools.translator; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +class CleanTargetCommandTest { + @TempDir + Path directory; + + private ProcessBuilder child(String mode) { + String executable = System.getProperty("os.name").startsWith("Windows") ? "java.exe" : "java"; + return new ProcessBuilder(Paths.get(System.getProperty("java.home"), "bin", executable).toString(), + "-cp", System.getProperty("java.class.path"), Child.class.getName(), mode) + .directory(directory.toFile()); + } + + @Test + void suppliesEofAndCapturesOutputLargerThanAPipeBuffer() throws Exception { + String output = CleanTargetIntegrationTest.runCommand(child("output"), 15, TimeUnit.SECONDS); + assertTrue(output.contains("STDIN_EOF")); + assertTrue(output.contains("STDERR_CAPTURED")); + assertTrue(output.endsWith("OUTPUT_COMPLETE")); + assertTrue(output.length() > 1024 * 1024); + try (Stream files = Files.list(directory)) { + assertEquals(0, files.count(), "Successful commands should remove their temporary log"); + } + } + + @Test + void reportsNonzeroExitAndRetainsOutput() throws Exception { + AssertionError failure = assertThrows(AssertionError.class, + () -> CleanTargetIntegrationTest.runCommand(child("failure"), 15, TimeUnit.SECONDS)); + assertTrue(failure.getMessage().contains("EXIT_FAILURE")); + try (Stream files = Files.list(directory)) { + assertEquals(1, files.count(), "Failed commands should retain their complete log"); + } + } + + @Test + void timesOutEvenWhenChildKeepsOutputOpen() throws Exception { + long started = System.nanoTime(); + AssertionError failure = assertThrows(AssertionError.class, + () -> CleanTargetIntegrationTest.runCommand(child("hang"), 5, TimeUnit.SECONDS)); + assertTrue(TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - started) < 20); + assertTrue(failure.getMessage().contains("Command timed out")); + assertTrue(failure.getMessage().contains("CHILD_STARTED")); + } + + public static class Child { + public static void main(String[] args) throws Exception { + System.out.println("CHILD_STARTED"); + System.out.flush(); + if ("hang".equals(args[0])) { + Thread.sleep(60000); + } else if ("failure".equals(args[0])) { + System.err.println("EXIT_FAILURE"); + System.exit(7); + } else { + if (System.in.read() == -1) { + System.out.println("STDIN_EOF"); + } + System.err.println("STDERR_CAPTURED"); + byte[] block = new byte[8192]; + java.util.Arrays.fill(block, (byte) 'x'); + for (int i = 0; i < 256; i++) { + System.out.write(block); + } + System.out.print("OUTPUT_COMPLETE"); + } + } + } +} diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java index a3474fe858a..cf8e7ff82d0 100644 --- a/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java +++ b/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java @@ -39,7 +39,6 @@ import java.nio.file.StandardCopyOption; import java.util.Arrays; import java.util.List; -import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.*; @@ -2019,16 +2018,44 @@ static String tailOf(String output) { static String runCommand(List command, Path workingDir) throws Exception { ProcessBuilder builder = new ProcessBuilder(command); builder.directory(workingDir.toFile()); + return runCommand(builder, 600, java.util.concurrent.TimeUnit.SECONDS); + } + + static String runCommand(ProcessBuilder builder, long timeout, java.util.concurrent.TimeUnit unit) throws Exception { + Path workingDir = builder.directory().toPath(); + Path log = Files.createTempFile(workingDir, "command-", ".log"); + String command = String.join(" ", builder.command()); + System.out.println("Running command: " + command + " (output: " + log + ")"); builder.redirectErrorStream(true); - Process process = builder.start(); - String output; - try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { - output = reader.lines().collect(Collectors.joining("\n")); + // A pipe read before waitFor() can block forever, bypassing the timeout. + builder.redirectOutput(log.toFile()); + Process process = null; + boolean succeeded = false; + try { + process = builder.start(); + // None of these commands receives input. Supply EOF instead of keeping + // a child that reads stdin waiting for the test runner indefinitely. + process.getOutputStream().close(); + boolean finished = process.waitFor(timeout, unit); + if (!finished) { + process.destroyForcibly(); + process.waitFor(10, java.util.concurrent.TimeUnit.SECONDS); + } + String output = new String(Files.readAllBytes(log), StandardCharsets.UTF_8); + String diagnostic = "Command: " + command + "\nFull output: " + log + + "\nOutput (tail):\n" + tailOf(output) + sourceRootSummary(workingDir); + assertTrue(finished, "Command timed out after " + timeout + " " + unit + "\n" + diagnostic); + assertEquals(0, process.exitValue(), diagnostic); + succeeded = true; + return output; + } finally { + if (process != null && process.isAlive()) { + process.destroyForcibly(); + } + if (succeeded) { + Files.deleteIfExists(log); + } } - int exit = process.waitFor(); - assertEquals(0, exit, "Command failed: " + String.join(" ", command) - + "\nOutput (tail):\n" + tailOf(output) + sourceRootSummary(workingDir)); - return output; } /// What the generated project's source roots held when its build failed. @@ -2183,12 +2210,7 @@ void argumentsAndEnvironmentDecodeAsUtf8(CompilerHelper.CompilerConfig config) t pb.directory(buildDir.toFile()); pb.redirectErrorStream(true); pb.environment().put("CN1_UTF8_PROBE", arg); - Process p = pb.start(); - String out; - try (BufferedReader r = new BufferedReader(new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8))) { - out = r.lines().collect(Collectors.joining("\n")); - } - assertEquals(0, p.waitFor(), "Utf8ArgsApp failed:\n" + out); + String out = runCommand(pb, 600, java.util.concurrent.TimeUnit.SECONDS); assertTrue(out.contains("ARG=99,97,102,233,8364"), "argv must decode to the right code points, got:\n" + out); assertTrue(out.contains("ENV=99,97,102,233,8364"), From 4a81a24c8b32dedbd49831c0cf0557513ced7dcc Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:53:26 +0300 Subject: [PATCH 09/11] Separate desktop build deadlines from executable timeouts --- .github/workflows/linux-build-run.yml | 5 ++- .github/workflows/parparvm-tests.yml | 8 ++--- .../CleanTargetIntegrationTest.java | 31 ++++++++++++------- .../CleanTargetLinuxIntegrationTest.java | 2 +- 4 files changed, 29 insertions(+), 17 deletions(-) diff --git a/.github/workflows/linux-build-run.yml b/.github/workflows/linux-build-run.yml index 1af97d42cc2..d77875bba8e 100644 --- a/.github/workflows/linux-build-run.yml +++ b/.github/workflows/linux-build-run.yml @@ -418,7 +418,10 @@ jobs: # the job's last step fails it on a regression. Before the upload below, so the # results travel with the screenshots. - name: ParparVM vs JDK 25 (performance gate) - if: success() || failure() + # A timed-out CMake parent can leave compiler children running. Do not + # measure performance after a failed build/suite with that extra load. + # The earlier failure still fails this job and its logs are uploaded. + if: success() # Names this pull request's overlay (perf-baseline/pr/.json) in the # comment. A workflow_dispatch run carries no pull request, so the gate asks gh. env: diff --git a/.github/workflows/parparvm-tests.yml b/.github/workflows/parparvm-tests.yml index 58e65b9a609..f24feaba7d8 100644 --- a/.github/workflows/parparvm-tests.yml +++ b/.github/workflows/parparvm-tests.yml @@ -178,10 +178,10 @@ jobs: vm-tests: runs-on: ubuntu-24.04 - # The full suite takes about an hour before the bounded GC benchmark arms. - # Android compatibility and Kotlin translation add time; leave room for - # both phases while each GC workload retains its own timeout. - timeout-minutes: 120 + # The full suite and serial GC benchmarks took 69 and 47 minutes in run + # 38355108612. Both passed, but setup plus reporting exceeded two hours. + # Leave reporting headroom; each GC workload retains its own timeout. + timeout-minutes: 150 # The two database engines the backend speaks a wire protocol to. # # BackendDatabaseTest and BackendOrmTest run one body of assertions against diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java index cf8e7ff82d0..359ac28ff9e 100644 --- a/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java +++ b/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java @@ -116,7 +116,7 @@ void generatesRunnableHelloWorldUsingCleanTarget(CompilerHelper.CompilerConfig c configure.addAll(CompilerHelper.cmakeToolchainArgs()); runCommand(configure, distDir); - runCommand(Arrays.asList("cmake", "--build", buildDir.toString()), distDir); + runCmakeBuild(buildDir, distDir); Path executable = buildDir.resolve(CompilerHelper.executableName("HelloCleanApp")); String output = runCommand(Arrays.asList(executable.toString()), buildDir); @@ -159,7 +159,7 @@ void scientificFloatToStringOutputIsTerminated(CompilerHelper.CompilerConfig con configure.addAll(CompilerHelper.cmakeToolchainArgs()); runCommand(configure, distDir); - runCommand(Arrays.asList("cmake", "--build", buildDir.toString()), distDir); + runCmakeBuild(buildDir, distDir); Path executable = buildDir.resolve(CompilerHelper.executableName("FloatingToStringApp")); String output = runCommand(Arrays.asList(executable.toString()), buildDir); @@ -245,7 +245,7 @@ void generatesRunnableExecutableForWindowsAppType(CompilerHelper.CompilerConfig configure.addAll(CompilerHelper.cmakeToolchainArgs()); runCommand(configure, distDir); - runCommand(Arrays.asList("cmake", "--build", buildDir.toString()), distDir); + runCmakeBuild(buildDir, distDir); Path executable = buildDir.resolve(CompilerHelper.executableName("WinCleanApp")); String output = runCommand(Arrays.asList(executable.toString()), buildDir); @@ -551,7 +551,7 @@ void rendersOffscreenToPngWithDirect2D() throws Exception { "-DCMAKE_BUILD_TYPE=Release", "-G", "Ninja", "-DCMAKE_C_COMPILER=clang-cl", "-DCMAKE_CXX_COMPILER=clang-cl")); runCommand(configure, distDir); - runCommand(Arrays.asList("cmake", "--build", buildDir.toString()), distDir); + runCmakeBuild(buildDir, distDir); Path exe = buildDir.resolve(CompilerHelper.executableName("WinGfxTest")); // The app writes cn1_render.png into its working directory. @@ -701,7 +701,7 @@ static Path buildWindowsNativeExe(String appName, String source) throws Exceptio "-DCMAKE_BUILD_TYPE=RelWithDebInfo", "-G", "Ninja", "-DCMAKE_C_COMPILER=clang-cl", "-DCMAKE_CXX_COMPILER=clang-cl")); runCommand(configure, cmakeRoot); - runCommand(Arrays.asList("cmake", "--build", buildDir.toString()), cmakeRoot); + runCmakeBuild(buildDir, cmakeRoot); Path exe = buildDir.resolve(CompilerHelper.executableName(appName)); assertTrue(Files.exists(exe), "native executable should be produced: " + exe); // Ship the port's native material theme next to the exe so the Windows @@ -838,7 +838,7 @@ static Path crossBuildDist(Path cmakeRoot, Path sys, String clangCl, String exeN "-DCMAKE_RC_COMPILER=" + llvmRc, "-DCMAKE_RC_FLAGS=" + rcFlags, "-DCMAKE_EXE_LINKER_FLAGS=" + linkFlags), cmakeRoot); - runCommand(Arrays.asList("cmake", "--build", buildDir.toString()), cmakeRoot); + runCmakeBuild(buildDir, cmakeRoot); Path exe = buildDir.resolve(exeName + ".exe"); assertTrue(Files.exists(exe), "cross-compiled Windows executable should be produced: " + exe); @@ -1065,7 +1065,7 @@ static Path buildHelloCodenameOneExe() throws Exception { runCommand(Arrays.asList("cmake", "-S", cmakeRoot.toString(), "-B", buildDir.toString(), "-DCMAKE_BUILD_TYPE=RelWithDebInfo", "-G", "Ninja", "-DCMAKE_C_COMPILER=clang-cl", "-DCMAKE_CXX_COMPILER=clang-cl"), cmakeRoot); - runCommand(Arrays.asList("cmake", "--build", buildDir.toString()), cmakeRoot); + runCmakeBuild(buildDir, cmakeRoot); Path exe = buildDir.resolve(CompilerHelper.executableName("WinHelloMain")); assertTrue(Files.exists(exe), "native executable should be produced: " + exe); // The single executable is fully self-contained: its classpath resources @@ -1643,7 +1643,7 @@ void rendersOffscreenToPngWithDirect3D() throws Exception { runCommand(Arrays.asList("cmake", "-S", distDir.toString(), "-B", buildDir.toString(), "-DCMAKE_BUILD_TYPE=Release", "-G", "Ninja", "-DCMAKE_C_COMPILER=clang-cl", "-DCMAKE_CXX_COMPILER=clang-cl"), distDir); - runCommand(Arrays.asList("cmake", "--build", buildDir.toString()), distDir); + runCmakeBuild(buildDir, distDir); Path exe = buildDir.resolve(CompilerHelper.executableName("WinGfx3DTest")); String out = runCommand(Arrays.asList(exe.toString()), buildDir); Path png = buildDir.resolve("cn1_render.png"); @@ -1693,7 +1693,7 @@ void rendersOffscreenToPngWithDirect3D() throws Exception { "-DCMAKE_BUILD_TYPE=Release", "-DCMAKE_C_FLAGS=" + inc, "-DCMAKE_CXX_FLAGS=" + inc, "-DCMAKE_RC_COMPILER=" + llvmRc, "-DCMAKE_RC_FLAGS=" + rcFlags, "-DCMAKE_EXE_LINKER_FLAGS=" + linkFlags), distDir); - runCommand(Arrays.asList("cmake", "--build", buildDir.toString()), distDir); + runCmakeBuild(buildDir, distDir); Path exe = buildDir.resolve("WinGfx3DTest.exe"); assertTrue(Files.exists(exe) && Files.size(exe) > 100_000, "cross-compiled D3D PE should be produced: " + exe); System.out.println("CN1_GPU_XWIN_EXE=" + exe.toAbsolutePath()); @@ -2015,6 +2015,15 @@ static String tailOf(String output) { return b.toString(); } + static String runCmakeBuild(Path buildDir, Path workingDir) throws Exception { + // The full desktop suite compiles more than 4,000 translation units. On + // Linux/musl, a healthy build can take over 20 minutes; it must not share + // the ten-minute deadline used to detect a hung test executable. + ProcessBuilder builder = new ProcessBuilder("cmake", "--build", buildDir.toString()); + builder.directory(workingDir.toFile()); + return runCommand(builder, 30, java.util.concurrent.TimeUnit.MINUTES); + } + static String runCommand(List command, Path workingDir) throws Exception { ProcessBuilder builder = new ProcessBuilder(command); builder.directory(workingDir.toFile()); @@ -2200,7 +2209,7 @@ void argumentsAndEnvironmentDecodeAsUtf8(CompilerHelper.CompilerConfig config) t "cmake", "-S", distDir.toString(), "-B", buildDir.toString(), "-DCMAKE_BUILD_TYPE=Release")); configure.addAll(CompilerHelper.cmakeToolchainArgs()); runCommand(configure, distDir); - runCommand(Arrays.asList("cmake", "--build", buildDir.toString()), distDir); + runCmakeBuild(buildDir, distDir); // U+00E9 (two UTF-8 bytes) and U+20AC (three), so both the 2- and 3-byte // paths are covered; widening would report the individual bytes instead. @@ -2246,7 +2255,7 @@ void closingStandardInputIsHonoured(CompilerHelper.CompilerConfig config) throws "cmake", "-S", distDir.toString(), "-B", buildDir.toString(), "-DCMAKE_BUILD_TYPE=Release")); configure.addAll(CompilerHelper.cmakeToolchainArgs()); runCommand(configure, distDir); - runCommand(Arrays.asList("cmake", "--build", buildDir.toString()), distDir); + runCmakeBuild(buildDir, distDir); String output = runCommand( Arrays.asList(buildDir.resolve(CompilerHelper.executableName("StdinCloseApp")).toString()), buildDir); diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetLinuxIntegrationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetLinuxIntegrationTest.java index 327e753283f..eddab1d23fd 100644 --- a/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetLinuxIntegrationTest.java +++ b/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetLinuxIntegrationTest.java @@ -314,7 +314,7 @@ static Path buildHelloCodenameOneElf(String launcherSource) throws Exception { // measured nothing, which is worse than no diagnostic at all. System.out.println("CN1SS:HARNESS: cmake configure: " + String.join(" ", configure)); CleanTargetIntegrationTest.runCommand(configure, cmakeRoot); - CleanTargetIntegrationTest.runCommand(Arrays.asList("cmake", "--build", buildDir.toString()), cmakeRoot); + CleanTargetIntegrationTest.runCmakeBuild(buildDir, cmakeRoot); Path elf = buildDir.resolve("LinuxHelloMain"); assertTrue(Files.exists(elf), "native ELF should be produced: " + elf); return elf; From ecf407f9d00d052cb13a46572d4bd2c13d128225 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:03:40 +0300 Subject: [PATCH 10/11] Calibrate Windows AMD 25/17 Hello memory from matched runs --- vm/selfhost/perf-baseline/pr/6010.json | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/vm/selfhost/perf-baseline/pr/6010.json b/vm/selfhost/perf-baseline/pr/6010.json index a41e3bf2d26..67dc71ccaa5 100644 --- a/vm/selfhost/perf-baseline/pr/6010.json +++ b/vm/selfhost/perf-baseline/pr/6010.json @@ -41,6 +41,28 @@ } } }, + "windows-x64@amd64-family-25-model-17-authenticamd": { + "hello": { + "all": { + "from": { + "memory": 0.766, + "time": 1.375, + "tolerance": { + "memory": 0.2, + "time": 0.5 + } + }, + "memory": 0.828, + "reason": "Recalibrate Windows AMD Family 25 Model 17 hello RAM from runs 38292660169 (0.725x median) and 38372408580 (0.931x median). Native peaks overlap: 1005-1153 MB in the earlier run versus 1021-1166 MB now; the JDK 25 reference shifted from 1496-1791 MB to 1023-1514 MB. The reference revision f8e878ece6 has the same collector and translator implementation apart from this PR Process API and monotonic-clock changes. Pool the two run medians (0.828x) to account for the observed reference-JVM variation; retain the 20% memory tolerance and the timing baseline.", + "runs": 2, + "time": 1.375, + "tolerance": { + "memory": 0.2, + "time": 0.5 + } + } + } + }, "windows-x64@amd64-family-26-model-2-authenticamd": { "stringBuilding": { "all": { From 654a4fd2fb4edd18ed1862e0f034282fa5574571 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:47:11 +0300 Subject: [PATCH 11/11] Fix playground readiness and calibrate CI runtime budgets --- .github/workflows/ant.yml | 3 ++- .github/workflows/pr.yml | 3 ++- .../tools/verify-lightweight-editor-input.mjs | 17 +++++++++++++++-- vm/selfhost/perf-baseline/pr/6010.json | 14 ++++++++++++++ 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ant.yml b/.github/workflows/ant.yml index bbad13942c3..f802a624105 100644 --- a/.github/workflows/ant.yml +++ b/.github/workflows/ant.yml @@ -22,7 +22,8 @@ jobs: build-linux-jdk8: runs-on: ubuntu-24.04 - timeout-minutes: 45 + # Includes archetype integration tests and backend packaging after the Ant build. + timeout-minutes: 75 steps: - uses: actions/checkout@v6 diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index a8906d677c2..771d193d25f 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -168,7 +168,8 @@ jobs: build-test: runs-on: ubuntu-24.04 - timeout-minutes: 60 + # Java 8 also runs the full quality reports and packaging checks. + timeout-minutes: ${{ matrix.java-version == 8 && 90 || 60 }} container: ghcr.io/codenameone/codenameone/pr-ci-container:latest env: # Java 8 uses the native locale for class-file names containing Unicode. diff --git a/scripts/cn1playground/tools/verify-lightweight-editor-input.mjs b/scripts/cn1playground/tools/verify-lightweight-editor-input.mjs index 2b68cf299fc..858a3e283aa 100644 --- a/scripts/cn1playground/tools/verify-lightweight-editor-input.mjs +++ b/scripts/cn1playground/tools/verify-lightweight-editor-input.mjs @@ -21,11 +21,17 @@ if (!url) { } const browser = await chromium.launch({headless: true}); +const artifactDir = process.env.PLAYGROUND_INPUT_ARTIFACT_DIR + || process.env.PLAYGROUND_BROWSER_ARTIFACT_DIR; let page; let completed = false; try { page = await browser.newPage({viewport: {width: 1440, height: 900}}); + let initialPreviewOutcome; page.on('console', message => { + if (message.text().startsWith('[playground]') && initialPreviewOutcome === undefined) { + initialPreviewOutcome = message.text(); + } if (message.type() === 'error' && message.text() !== 'Failed to load resource: net::ERR_FAILED' && !message.text().includes('cloudflareinsights.com/cdn-cgi/rum')) { @@ -60,6 +66,15 @@ try { assert.ok(appFrame, 'Playground app iframe did not finish navigating'); await appFrame.waitForFunction(() => window.cn1Started === true, null, {timeout: 90000}); + // VM startup precedes the initial compilation and editor layout. Clicking while + // those are still running can focus the Code tab instead of opening the editor. + const previewDeadline = Date.now() + 90000; + while (initialPreviewOutcome === undefined && Date.now() < previewDeadline) { + await page.waitForTimeout(100); + } + assert.ok(initialPreviewOutcome && initialPreviewOutcome.includes('preview updated'), + `Initial playground preview did not become ready: ${initialPreviewOutcome || 'no outcome'}`); + const frameElement = appFrame === page.mainFrame() ? null : await appFrame.frameElement(); const frameBox = frameElement ? await frameElement.boundingBox() : {x: 0, y: 0}; const messageCountBeforeClick = await appFrame.evaluate(() => (window.__parparMessages || []).length); @@ -196,14 +211,12 @@ try { throw error; } - const artifactDir = process.env.PLAYGROUND_INPUT_ARTIFACT_DIR; if (artifactDir) { await page.screenshot({path: `${artifactDir}/playground-lightweight-input.png`, fullPage: true}); } console.log('PASS: lightweight playground input accepted spaces, arrows, rapid text, negotiated paste, and visibly repainted.'); completed = true; } finally { - const artifactDir = process.env.PLAYGROUND_INPUT_ARTIFACT_DIR; if (page && artifactDir && !completed) { await page.screenshot({path: `${artifactDir}/playground-lightweight-input-final.png`, fullPage: true}).catch(() => {}); console.error('Browser frames:', page.frames().map(frame => frame.url())); diff --git a/vm/selfhost/perf-baseline/pr/6010.json b/vm/selfhost/perf-baseline/pr/6010.json index 67dc71ccaa5..e3c9d349362 100644 --- a/vm/selfhost/perf-baseline/pr/6010.json +++ b/vm/selfhost/perf-baseline/pr/6010.json @@ -21,6 +21,20 @@ } } }, + "windows-arm64@armv8-64-bit-family-8-model-d49-microsoft-corporation": { + "translator": { + "all": { + "from": { + "memory": 0.451, + "time": 0.838 + }, + "memory": 0.513, + "reason": "Recalibrate Windows ARM64 D49 translator RAM from four matching-CPU runs: 38290890489 (0.510x), 38328925998 (0.485x), 38338248979 (0.516x), and 38387632437 (0.524x). The median of these run medians is 0.513x. Native peaks remain stable: 257.1-274.7 MB in run 38338248979 versus 259.2-265.3 MB now; the JDK 25 reference ranged from 491.9-558.7 MB versus 476.9-539.0 MB now. Retain the existing 15% memory tolerance and timing baseline; this records repeated measurements, not a new native memory allowance.", + "runs": 4, + "time": 0.838 + } + } + }, "windows-arm64@armv8-64-bit-family-8-model-d84-microsoft-corporation": { "translator": { "all": {