From 030e38676504c65ce8435c7926d89407dbd54cc3 Mon Sep 17 00:00:00 2001 From: dkimitsa Date: Mon, 24 Nov 2025 15:16:13 +0200 Subject: [PATCH 1/2] * support for launching on ios17+ devices implemented using `devicectl` utility shipped with xcode RoboVM launchers re-arranged to isolate per Target implementation currently locked to new approach and ios17+ only (for testing) --- .../java/org/robovm/compiler/AppCompiler.java | 7 +- .../org/robovm/compiler/config/Config.java | 2 +- .../plugin/debug/DebuggerLaunchPlugin.java | 154 +------- .../compiler/target/AbstractTarget.java | 5 - .../compiler/target/LaunchParameters.java | 32 +- .../console/ConsoleLaunchParameters.java | 25 ++ .../console/ConsoleLauncherProcess.java | 99 +++++ .../target/{ => console}/ConsoleTarget.java | 48 +-- .../target/framework/FrameworkTarget.java | 6 + .../target/ios/AppLauncherProcess.java | 129 ------- .../target/ios/IIOSLaunchParameters.java | 27 ++ .../robovm/compiler/target/ios/IOSTarget.java | 138 +------ .../target/ios/devicectl/AppleDevice.java | 284 ++++++++++++++ .../target/ios/devicectl/DeviceCtl.java | 147 ++++++++ .../devicectl/DeviceCtlLauncherProcess.java | 285 ++++++++++++++ .../ios/devicectl/DeviceCtlParsers.java | 248 +++++++++++++ .../IOSDeviceCtlLaunchParameters.java | 35 ++ .../target/ios/devicectl/ValueEnumEntry.java | 67 ++++ .../ios/devicelib/AppLauncherProcess.java | 263 +++++++++++++ .../IOSDeviceLaunchParameters.java | 16 +- .../ios/{ => simulator}/DeviceType.java | 116 +----- .../IOSSimulatorLaunchParameters.java | 6 +- .../compiler/target/ios/simulator/SimCtl.java | 106 ++++++ .../target/ios/simulator/SimCtlParsers.java | 106 ++++++ .../{ => simulator}/SimLauncherProcess.java | 127 +++---- .../org/robovm/compiler/util/Executor.java | 5 +- .../robovm/compiler/config/ConfigTest.java | 2 +- compiler/vm/core/src/init.c | 10 +- .../java/org/robovm/debugger/Debugger.java | 2 +- .../org/robovm/debugger/DebuggerConfig.java | 52 +-- .../robovm/debugger/hooks/HooksChannel.java | 58 +-- .../debugger/utils/IHooksConnectionUtils.java | 350 ++++++++++++++++++ .../tasks/AbstractIOSSimulatorTask.java | 2 +- .../gradle/tasks/AbstractSimulatorTask.java | 4 +- .../org/robovm/gradle/tasks/ConsoleTask.java | 2 +- .../robovm/gradle/tasks/IOSDeviceTask.java | 2 +- .../gradle/tasks/IPadSimulatorTask.java | 2 +- .../gradle/tasks/IPhoneSimulatorTask.java | 2 +- .../idea/compilation/RoboVmCompileTask.java | 2 +- ...ConsoleRunConfigurationSettingsEditor.java | 4 +- ...boVmIOSRunConfigurationSettingsEditor.java | 5 +- .../idea/running/RoboVmRunConfiguration.java | 3 +- .../running/RoboVmRunConfigurationUtils.java | 2 +- .../idea/running/RoboVmRunProfileState.java | 40 +- .../org/robovm/junit/client/TestClient.java | 15 +- .../plugin/AbstractIOSSimulatorMojo.java | 6 +- .../org/robovm/maven/plugin/ConsoleMojo.java | 3 +- .../org/robovm/maven/plugin/IPadSimMojo.java | 2 +- .../robovm/maven/plugin/IPhoneSimMojo.java | 2 +- .../surefire/RoboVMSurefireProvider.java | 5 +- 50 files changed, 2291 insertions(+), 769 deletions(-) create mode 100644 compiler/compiler/src/main/java/org/robovm/compiler/target/console/ConsoleLaunchParameters.java create mode 100644 compiler/compiler/src/main/java/org/robovm/compiler/target/console/ConsoleLauncherProcess.java rename compiler/compiler/src/main/java/org/robovm/compiler/target/{ => console}/ConsoleTarget.java (64%) delete mode 100755 compiler/compiler/src/main/java/org/robovm/compiler/target/ios/AppLauncherProcess.java create mode 100644 compiler/compiler/src/main/java/org/robovm/compiler/target/ios/IIOSLaunchParameters.java create mode 100644 compiler/compiler/src/main/java/org/robovm/compiler/target/ios/devicectl/AppleDevice.java create mode 100644 compiler/compiler/src/main/java/org/robovm/compiler/target/ios/devicectl/DeviceCtl.java create mode 100755 compiler/compiler/src/main/java/org/robovm/compiler/target/ios/devicectl/DeviceCtlLauncherProcess.java create mode 100644 compiler/compiler/src/main/java/org/robovm/compiler/target/ios/devicectl/DeviceCtlParsers.java create mode 100755 compiler/compiler/src/main/java/org/robovm/compiler/target/ios/devicectl/IOSDeviceCtlLaunchParameters.java create mode 100644 compiler/compiler/src/main/java/org/robovm/compiler/target/ios/devicectl/ValueEnumEntry.java create mode 100755 compiler/compiler/src/main/java/org/robovm/compiler/target/ios/devicelib/AppLauncherProcess.java rename compiler/compiler/src/main/java/org/robovm/compiler/target/ios/{ => devicelib}/IOSDeviceLaunchParameters.java (82%) rename compiler/compiler/src/main/java/org/robovm/compiler/target/ios/{ => simulator}/DeviceType.java (61%) rename compiler/compiler/src/main/java/org/robovm/compiler/target/ios/{ => simulator}/IOSSimulatorLaunchParameters.java (88%) create mode 100644 compiler/compiler/src/main/java/org/robovm/compiler/target/ios/simulator/SimCtl.java create mode 100644 compiler/compiler/src/main/java/org/robovm/compiler/target/ios/simulator/SimCtlParsers.java rename compiler/compiler/src/main/java/org/robovm/compiler/target/ios/{ => simulator}/SimLauncherProcess.java (63%) create mode 100644 plugins/debugger/src/main/java/org/robovm/debugger/utils/IHooksConnectionUtils.java diff --git a/compiler/compiler/src/main/java/org/robovm/compiler/AppCompiler.java b/compiler/compiler/src/main/java/org/robovm/compiler/AppCompiler.java index f7cba3e5a..12a4cb850 100755 --- a/compiler/compiler/src/main/java/org/robovm/compiler/AppCompiler.java +++ b/compiler/compiler/src/main/java/org/robovm/compiler/AppCompiler.java @@ -29,9 +29,12 @@ import org.robovm.compiler.config.StripArchivesConfig.StripArchivesBuilder; import org.robovm.compiler.log.ConsoleLogger; import org.robovm.compiler.plugin.*; -import org.robovm.compiler.target.ConsoleTarget; +import org.robovm.compiler.target.console.ConsoleTarget; import org.robovm.compiler.target.LaunchParameters; import org.robovm.compiler.target.ios.*; +import org.robovm.compiler.target.ios.simulator.DeviceType; +import org.robovm.compiler.target.ios.simulator.IOSSimulatorLaunchParameters; +import org.robovm.compiler.target.ios.simulator.SimCtl; import org.robovm.compiler.util.AntPathMatcher; import org.simpleframework.xml.Serializer; @@ -1034,7 +1037,7 @@ public void launchAsyncCleanup() { } private static void printDeviceTypesAndExit() throws IOException { - List types = DeviceType.listDeviceTypes(); + List types = SimCtl.list(); for (DeviceType type : types) { System.out.println(type.getSimpleDeviceTypeId()); } diff --git a/compiler/compiler/src/main/java/org/robovm/compiler/config/Config.java b/compiler/compiler/src/main/java/org/robovm/compiler/config/Config.java index a3fb72307..9813154f2 100755 --- a/compiler/compiler/src/main/java/org/robovm/compiler/config/Config.java +++ b/compiler/compiler/src/main/java/org/robovm/compiler/config/Config.java @@ -35,7 +35,7 @@ import org.robovm.compiler.plugin.debug.DebuggerLaunchPlugin; import org.robovm.compiler.plugin.invokedynamic.InvokeDynamicCompilerPlugin; import org.robovm.compiler.plugin.objc.*; -import org.robovm.compiler.target.ConsoleTarget; +import org.robovm.compiler.target.console.ConsoleTarget; import org.robovm.compiler.target.Target; import org.robovm.compiler.target.framework.FrameworkTarget; import org.robovm.compiler.target.ios.IOSTarget; diff --git a/compiler/compiler/src/main/java/org/robovm/compiler/plugin/debug/DebuggerLaunchPlugin.java b/compiler/compiler/src/main/java/org/robovm/compiler/plugin/debug/DebuggerLaunchPlugin.java index 607a89921..10880bd38 100644 --- a/compiler/compiler/src/main/java/org/robovm/compiler/plugin/debug/DebuggerLaunchPlugin.java +++ b/compiler/compiler/src/main/java/org/robovm/compiler/plugin/debug/DebuggerLaunchPlugin.java @@ -20,23 +20,16 @@ import org.robovm.compiler.plugin.LaunchPlugin; import org.robovm.compiler.plugin.PluginArgument; import org.robovm.compiler.plugin.PluginArguments; -import org.robovm.compiler.target.ConsoleTarget; import org.robovm.compiler.target.LaunchParameters; import org.robovm.compiler.target.Target; -import org.robovm.compiler.target.ios.IOSDeviceLaunchParameters; -import org.robovm.compiler.target.ios.IOSTarget; +import org.robovm.compiler.target.console.ConsoleLaunchParameters; +import org.robovm.compiler.target.ios.IIOSLaunchParameters; import org.robovm.debugger.Debugger; import org.robovm.debugger.DebuggerConfig; -import org.robovm.debugger.DebuggerException; import org.robovm.debugger.hooks.IHooksConnection; -import org.robovm.libimobiledevice.IDeviceConnection; -import org.robovm.libimobiledevice.util.AppLauncherCallback; +import org.robovm.debugger.utils.IHooksConnectionUtils.DelegatingFuture; import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -103,49 +96,22 @@ public void beforeLaunch(Config config, LaunchParameters parameters) { builder.setLogDir(new File(logDir)); builder.setArch(DebuggerConfig.Arch.valueOf(target.getArch().getCpuArch().name())); - // make list of arguments for target - if (ConsoleTarget.TYPE.equals(target.getType())) { + // specific settings depending on launch type + if (parameters instanceof ConsoleLaunchParameters) { File appDir = config.isSkipInstall() ? config.getTmpDir() : config.getInstallDir(); builder.setAppfile(new File(appDir, config.getExecutableName())); - - File hooksPortFile; - try { - hooksPortFile = File.createTempFile("robovm-dbg-console", ".port"); - builder.setHooksPortFile(hooksPortFile); - } catch (IOException e) { - throw new CompilerException("Failed to create debugger port file", e); - } - - parameters.getArguments().add("-rvm:PrintDebugPort=" + hooksPortFile.getAbsolutePath()); - } else if (IOSTarget.TYPE.equals(target.getType())) { + } else if (parameters instanceof IIOSLaunchParameters) { + // all ios File appDir = new File(config.isSkipInstall() ? config.getTmpDir() : config.getInstallDir(), config.getExecutableName() + ".app"); builder.setAppfile(new File(appDir, config.getExecutableName())); - - if (IOSTarget.isSimulatorArch(config.getArch())) { - // launching on simulator, it can write down port number to file on local system - File hooksPortFile; - try { - hooksPortFile = File.createTempFile("robovm-dbg-sim", ".port"); - builder.setHooksPortFile(hooksPortFile); - } catch (IOException e) { - throw new CompilerException("Failed to create simulator debuuger port file", e); - } - - parameters.getArguments().add("-rvm:PrintDebugPort=" + hooksPortFile.getAbsolutePath()); - } else { - // launching on device - IOSDeviceLaunchParameters deviceLaunchParameters = (IOSDeviceLaunchParameters) parameters; - DebuggerLauncherCallback callback = new DebuggerLauncherCallback(); - deviceLaunchParameters.setAppLauncherCallback(callback); - deviceLaunchParameters.getArguments().add("-rvm:PrintDebugPort"); - - // wait for hooks channel from launch callback - builder.setHooksConnection(callback); - } } else { throw new IllegalArgumentException("Unsupported target " + target.getType()); } + // tell launcher that debugger/hooks connection is expected + DelegatingFuture connectionPromise = new DelegatingFuture<>(); + builder.setHooksConnectionPromise(connectionPromise); + parameters.setRequestForDebuggerConnection(connectionPromise); debuggerConfig = builder.build(); } @@ -192,102 +158,4 @@ boolean argumentBoolValue(Map arguments, String key) { return Boolean.parseBoolean(v); } - - /** - * callback to receive hook port from device to connect debugger to. - * device will print out [DEBUG] hooks: debugPort= - * check hooks.c/_rvmHookSetupTCPChannel for details - * implements hooks connection interface to provide in and out streams - */ - private static class DebuggerLauncherCallback implements AppLauncherCallback, IHooksConnection { - private final static String tag = "[DEBUG] hooks: debugPort="; - private volatile Integer hooksPort; - private IDeviceConnection deviceConnection; - private String incompleteLine; - private AppLauncherInfo launchInfo; - - - @Override - public void setAppLaunchInfo(AppLauncherInfo info) { - launchInfo = info; - } - - @Override - public byte[] filterOutput(byte[] data) { - if (hooksPort == null) { - // port is not received yet, keep working - String str = new String(data, StandardCharsets.UTF_8); - if (incompleteLine != null) { - str = incompleteLine + str; - incompleteLine = null; - } - - int lookingPos = 0; - int newLineIdx = str.indexOf('\n'); - while (newLineIdx >= 0 ) { - // get next new line - if (str.startsWith(tag, lookingPos)) { - // got it - hooksPort = Integer.parseInt(str.substring(lookingPos + tag.length(), newLineIdx).trim()); - break; - } else { - // move to next line - lookingPos = newLineIdx + 1; - newLineIdx = str.indexOf('\n', newLineIdx + 1); - } - } - - // keep trailing line (without eol) - if (hooksPort == null && lookingPos < str.length()) { - incompleteLine = lookingPos != 0 ? str.substring(lookingPos) : str; - } - } - - return data; - } - - /** - * waits till port hooks port is available and establish connection - */ - @Override - public void connect() { - try { - // FIXME: waiting for app to be deployed and prepared, its should not use TARGET_WAIT_TIMEOUT time - // and it has to be moved to pre-launch sequence - long ts = System.currentTimeMillis(); - while (launchInfo == null) { - if (System.currentTimeMillis() - ts > DebuggerConfig.TARGET_DEPLOY_TIMEOUT) - throw new DebuggerException("Timeout while waiting app is deployed"); - Thread.sleep(200); - } - - // waiting for target to start and hooks are available - ts = System.currentTimeMillis(); - while (hooksPort == null) { - if (System.currentTimeMillis() - ts > DebuggerConfig.TARGET_WAIT_TIMEOUT) - throw new DebuggerException("Timeout while waiting app is responding on device"); - Thread.sleep(200); - } - - deviceConnection = launchInfo.getDevice().connect(hooksPort); - } catch (InterruptedException e) { - throw new DebuggerException(e); - } - } - - @Override - public void disconnect() { - deviceConnection.close(); - } - - @Override - public InputStream getInputStream() { - return deviceConnection.getInputStream(); - } - - @Override - public OutputStream getOutputStream() { - return deviceConnection.getOutputStream(); - } - } } diff --git a/compiler/compiler/src/main/java/org/robovm/compiler/target/AbstractTarget.java b/compiler/compiler/src/main/java/org/robovm/compiler/target/AbstractTarget.java index b30716c36..e83e0feea 100755 --- a/compiler/compiler/src/main/java/org/robovm/compiler/target/AbstractTarget.java +++ b/compiler/compiler/src/main/java/org/robovm/compiler/target/AbstractTarget.java @@ -62,11 +62,6 @@ public boolean canLaunch() { public void prepareLaunch() throws IOException { } - @Override - public LaunchParameters createLaunchParameters() { - return new LaunchParameters(); - } - public String getInstallRelativeArchivePath(Path path) { String name = config.getArchiveName(path); if (path.isInBootClasspath()) { diff --git a/compiler/compiler/src/main/java/org/robovm/compiler/target/LaunchParameters.java b/compiler/compiler/src/main/java/org/robovm/compiler/target/LaunchParameters.java index 9ead3ddad..ef71e32e6 100755 --- a/compiler/compiler/src/main/java/org/robovm/compiler/target/LaunchParameters.java +++ b/compiler/compiler/src/main/java/org/robovm/compiler/target/LaunchParameters.java @@ -16,19 +16,26 @@ */ package org.robovm.compiler.target; +import org.robovm.debugger.hooks.IHooksConnection; +import org.robovm.debugger.utils.IHooksConnectionUtils.DelegatingFuture; + import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Map; /** + * Base class for parameters used to launch the app on different targets */ -public class LaunchParameters { - private List arguments = new ArrayList<>(); +public abstract class LaunchParameters { + private final List arguments = new ArrayList<>(); private Map environment = null; private File workingDirectory = new File("."); private File stdoutFifo = null; private File stderrFifo = null; + + /// debugger support + private DelegatingFuture requestForDebuggerConnection = null; public List getArguments() { return arguments; @@ -90,4 +97,25 @@ public File getStderrFifo() { public void setStderrFifo(File stderrFifo) { this.stderrFifo = stderrFifo; } + + + public DelegatingFuture getRequestForDebuggerConnection() { + return requestForDebuggerConnection; + } + + /** + * Sets Future debugger will wait to retrieve debug connection to target. + * Launchers expected to take additional steps to capture information about + * connection (e.g. capture port from std output or from file) + *

+ * If launcher is not able to provide such information or debug mode is not supported + * it should complete feature with exception + *

+ * if `requestForDebugConnection` wasn't set -- Launcher should launch without preparing + * for debug + */ + public LaunchParameters setRequestForDebuggerConnection(DelegatingFuture request) { + this.requestForDebuggerConnection = request; + return this; + } } diff --git a/compiler/compiler/src/main/java/org/robovm/compiler/target/console/ConsoleLaunchParameters.java b/compiler/compiler/src/main/java/org/robovm/compiler/target/console/ConsoleLaunchParameters.java new file mode 100644 index 000000000..62d8726d5 --- /dev/null +++ b/compiler/compiler/src/main/java/org/robovm/compiler/target/console/ConsoleLaunchParameters.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2025 The MobiVM Contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.robovm.compiler.target.console; + +import org.robovm.compiler.target.LaunchParameters; + +/** + * Launch parameters dedicated for console tartget + */ +public class ConsoleLaunchParameters extends LaunchParameters { +} diff --git a/compiler/compiler/src/main/java/org/robovm/compiler/target/console/ConsoleLauncherProcess.java b/compiler/compiler/src/main/java/org/robovm/compiler/target/console/ConsoleLauncherProcess.java new file mode 100644 index 000000000..72388d1fe --- /dev/null +++ b/compiler/compiler/src/main/java/org/robovm/compiler/target/console/ConsoleLauncherProcess.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2025 The MobiVM Contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.robovm.compiler.target.console; + +import org.robovm.compiler.CompilerException; +import org.robovm.compiler.log.Logger; +import org.robovm.compiler.target.Launcher; +import org.robovm.compiler.util.Executor; +import org.robovm.compiler.util.io.OpenOnWriteFileOutputStream; +import org.robovm.debugger.hooks.IHooksConnection; +import org.robovm.debugger.utils.IHooksConnectionUtils; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Future; + +/** + * Launcher for console application. + * Just delegates to Executor, setup debugger if required + */ +public class ConsoleLauncherProcess implements Launcher { + private final Logger log; + private final File executable; + private final ConsoleLaunchParameters launchParameters; + + public ConsoleLauncherProcess(Logger log, File executable, ConsoleLaunchParameters launchParameters) { + this.log = log; + this.executable = executable; + this.launchParameters = launchParameters; + } + + @Override + public Process execAsync() throws IOException { + // provide debugger connection information if it was requested + // has to be done before argument list is built + setupDebuggerConnection(); + + List arguments = new ArrayList<>(launchParameters.getArguments(true)); + Map env = launchParameters.getEnvironment(); + File wd = launchParameters.getWorkingDirectory(); + OutputStream errStream = System.out; + OutputStream outStream = System.err; + if (launchParameters.getStdoutFifo() != null) { + outStream = new OpenOnWriteFileOutputStream(launchParameters.getStdoutFifo()); + } + if (launchParameters.getStderrFifo() != null) { + errStream = new OpenOnWriteFileOutputStream(launchParameters.getStderrFifo()); + } + + return new Executor(log, executable.getAbsolutePath()) + .args(arguments) + .wd(wd) + .inheritEnv(env == null) + .out(outStream).err(errStream).closeOutputStreams(true) + .env(env == null ? Collections.emptyMap() : env) + .execAsync(); + } + + /** + * setups additional debug parameters, shall be called BEFORE arguments are extracted from launch params + */ + private void setupDebuggerConnection() { + IHooksConnectionUtils.DelegatingFuture requestFuture = launchParameters.getRequestForDebuggerConnection(); + if (requestFuture == null) return; + + // launching on simulator, it can write down port number to file on local system + File hooksPortFile; + try { + hooksPortFile = File.createTempFile("robovm-dbg-console", ".port"); + } catch (IOException e) { + throw new CompilerException("Failed to create debugger port file", e); + } + launchParameters.getArguments().add("-rvm:PrintDebugPort=" + hooksPortFile.getAbsolutePath()); + + // provide future to debugger + Future connectionFuture = IHooksConnectionUtils.waitForPortFromFile(hooksPortFile) + .thenApply(IHooksConnectionUtils.SocketHooksConnection::new); + requestFuture.setDelegate(connectionFuture); + } +} diff --git a/compiler/compiler/src/main/java/org/robovm/compiler/target/ConsoleTarget.java b/compiler/compiler/src/main/java/org/robovm/compiler/target/console/ConsoleTarget.java similarity index 64% rename from compiler/compiler/src/main/java/org/robovm/compiler/target/ConsoleTarget.java rename to compiler/compiler/src/main/java/org/robovm/compiler/target/console/ConsoleTarget.java index 976dd0550..af27f5a43 100755 --- a/compiler/compiler/src/main/java/org/robovm/compiler/target/ConsoleTarget.java +++ b/compiler/compiler/src/main/java/org/robovm/compiler/target/console/ConsoleTarget.java @@ -14,21 +14,20 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.robovm.compiler.target; +package org.robovm.compiler.target.console; + +import org.robovm.compiler.config.Arch; +import org.robovm.compiler.config.Config; +import org.robovm.compiler.config.OS; +import org.robovm.compiler.target.AbstractTarget; +import org.robovm.compiler.target.LaunchParameters; +import org.robovm.compiler.target.Launcher; +import org.robovm.compiler.target.Target; import java.io.File; import java.io.IOException; -import java.io.OutputStream; import java.util.Collections; import java.util.List; -import java.util.Map; - -import org.robovm.compiler.config.Arch; -import org.robovm.compiler.config.Config; -import org.robovm.compiler.config.CpuArch; -import org.robovm.compiler.config.OS; -import org.robovm.compiler.util.Executor; -import org.robovm.compiler.util.io.OpenOnWriteFileOutputStream; /** @@ -63,28 +62,8 @@ public List getDefaultArchs() { @Override protected Launcher createLauncher(LaunchParameters launchParameters) throws IOException { File dir = config.isSkipInstall() ? config.getTmpDir() : config.getInstallDir(); - OutputStream out = System.out; - OutputStream err = System.err; - if (launchParameters.getStdoutFifo() != null) { - out = new OpenOnWriteFileOutputStream(launchParameters.getStdoutFifo()); - } - if (launchParameters.getStderrFifo() != null) { - err = new OpenOnWriteFileOutputStream(launchParameters.getStderrFifo()); - } - - return createExecutor(launchParameters, new File(dir, - config.getExecutableName()).getAbsolutePath(), - launchParameters.getArguments(true)) - .out(out).err(err).closeOutputStreams(true); - } - - protected Executor createExecutor(LaunchParameters launchParameters, String cmd, List args) throws IOException { - Map env = launchParameters.getEnvironment(); - return new Executor(config.getLogger(), cmd) - .args(args) - .wd(launchParameters.getWorkingDirectory()) - .inheritEnv(env == null) - .env(env == null ? Collections.emptyMap() : env); + File executable = new File(dir, config.getExecutableName()); + return new ConsoleLauncherProcess(config.getLogger(), executable, (ConsoleLaunchParameters) launchParameters); } public void init(Config config) { @@ -112,4 +91,9 @@ protected void doBuild(File outFile, List ccArgs, } super.doBuild(outFile, ccArgs, objectFiles, libArgs); } + + @Override + public LaunchParameters createLaunchParameters() { + return new ConsoleLaunchParameters(); + } } diff --git a/compiler/compiler/src/main/java/org/robovm/compiler/target/framework/FrameworkTarget.java b/compiler/compiler/src/main/java/org/robovm/compiler/target/framework/FrameworkTarget.java index aa3b1e908..ea775d0a8 100644 --- a/compiler/compiler/src/main/java/org/robovm/compiler/target/framework/FrameworkTarget.java +++ b/compiler/compiler/src/main/java/org/robovm/compiler/target/framework/FrameworkTarget.java @@ -10,6 +10,7 @@ import org.robovm.compiler.config.Environment; import org.robovm.compiler.config.OS; import org.robovm.compiler.target.AbstractTarget; +import org.robovm.compiler.target.LaunchParameters; import org.robovm.compiler.target.ios.SDK; import org.robovm.compiler.util.Executor; import org.robovm.compiler.util.ToolchainUtil; @@ -373,4 +374,9 @@ private void installFramework(File frameworkDir, File dsymDir, File binary, Stri config.getLogger().info("Installing Info.plist to: %s", infoPlistBin); PropertyListParser.saveAsBinary(infoPlist, infoPlistBin); } + + @Override + public LaunchParameters createLaunchParameters() { + throw new UnsupportedOperationException(); + } } diff --git a/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/AppLauncherProcess.java b/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/AppLauncherProcess.java deleted file mode 100755 index 2e5b6c23f..000000000 --- a/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/AppLauncherProcess.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (C) 2013 RoboVM AB - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - * - * This program 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 for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.robovm.compiler.target.ios; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InterruptedIOException; -import java.io.OutputStream; -import java.io.PrintStream; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicInteger; - -import org.apache.commons.io.IOUtils; -import org.apache.commons.io.output.NullOutputStream; -import org.robovm.compiler.log.ErrorOutputStream; -import org.robovm.compiler.log.Logger; -import org.robovm.compiler.target.LaunchParameters; -import org.robovm.compiler.target.Launcher; -import org.robovm.compiler.util.io.OpenOnWriteFileOutputStream; -import org.robovm.libimobiledevice.util.AppLauncher; - -/** - * {@link Process} implementation which runs an app on a device using an - * {@link AppLauncher}. - */ -public class AppLauncherProcess extends Process implements Launcher { - private final AtomicInteger threadCounter = new AtomicInteger(); - private final Logger log; - private final AppLauncher launcher; - private final WaitInputStream in = new WaitInputStream(); - private final WaitInputStream err = new WaitInputStream(); - private final CountDownLatch countDownLatch = new CountDownLatch(1); - private Thread launcherThread; - private volatile boolean finished = false; - private volatile int exitCode = -1; - private OutputStream errStream; - - public AppLauncherProcess(Logger log, AppLauncher launcher, LaunchParameters launchParameters) { - this.log = log; - this.launcher = launcher; - if (launchParameters.getStderrFifo() != null) { - this.errStream = new OpenOnWriteFileOutputStream(launchParameters.getStderrFifo()); - } - } - - @Override - public Process execAsync() throws IOException { - this.launcherThread = new Thread("AppLauncherThread-" + threadCounter.getAndIncrement()) { - @Override - public void run() { - try { - // install and launch - exitCode = launcher.launch(); - } catch (Throwable t) { - log.error("AppLauncher failed with an exception:", t.getMessage()); - t.printStackTrace(new PrintStream(new ErrorOutputStream(log), true)); - } finally { - IOUtils.closeQuietly(errStream); - finished = true; - countDownLatch.countDown(); - } - } - }; - this.launcherThread.start(); - return this; - } - - @Override - public OutputStream getOutputStream() { - return new NullOutputStream(); - } - - @Override - public InputStream getInputStream() { - return in; - } - - @Override - public InputStream getErrorStream() { - return err; - } - - @Override - public int waitFor() throws InterruptedException { - countDownLatch.await(); - return exitCode; - } - - @Override - public int exitValue() { - if (!finished) { - throw new IllegalThreadStateException("Not terminated"); - } - return exitCode; - } - - @Override - public void destroy() { - launcher.kill(); - } - - private class WaitInputStream extends InputStream { - - @Override - public int read() throws IOException { - try { - countDownLatch.await(); - } catch (InterruptedException e) { - throw new InterruptedIOException(); - } - return -1; - } - - } -} diff --git a/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/IIOSLaunchParameters.java b/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/IIOSLaunchParameters.java new file mode 100644 index 000000000..11d8d71e7 --- /dev/null +++ b/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/IIOSLaunchParameters.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2025 The MobiVM Contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.robovm.compiler.target.ios; + + +/** + * Empty interface to group IOS launch parameters: + * - simulator + * - device (using ilibmobiledevice) + * - device (using devicectl) + */ +public interface IIOSLaunchParameters { +} diff --git a/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/IOSTarget.java b/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/IOSTarget.java index cc9c42821..85037ea49 100755 --- a/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/IOSTarget.java +++ b/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/IOSTarget.java @@ -17,13 +17,7 @@ */ package org.robovm.compiler.target.ios; -import com.dd.plist.NSArray; -import com.dd.plist.NSDictionary; -import com.dd.plist.NSNumber; -import com.dd.plist.NSObject; -import com.dd.plist.NSString; -import com.dd.plist.PropertyListFormatException; -import com.dd.plist.PropertyListParser; +import com.dd.plist.*; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.filefilter.AndFileFilter; @@ -38,15 +32,16 @@ import org.robovm.compiler.target.LaunchParameters; import org.robovm.compiler.target.Launcher; import org.robovm.compiler.target.ios.ProvisioningProfile.Type; +import org.robovm.compiler.target.ios.devicectl.DeviceCtlLauncherProcess; +import org.robovm.compiler.target.ios.devicectl.IOSDeviceCtlLaunchParameters; +import org.robovm.compiler.target.ios.devicelib.AppLauncherProcess; +import org.robovm.compiler.target.ios.devicelib.IOSDeviceLaunchParameters; +import org.robovm.compiler.target.ios.simulator.IOSSimulatorLaunchParameters; +import org.robovm.compiler.target.ios.simulator.SimLauncherProcess; import org.robovm.compiler.util.Executor; import org.robovm.compiler.util.PList; import org.robovm.compiler.util.ToolchainUtil; -import org.robovm.compiler.util.io.OpenOnWriteFileOutputStream; -import org.robovm.libimobiledevice.AfcClient.UploadProgressCallback; import org.robovm.libimobiledevice.IDevice; -import org.robovm.libimobiledevice.InstallationProxyClient.StatusCallback; -import org.robovm.libimobiledevice.util.AppLauncher; -import org.robovm.libimobiledevice.util.AppLauncherCallback; import org.xml.sax.SAXException; import javax.xml.parsers.ParserConfigurationException; @@ -54,7 +49,6 @@ import java.io.FileFilter; import java.io.FileNotFoundException; import java.io.IOException; -import java.io.OutputStream; import java.math.BigInteger; import java.nio.file.Files; import java.nio.file.StandardCopyOption; @@ -94,8 +88,6 @@ public class IOSTarget extends AbstractTarget { private File entitlementsPList; private SigningIdentity signIdentity; private ProvisioningProfile provisioningProfile; - @Deprecated - private IDevice device; private File partialPListDir; public IOSTarget() {} @@ -114,7 +106,9 @@ public LaunchParameters createLaunchParameters() { if (isSimulatorArch(arch)) { return new IOSSimulatorLaunchParameters(); } - return new IOSDeviceLaunchParameters(); + // TODO: FIXME: do a smart switch here + // return new IOSDeviceLaunchParameters(); + return new IOSDeviceCtlLaunchParameters(); } public static boolean isSimulatorArch(Arch arch) { @@ -150,113 +144,15 @@ public List getSDKs() { } } - /** - * Returns the {@link IDevice} when an app has been launched on a device. - * Returns {@code null} before {@link #launch(LaunchParameters)} has been - * called or if the app was launched in the simulator. - */ - public IDevice getDevice() { - return device; - } - @Override protected Launcher createLauncher(LaunchParameters launchParameters) throws IOException { - if (isSimulatorArch(arch)) { - return createIOSSimLauncher(launchParameters); - } else { - return createIOSDevLauncher(launchParameters); - } - } - - private Launcher createIOSSimLauncher(LaunchParameters launchParameters) throws IOException { - return new SimLauncherProcess(config.getLogger(), getAppDir(), getBundleId(), (IOSSimulatorLaunchParameters) launchParameters); - } - - private Launcher createIOSDevLauncher(LaunchParameters launchParameters) - throws IOException { - - IOSDeviceLaunchParameters deviceLaunchParameters = (IOSDeviceLaunchParameters) launchParameters; - String deviceUdid = deviceLaunchParameters.getDeviceId(); - int forwardPort = deviceLaunchParameters.getForwardPort(); - - // TODO: FIXME: proxy AppLauncherCallback here: device to be captured as it is being used in junit client - // its a subject for future rework - AppLauncherCallback callback = deviceLaunchParameters.getAppPathCallback() != null ? new AppLauncherCallback() { - final AppLauncherCallback delegate = deviceLaunchParameters.getAppPathCallback(); - @Override - public void setAppLaunchInfo(AppLauncherInfo info) { - device = info.getDevice(); - delegate.setAppLaunchInfo(info); - } - - @Override - public byte[] filterOutput(byte[] data) { - return delegate.filterOutput(data); - } - } : null; - - OutputStream out = null; - if (launchParameters.getStdoutFifo() != null) { - out = new OpenOnWriteFileOutputStream(launchParameters.getStdoutFifo()); - } else { - out = System.out; - } - - Map env = launchParameters.getEnvironment(); - if (env == null) { - env = new HashMap<>(); - } - //Fix for #71, see http://stackoverflow.com/questions/37800790/hide-strange-unwanted-xcode-8-logs - env.put("OS_ACTIVITY_DT_MODE", ""); - - AppLauncher launcher = new AppLauncher(deviceUdid, getAppDir()) { - protected void log(String s, Object... args) { - config.getLogger().info(s, args); - } - } - .stdout(out) - .closeOutOnExit(true) - .args(launchParameters.getArguments(true).toArray(new String[0])) - .env(env) - .forward(forwardPort) - .appLauncherCallback(callback) - .xcodePath(ToolchainUtil.findXcodePath()) - .uploadProgressCallback(new UploadProgressCallback() { - boolean first = true; - - public void success() { - config.getLogger().info("[100%%] Upload complete"); - } - - public void progress(File path, int percentComplete) { - if (first) { - config.getLogger().info("[ 0%%] Beginning upload..."); - } - first = false; - config.getLogger().info("[%3d%%] Uploading %s...", percentComplete, path); - } - - public void error(String message) {} - }) - .installStatusCallback(new StatusCallback() { - boolean first = true; - - public void success() { - config.getLogger().info("[100%%] Install complete"); - } - - public void progress(String status, int percentComplete) { - if (first) { - config.getLogger().info("[ 0%%] Beginning installation..."); - } - first = false; - config.getLogger().info("[%3d%%] %s", percentComplete, status); - } - - public void error(String message) {} - }); - - return new AppLauncherProcess(config.getLogger(), launcher, launchParameters); + if (launchParameters instanceof IOSSimulatorLaunchParameters) { + return new SimLauncherProcess(config.getLogger(), getAppDir(), getBundleId(), (IOSSimulatorLaunchParameters) launchParameters); + } else if (launchParameters instanceof IOSDeviceLaunchParameters) { + return new AppLauncherProcess(config.getLogger(), getAppDir(), (IOSDeviceLaunchParameters) launchParameters); + } else if (launchParameters instanceof IOSDeviceCtlLaunchParameters) { + return new DeviceCtlLauncherProcess(config.getLogger(), getAppDir(), getBundleId(), (IOSDeviceCtlLaunchParameters) launchParameters); + } else throw new IllegalArgumentException("Unexpected launchParametersType: " + launchParameters.getClass().getSimpleName()); } @Override diff --git a/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/devicectl/AppleDevice.java b/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/devicectl/AppleDevice.java new file mode 100644 index 000000000..ccd4dfe2c --- /dev/null +++ b/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/devicectl/AppleDevice.java @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2025 The MobiVM Contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.robovm.compiler.target.ios.devicectl; + +import java.util.Set; + + + +/** + * Physical device types, consisting of the device type id and SDK version as + * listed by xcrun devicectl list devices -j @dest-file + */ +public class AppleDevice { + public final String identifier; + public final Set capability; + public final ConnectionProperties connectionProperties; + public final DeviceProperties deviceProperties; + public final HardwareProperties hardwareProperties; + + public AppleDevice( + String identifier, + Set capability, + ConnectionProperties connectionProperties, + DeviceProperties deviceProperties, + HardwareProperties hardwareProperties + ) { + this.identifier = identifier; + this.capability = capability; + this.connectionProperties = connectionProperties; + this.deviceProperties = deviceProperties; + this.hardwareProperties = hardwareProperties; + } + + @Override + public String toString() { + return "AppleDevice{" + + "identifier='" + identifier + '\'' + + ", capability=" + capability + + ", connectionProperties=" + connectionProperties + + ", deviceProperties=" + deviceProperties + + ", hardwareProperties=" + hardwareProperties + + '}'; + } + + /** + * capability object as parsed from + * "result.devices[0].capabilities" : [ + * { + * "featureIdentifier" : "com.apple.coredevice.feature.acquireusageassertion", + * "name" : "Acquire Usage Assertion" + * } + * ] + */ + public final static class Capability extends ValueEnumEntry { + private Capability(String rawValue) { super(rawValue); } + private static final Producer producer = new Producer<>(Capability::new); + public static Capability of(String id) { + return producer.of(id); + } + + // known constants + public static Capability ACQUIRE_USAGE_ASSERTION = of("com.apple.coredevice.feature.acquireusageassertion"); + public static Capability CAPTURE_SYSDIAGNOSE = of("com.apple.coredevice.feature.capturesysdiagnose"); + public static Capability CREATE_SERVICE_CONNECTION = of("com.apple.dt.serviceconnection.create"); + public static Capability CREATE_SERVICE_SOCKET = of("com.apple.dt.servicesocket.create"); + public static Capability DISABLE_DDI = of("com.apple.coredevice.feature.disableddiservices"); + public static Capability DISCONNECT_DEVICE = of("com.apple.coredevice.feature.disconnectdevice"); + public static Capability INSTALL_APP = of("com.apple.coredevice.feature.installapp"); + public static Capability UNINSTALL_APP = of("com.apple.coredevice.feature.uninstallapp"); + public static Capability VIEW_DEVICE_SCREEN = of("com.apple.coredevice.feature.viewdevicescreen"); + public static Capability SPAWN_EXECUTABLE = of("com.apple.coredevice.feature.spawnexecutable"); + } + + /** + * connectionProperties object as parsed from + * "result.devices[0].connectionProperties" : [ + * { + * "authenticationType" : "manualPairing", + * "pairingState" : "paired", + * "transportType" : "wired", + * "tunnelState" : "connected", + * "tunnelIPAddress" : "fdeb:b11:406e::1", + * } + * ] + */ + public final static class ConnectionProperties { + /// value of field "authenticationType" : "manualPairing", + public static final class AuthenticationType extends ValueEnumEntry{ + private AuthenticationType(String rawValue) { super(rawValue); } + private static final Producer producer = new Producer<>(AuthenticationType::new); + public static AuthenticationType of(String id) { + return producer.of(id == null ? "null" : id); + } + + // known constants + public static AuthenticationType MANUAL_PAIRING = of("manualPairing"); + } + + /// value of field pairingState" : "paired", + public static final class PairingState extends ValueEnumEntry{ + private PairingState(String rawValue) { super(rawValue); } + private static final Producer producer = new Producer<>(PairingState::new); + public static PairingState of(String id) { + return producer.of(id == null ? "null" : id); + } + + // known constants + public static PairingState PAIRED = of("paired"); + public static PairingState UNPAIRED = of("unpaired"); + } + + /// value of field "transportType" : "wired", + public static final class TransportType extends ValueEnumEntry{ + private TransportType(String rawValue) { super(rawValue); } + private static final Producer producer = new Producer<>(TransportType::new); + public static TransportType of(String id) { + return producer.of(id == null ? "null" : id); + } + + // known constants + public static TransportType WIRED = of("wired"); + } + + /// value of field "tunnelState" : "connected", + public static final class TunnelState extends ValueEnumEntry{ + private TunnelState(String rawValue) { super(rawValue); } + private static final Producer producer = new Producer<>(TunnelState::new); + public static TunnelState of(String id) { + return producer.of(id == null ? "null" : id); + } + + // known constants + public static TunnelState CONNECTED = of("connected"); + } + + public final AuthenticationType authenticationType; + public final PairingState pairingState; + public final TransportType transportType; + public final TunnelState tunnelState; + public final String tunnelIPAddress; + + public ConnectionProperties( + AuthenticationType authenticationType, + PairingState pairingState, + TransportType transportType, + TunnelState tunnelState, + String tunnelIPAddress + ) { + this.authenticationType = authenticationType; + this.pairingState = pairingState; + this.transportType = transportType; + this.tunnelState = tunnelState; + this.tunnelIPAddress = tunnelIPAddress; + } + + @Override + public String toString() { + return "ConnectionProperties{" + + "authenticationType=" + authenticationType + + ", pairingState=" + pairingState + + ", transportType=" + transportType + + ", tunnelState=" + tunnelState + + '}'; + } + } + + public final static class DeviceProperties { + public final boolean bootState; // "bootState" : "booted" + public final boolean developerModeStatus; // "developerModeStatus" : "enabled", + public final String name; // "name" : "iPhone XR", + public final String osBuildUpdate; // "osBuildUpdate" : "22F76", + public final String osVersionNumber; // "osVersionNumber" : "18.5", + + public DeviceProperties( + boolean bootState, + boolean developerModeStatus, + String name, + String osBuildUpdate, + String osVersionNumber + ) { + this.bootState = bootState; + this.developerModeStatus = developerModeStatus; + this.name = name; + this.osBuildUpdate = osBuildUpdate; + this.osVersionNumber = osVersionNumber; + } + + @Override + public String toString() { + return "DeviceProperties{" + + "bootState=" + bootState + + ", developerModeStatus=" + developerModeStatus + + ", name='" + name + '\'' + + ", osBuildUpdate='" + osBuildUpdate + '\'' + + ", osVersionNumber='" + osVersionNumber + '\'' + + '}'; + } + } + + public final static class HardwareProperties { + /// possible CPU types + public static final class CPUType extends ValueEnumEntry { + private CPUType(String rawValue) { + super(rawValue); + } + + private static final Producer producer = new Producer<>(CPUType::new); + + public static CPUType of(String id) { + return producer.of(id == null ? "null" : id); + } + + // known constants + public static CPUType ARM64E = of("arm64e"); + public static CPUType ARM64 = of("arm64"); + public static CPUType ARMV8 = of("armv8"); + } + + public final CPUType cpuType; // "cpuType" : { "name" : "arm64e" } + public final String deviceType; // "deviceType" : "iPhone", + public final long ecid; // "ecid" : 1813464338726958, + public final String hardwareModel; // "hardwareModel" : "N841AP", + public final String marketingName; // "marketingName" : "iPhone XR", + public final String platform; // "platform" : "iOS", + public final String productType; // "productType" : "iPhone11,8" + public final String serialNumber; // "serialNumber" : "DX123456789" + public final Set supportedCPUTypes; // "supportedCPUTypes" : [ { "name" : "arm64e" } ] + public final String udid; // "udid" : "00001234-000123456789012" + + public HardwareProperties( + CPUType cpuType, + String deviceType, + long ecid, + String hardwareModel, + String marketingName, + String platform, + String productType, + String serialNumber, + Set supportedCPUTypes, + String udid + ) { + this.cpuType = cpuType; + this.deviceType = deviceType; + this.ecid = ecid; + this.hardwareModel = hardwareModel; + this.marketingName = marketingName; + this.platform = platform; + this.productType = productType; + this.serialNumber = serialNumber; + this.supportedCPUTypes = supportedCPUTypes; + this.udid = udid; + } + + @Override + public String toString() { + return "HardwareProperties{" + + "cpuType=" + cpuType + + ", deviceType='" + deviceType + '\'' + + ", ecid=" + ecid + + ", hardwareModel='" + hardwareModel + '\'' + + ", marketingName='" + marketingName + '\'' + + ", platform='" + platform + '\'' + + ", productType='" + productType + '\'' + + ", serialNumber='" + serialNumber + '\'' + + ", supportedCPUTypes=" + supportedCPUTypes + + ", udid='" + udid + '\'' + + '}'; + } + } +} diff --git a/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/devicectl/DeviceCtl.java b/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/devicectl/DeviceCtl.java new file mode 100644 index 000000000..e5676b65b --- /dev/null +++ b/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/devicectl/DeviceCtl.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2025 The MobiVM Contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.robovm.compiler.target.ios.devicectl; + +import org.apache.commons.exec.ExecuteException; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; +import org.robovm.compiler.log.Logger; +import org.robovm.compiler.util.Executor; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.OutputStream; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Wrapper around `xcrun devicectl` tool + */ +public class DeviceCtl { + + /** + * gets list of devices by invoking `xcrun devicectl list devices -j @dest-file` + */ + public static List listDevices(Logger log) throws IOException, ExecuteException, ParseException { + File f = File.createTempFile("robovm-devicectl-", ".list"); + f.delete(); + + new Executor(log, "xcrun") + .args("devicectl", "list", "devices", "-j", f.getAbsolutePath()) + .exec(); + + JSONParser parser = new JSONParser(); + try (FileReader reader = new FileReader(f)) { + JSONObject root = (JSONObject) parser.parse(reader); + return DeviceCtlParsers.parseListResponse(root); + } + } + + /** + * Lists devices, no output to logger, if something goes wrong -- just return empty list + */ + public static List listDevices() { + try { + return listDevices(Logger.NULL_LOGGER); + } catch (Exception e) { + return Collections.emptyList(); + } + } + + public static List listDeviceUDIDs() { + return listDevices().stream().map( d -> d.hardwareProperties.udid).collect(Collectors.toList()); + } + + + public static AppleDevice getDeviceInfo(Logger log, String udid) throws IOException, ExecuteException, ParseException { + File f = File.createTempFile("robovm-devicectl-", ".deviceinfo"); + f.delete(); + + new Executor(log, "xcrun") + .args("devicectl", "device", "info", "details", "-q", "-d", udid, "-j", f.getAbsolutePath()) + .exec(); + + JSONParser parser = new JSONParser(); + try (FileReader reader = new FileReader(f)) { + JSONObject root = (JSONObject) parser.parse(reader); + return DeviceCtlParsers.parseDeviceInfoResponse(root); + } + } + + public static void install(Logger log, String udid, String localAppPath) throws ExecuteException, IOException, ParseException { + File f = File.createTempFile("robovm-devicectl-", ".install"); + f.delete(); + + new Executor(log, "xcrun") + .args("devicectl", "device", "install", "app", "-d", udid, "-j", f.getAbsolutePath(), localAppPath) + .exec(); + JSONParser parser = new JSONParser(); + try (FileReader reader = new FileReader(f)) { + JSONObject root = (JSONObject) parser.parse(reader); + // TODO: parse response if required + } + } + + public static void launchAndWait( + Logger log, String udid, String bundleId, + List arguments, + Map env, + OutputStream errStream, + OutputStream outStream + ) throws IOException, ExecuteException { + Executor executor = new Executor(log, "xcrun"); + List args = new ArrayList<>(); + args.add("devicectl"); + args.add("device"); + args.add("process"); + args.add("launch"); + args.add("--device"); + args.add(udid); + args.add("--console"); + args.add("--terminate-existing"); + args.add(bundleId); + if (!arguments.isEmpty()) { + args.add("--"); + args.addAll(arguments); + } + executor.args(args); + if (env != null && !env.isEmpty()) { + Map devEnv = new HashMap<>(); + for (Map.Entry entry : env.entrySet()) { + devEnv.put("DEVICECTL_CHILD_" + entry.getKey(), entry.getValue()); + } + executor.env(devEnv); + } + executor.out(outStream).err(errStream).closeOutputStreams(true).inheritEnv(false); + executor.exec(); + } + + + public static void pairDevice(Logger log, String udid) throws IOException, ExecuteException, ParseException { + new Executor(log, "xcrun") + .args("devicectl", "manage", "pair", "-d", udid) + .exec(); + } + + public static void main(String[] args) { + listDevices().forEach( + d -> System.out.println(d) + ); + } +} diff --git a/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/devicectl/DeviceCtlLauncherProcess.java b/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/devicectl/DeviceCtlLauncherProcess.java new file mode 100755 index 000000000..05e2557a9 --- /dev/null +++ b/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/devicectl/DeviceCtlLauncherProcess.java @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2025 The MobiVM Contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */package org.robovm.compiler.target.ios.devicectl; + +import org.apache.commons.exec.ExecuteException; +import org.apache.commons.io.output.NullOutputStream; +import org.robovm.compiler.CompilerException; +import org.robovm.compiler.log.ErrorOutputStream; +import org.robovm.compiler.log.Logger; +import org.robovm.compiler.target.Launcher; +import org.robovm.compiler.target.ios.devicectl.AppleDevice.ConnectionProperties.PairingState; +import org.robovm.compiler.target.ios.devicectl.AppleDevice.ConnectionProperties.TunnelState; +import org.robovm.compiler.util.io.OpenOnWriteFileOutputStream; +import org.robovm.debugger.hooks.IHooksConnection; +import org.robovm.debugger.utils.IHooksConnectionUtils; +import org.robovm.debugger.utils.IHooksConnectionUtils.OutputHookPortObserverFuture; +import org.robovm.debugger.utils.IHooksConnectionUtils.SocketHooksConnection; + +import java.io.*; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.apache.commons.exec.Executor.INVALID_EXITVALUE; + +/** + * {@link Process} implementation which runs an app on a attached device using a + * simctl + */ +public class DeviceCtlLauncherProcess extends Process implements Launcher { + private final CountDownLatch countDownLatch = new CountDownLatch(1); + private final AtomicInteger threadCounter = new AtomicInteger(); + private final Logger log; + private final File appDir; + private final String bundleId; + private final IOSDeviceCtlLaunchParameters launchParameters; + private AppleDevice device; + private Thread launcherThread; + private volatile boolean finished = false; + private volatile int exitCode = -1; + + public DeviceCtlLauncherProcess(Logger log, File appDir, String bundleId, IOSDeviceCtlLaunchParameters launchParameters) { + this.log = log; + this.appDir = appDir; + this.bundleId = bundleId; + this.launchParameters = launchParameters; + } + + private AppleDevice waitForDevice(String deviceId) throws Exception { + int retries = 20; + int retriesLeft = retries; + int secondsBetweenRetries = 1; + + while (true) { + List devices = DeviceCtl.listDevices(log); + if (devices.size() == 1) { + AppleDevice candidate = devices.get(0); + if (deviceId == null || deviceId.equals(candidate.hardwareProperties.udid)) { + // single device and it's a match + return candidate; + } + } else if (devices.size() > 1 && deviceId != null) { + // multiple devices connected but specified is there + AppleDevice candidate = devices.stream().filter(d -> deviceId.equals(d.hardwareProperties.udid)) + .findFirst().orElse(null); + if (candidate != null) + return candidate; + } + + String message; + if (devices.isEmpty()) message = "No devices connected"; + else if (deviceId != null) message = String.format("Required %s is not connected", deviceId); + else message = String.format("More than 1 device connected (%d)", devices.size()); + + if (retriesLeft > 0) { + retriesLeft -= 1; + log.info("Waiting for device: %s. (retry %d of %d)...", message, (retries - retriesLeft), retries); + //noinspection BusyWait + Thread.sleep(secondsBetweenRetries * 1000L); + } else throw new IllegalStateException(message); + } + } + + private void checkPreRequirements(AppleDevice device) throws Exception { + // check if developer mode is enabled + if (!device.deviceProperties.developerModeStatus) { + log.error("Developer mode is not enabled on device %s", device.hardwareProperties.udid); + log.error("https://developer.apple.com/documentation/xcode/enabling-developer-mode-on-a-device"); + throw new CompilerException("Developer mode is not enabled!"); + + } + + // check if paired + if (device.connectionProperties.pairingState != PairingState.PAIRED) { + log.info("Device is not pairing, trying to pair device %s", device.hardwareProperties.udid); + DeviceCtl.pairDevice(log, device.hardwareProperties.udid); + } + + } + + private String getDeviceTunnelAddress(AppleDevice device) throws Exception { + // refresh device information + log.info("Retrieving tunnel ipv6 for device %s", device.hardwareProperties.udid); + AppleDevice updatedInfo = DeviceCtl.getDeviceInfo(log, device.hardwareProperties.udid); + if (updatedInfo.connectionProperties.tunnelState != TunnelState.CONNECTED || updatedInfo.connectionProperties.tunnelIPAddress == null) { + throw new CompilerException("Tunnel is not established !"); + } + return updatedInfo.connectionProperties.tunnelIPAddress; + } + + + + @Override + public Process execAsync() throws IOException { + // pick parameters and setup debugger before returning Process + String deviceId = launchParameters.getDeviceId(); + OutputStream outStream = System.out; + OutputStream errStream = System.err; + if (launchParameters.getStdoutFifo() != null) { + outStream = new OpenOnWriteFileOutputStream(launchParameters.getStdoutFifo()); + } + if (launchParameters.getStderrFifo() != null) { + errStream = new OpenOnWriteFileOutputStream(launchParameters.getStderrFifo()); + } + + // inject debugger related parameters if thre is request from the debugger + OutputStream outStreamFinal = setupDebuggerConnection(outStream); + OutputStream errStreamFinal = errStream; + List arguments = new ArrayList<>(launchParameters.getArguments(true)); + Map env = launchParameters.getEnvironment(); + + + this.launcherThread = new Thread("SimLauncherThread-" + threadCounter.getAndIncrement()) { + @Override + public void run() { + try { + // wait for device or fail + device = waitForDevice(deviceId); + + // check pre-requirements (paired status, dev mode) + checkPreRequirements(device); + + // deploying to device + log.info("Deploying app %s to device %s", appDir.getAbsolutePath(), device.deviceProperties.name); + DeviceCtl.install(log, device.hardwareProperties.udid, appDir.getAbsolutePath()); + + // launch + log.info("Launching app %s on device %s", appDir.getAbsolutePath(), device.deviceProperties.name); + DeviceCtl.launchAndWait(log, device.hardwareProperties.udid, bundleId, arguments, env, outStreamFinal, errStreamFinal); + + exitCode = 0; + } catch (ExecuteException e) { + exitCode = e.getExitValue(); + // if process is interrupted Apache Executor will use this constant, replace with 0 otherwise + // -559038737 looks odd in console output + if (exitCode == INVALID_EXITVALUE) + exitCode = 0; + } catch (Throwable t) { + log.error("AppLauncher failed with an exception:", t.getMessage()); + t.printStackTrace(new PrintStream(new ErrorOutputStream(log), true)); + } finally { + finished = true; + countDownLatch.countDown(); + } + } + }; + this.launcherThread.start(); + return this; + } + + @Override + public OutputStream getOutputStream() { + return new NullOutputStream(); + } + + @Override + public InputStream getInputStream() { + return waitInputStream; + } + + @Override + public InputStream getErrorStream() { + return waitInputStream; + } + + @Override + public int waitFor() throws InterruptedException { + countDownLatch.await(); + return exitCode; + } + + @Override + public int exitValue() { + if (!finished) { + throw new IllegalThreadStateException("Not terminated"); + } + return exitCode; + } + + @Override + public void destroy() { + try { + this.launcherThread.interrupt(); + this.launcherThread.join(); + } catch (InterruptedException ignored) { + } + } + + final InputStream waitInputStream = new InputStream() { + @Override + public int read() throws IOException { + try { + countDownLatch.await(); + } catch (InterruptedException e) { + throw new InterruptedIOException(); + } + return -1; + } + }; + + /** + * Setups debugger/hooks connection: + * 1. observes stdout for printed hook port + * 2. checks device tunnel ipv6 to establish connection to it + * if there is no request from debugger -- doesn't affect run parameters/output stream + */ + private OutputStream setupDebuggerConnection(OutputStream outputStream){ + IHooksConnectionUtils.DelegatingFuture requestFuture = launchParameters.getRequestForDebuggerConnection(); + if (requestFuture == null) return outputStream; + + // launching on device using devicectl, observe hooks port from its output + launchParameters.getArguments().add("-rvm:PrintDebugPort"); + + OutputHookPortObserverFuture observeOutputFeature = new OutputHookPortObserverFuture(); + class ObservingOutputStream extends FilterOutputStream { + public ObservingOutputStream() { + super(outputStream); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + observeOutputFeature.observeOutput(b, off, len); + super.write(b, off, len); + } + + @Override + public void write(int b) throws IOException { + observeOutputFeature.observeOutput(new byte[]{(byte) b}, 0, 1); + super.write(b); + } + } + OutputStream overvedOutputStream = new ObservingOutputStream(); + + Future connectionFuture = observeOutputFeature.thenApply(port -> { + // port captured from device, pick tunnel address + String tunnelAddress; + try { + tunnelAddress = getDeviceTunnelAddress(device); + } catch (Exception e) { + throw new CompilerException(e); + } + return new SocketHooksConnection(new InetSocketAddress(tunnelAddress, port)); + }); + requestFuture.setDelegate(connectionFuture); + + return overvedOutputStream; + } +} \ No newline at end of file diff --git a/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/devicectl/DeviceCtlParsers.java b/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/devicectl/DeviceCtlParsers.java new file mode 100644 index 000000000..e26d758e2 --- /dev/null +++ b/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/devicectl/DeviceCtlParsers.java @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2025 The MobiVM Contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.robovm.compiler.target.ios.devicectl; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.robovm.compiler.target.ios.devicectl.AppleDevice.Capability; +import org.robovm.compiler.target.ios.devicectl.AppleDevice.ConnectionProperties; +import org.robovm.compiler.target.ios.devicectl.AppleDevice.ConnectionProperties.AuthenticationType; +import org.robovm.compiler.target.ios.devicectl.AppleDevice.ConnectionProperties.PairingState; +import org.robovm.compiler.target.ios.devicectl.AppleDevice.ConnectionProperties.TransportType; +import org.robovm.compiler.target.ios.devicectl.AppleDevice.ConnectionProperties.TunnelState; +import org.robovm.compiler.target.ios.devicectl.AppleDevice.DeviceProperties; +import org.robovm.compiler.target.ios.devicectl.AppleDevice.HardwareProperties; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * parsers for output of `devicectl` utility + */ +public final class DeviceCtlParsers { + + /** + * parses array of capabilities + * [ + * { + * "featureIdentifier" : "com.apple.coredevice.feature.acquireusageassertion", + * "name" : "Acquire Usage Assertion" + * } + * ] + */ + public static Set parseCapabilities(@Required Stream stream) { + return stream.map(js -> js.get("featureIdentifier")) + .filter(s -> s instanceof String) + .map(s -> Capability.of((String)s)).collect(Collectors.toSet()); + } + + /** + * parses connection properties entry + * { + * "authenticationType" : "manualPairing", + * "pairingState" : "paired", + * "transportType" : "wired", + * "tunnelState" : "connected", + * "tunnelIPAddress" : "fdeb:b11:406e::1", + * } + */ + public static ConnectionProperties parseConnectionProperties(@Optional JSONObject json) { + AuthenticationType authenticationType = AuthenticationType.of(asString(json, "authenticationType")); + PairingState pairingState = PairingState.of(asString(json, "pairingState")); + TransportType transportType = TransportType.of(asString(json, "transportType")); + TunnelState tunnelState = TunnelState.of(asString(json, "tunnelState")); + String tunnelIPAddress = asString(json, "tunnelIPAddress"); + + return new ConnectionProperties(authenticationType, pairingState, transportType, tunnelState, tunnelIPAddress); + } + + /** + * parses device properties section + * { + * "bootState" : "booted", + * "developerModeStatus" : "enabled", + * "name" : "iPhone XR", + * "osBuildUpdate" : "22F76", + * "osVersionNumber" : "18.5", + * } + */ + public static DeviceProperties parseDeviceProperties(@Optional JSONObject json) { + return new DeviceProperties( + "booted".equals(asString(json, "bootState")), + "enabled".equals(asString(json, "developerModeStatus")), + asString(json, "name", "?"), + asString(json, "osBuildUpdate", "?"), + asString(json, "osVersionNumber", "0") + ); + } + + /** + * parses hardware properties section + * { + * "cpuType" : { + * "name" : "arm64e", + * }, + * "deviceType" : "iPhone", + * "ecid" : 123456, + * "hardwareModel" : "N841AP", + * "marketingName" : "iPhone XR", + * "platform" : "iOS", + * "productType" : "iPhone11,8", + * "serialNumber" : "1234567", + * "supportedCPUTypes" : [ + * { "name" : "arm64e" }, + * { "name" : "arm64" }, + * { "name" : "armv8"} + * ], + * "udid" : "00008020-000123123123123" + * } + */ + public static HardwareProperties parseHardwareProperties(@Optional JSONObject json) { + HardwareProperties.CPUType cpuType = HardwareProperties.CPUType.of( + asString(asJson(json, "cpuType"), "name") + ); + String deviceType = asString(json, "deviceType"); + long ecid = asLong(json, "ecid", 0L); + String hardwareModel = asString(json, "hardwareModel"); + String marketingName = asString(json, "marketingName"); + String platform = asString(json, "platform"); + String productType = asString(json, "productType"); + String serialNumber = asString(json, "serialNumber"); + Set supportedCPUTypes = asObjectStream(json, "supportedCPUTypes", Stream.empty()) + .map( js -> HardwareProperties.CPUType.of(asString(js, "name"))).collect(Collectors.toSet()); + String udid = asString(json, "udid"); + return new HardwareProperties( + cpuType, + deviceType, + ecid, + hardwareModel, + marketingName, + platform, + productType, + serialNumber, + supportedCPUTypes, + udid + ); + } + + /** + * Parses single device section + * { + * "capabilities" : [], + * "connectionProperties" : { }, + * "deviceProperties" : { }, + * "hardwareProperties" : { }, + * "identifier" : "AF319CBD-DC10-5AC0-815E-3774F8270D13", + * } + */ + public static AppleDevice parseAppleDevice(@Required JSONObject json) { + return new AppleDevice( + asString(json, "identifier"), + parseCapabilities(asObjectStream(json, "capabilities", Stream.empty())), + parseConnectionProperties(asJson(json, "connectionProperties")), + parseDeviceProperties(asJson(json, "deviceProperties")), + parseHardwareProperties(asJson(json, "hardwareProperties")) + ); + } + + /** + * parses `xcrun devicectl list devices -j @dest-file` response + * { + * "result" : { + * "devices" : [] + * } + * } + */ + public static List parseListResponse(@Required JSONObject json) { + JSONObject result = asJson(json, "result"); + return asObjectStream(result, "devices", Stream.empty()) + .map(DeviceCtlParsers::parseAppleDevice) + .filter(d -> d.deviceProperties.bootState) + .collect(Collectors.toList()); + } + + /** + * parses `xcrun devicectl device info details -d -j @dest-file` response + * { + * "result" : { + * } + * } + */ + public static AppleDevice parseDeviceInfoResponse(@Required JSONObject json) { + JSONObject result = asJson(json, "result"); + if (result == null) throw new IllegalStateException("Unexpected JSON response: result is missing!"); + return parseAppleDevice(result); + } + + // + // Internal annotations for visibility + // + @Retention(RetentionPolicy.SOURCE) + public @interface Required { + } + + @Retention(RetentionPolicy.SOURCE) + public @interface Optional { + } + + // + // JSON utilities bellow + // + + public static Stream toObjectStream(@Required JSONArray arr) { + return ((List) arr).stream() + .filter(o -> o instanceof JSONObject) + .map(o -> (JSONObject)o); + } + + public static Stream asObjectStream(@Optional JSONObject json, String key, Stream defaultValue) { + if (json == null) return defaultValue; + Object o = json.get(key); + if (o instanceof JSONArray) return toObjectStream((JSONArray) o); + return defaultValue; + } + + public static String asString(@Optional JSONObject json, String key, String defaultValue) { + if (json == null) return defaultValue; + Object o = json.get(key); + if (o instanceof String) return (String) o; + return defaultValue; + } + + public static String asString(@Optional JSONObject json, String key) { + return asString(json, key, null); + } + + public static JSONObject asJson(@Optional JSONObject json, String key) { + if (json == null) return null; + Object o = json.get(key); + if (o instanceof JSONObject) return (JSONObject) o; + return null; + } + + public static long asLong(@Optional JSONObject json, String key, long defaultValue) { + if (json == null) return defaultValue; + Object o = json.get(key); + if (o instanceof Number) return ((Number) o).longValue(); + return defaultValue; + } + +} diff --git a/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/devicectl/IOSDeviceCtlLaunchParameters.java b/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/devicectl/IOSDeviceCtlLaunchParameters.java new file mode 100755 index 000000000..83d4a523f --- /dev/null +++ b/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/devicectl/IOSDeviceCtlLaunchParameters.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2025 The MobiVM Contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */package org.robovm.compiler.target.ios.devicectl; + +import org.robovm.compiler.target.LaunchParameters; +import org.robovm.compiler.target.ios.IIOSLaunchParameters; +import org.robovm.compiler.target.ios.IOSTarget; + +/** + * {@link LaunchParameters} implementation used by {@link IOSTarget} when + * launching on device using `devicectl` tool + */ +public class IOSDeviceCtlLaunchParameters extends LaunchParameters implements IIOSLaunchParameters { + private String deviceId; + + public String getDeviceId() { + return deviceId; + } + public void setDeviceId(String deviceId) { + this.deviceId = deviceId != null && !deviceId.isEmpty() ? deviceId : null; + } +} diff --git a/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/devicectl/ValueEnumEntry.java b/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/devicectl/ValueEnumEntry.java new file mode 100644 index 000000000..b126384ec --- /dev/null +++ b/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/devicectl/ValueEnumEntry.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2025 The MobiVM Contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.robovm.compiler.target.ios.devicectl; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +/** + * Base class for "open enum-like types". + * Entries are being produced only if one with raw value was not produced before + * Used as alternative to enums when it is useful to get unknown value captured + */ +abstract class ValueEnumEntry { + public final T rawValue; + + protected ValueEnumEntry(T rawValue) { + this.rawValue = rawValue; + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + " " + (rawValue != null ? rawValue.toString() : "null"); + } + + @Override + public boolean equals(Object o) { + if (o == null || o.getClass() != this.getClass()) return false; + ValueEnumEntry that = (ValueEnumEntry) o; + return Objects.equals(rawValue, that.rawValue); + } + + @Override + public int hashCode() { + return Objects.hashCode(this.getClass()) * 31 + Objects.hashCode(rawValue); + } + + protected static class Producer> { + private final Map known = new HashMap<>(); + private final Function producer; + + public Producer(Function producer) { + this.producer = producer; + } + + public R of(T id) { + synchronized (known) { + return known.computeIfAbsent(id, producer); + } + } + } +} diff --git a/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/devicelib/AppLauncherProcess.java b/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/devicelib/AppLauncherProcess.java new file mode 100755 index 000000000..1d086562f --- /dev/null +++ b/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/devicelib/AppLauncherProcess.java @@ -0,0 +1,263 @@ +/* + * Copyright (C) 2013 RoboVM AB + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.robovm.compiler.target.ios.devicelib; + +import org.apache.commons.io.output.NullOutputStream; +import org.robovm.compiler.log.ErrorOutputStream; +import org.robovm.compiler.log.Logger; +import org.robovm.compiler.target.Launcher; +import org.robovm.compiler.util.ToolchainUtil; +import org.robovm.compiler.util.io.OpenOnWriteFileOutputStream; +import org.robovm.debugger.hooks.IHooksConnection; +import org.robovm.debugger.utils.IHooksConnectionUtils; +import org.robovm.debugger.utils.IHooksConnectionUtils.OutputHookPortObserverFuture; +import org.robovm.libimobiledevice.AfcClient; +import org.robovm.libimobiledevice.IDevice; +import org.robovm.libimobiledevice.IDeviceConnection; +import org.robovm.libimobiledevice.InstallationProxyClient; +import org.robovm.libimobiledevice.util.AppLauncher; +import org.robovm.libimobiledevice.util.AppLauncherCallback; + +import java.io.*; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * {@link Process} implementation which runs an app on a device using an + * {@link AppLauncher}. + */ +public class AppLauncherProcess extends Process implements Launcher { + private final AtomicInteger threadCounter = new AtomicInteger(); + private final Logger log; + private final File appDir; + private final IOSDeviceLaunchParameters launchParameters; + private final WaitInputStream in = new WaitInputStream(); + private final WaitInputStream err = new WaitInputStream(); + private final CountDownLatch countDownLatch = new CountDownLatch(1); + private Thread launcherThread; + private AppLauncher launcher; + private volatile boolean finished = false; + private volatile int exitCode = -1; + + public AppLauncherProcess(Logger log, File appDir, IOSDeviceLaunchParameters launchParameters) { + this.log = log; + this.appDir = appDir; + this.launchParameters = launchParameters; + } + + @Override + public Process execAsync() throws IOException { + this.launcherThread = new Thread("AppLauncherThread-" + threadCounter.getAndIncrement()) { + @Override + public void run() { + try { + // install and launch + exitCode = internalLaunch(); + } catch (Throwable t) { + log.error("AppLauncher failed with an exception:", t.getMessage()); + t.printStackTrace(new PrintStream(new ErrorOutputStream(log), true)); + } finally { + finished = true; + countDownLatch.countDown(); + } + } + }; + this.launcherThread.start(); + return this; + } + + private int internalLaunch() throws IOException { + String deviceUdid = launchParameters.getDeviceId(); + int forwardPort = launchParameters.getForwardPort(); + + // setup app launcher callback if debugger connection was requested + AppLauncherCallback callback = setupDebuggerConnection(); + + OutputStream outStream = System.out; + if (launchParameters.getStdoutFifo() != null) { + outStream = new OpenOnWriteFileOutputStream(launchParameters.getStdoutFifo()); + } + + Map env = launchParameters.getEnvironment(); + if (env == null) { + env = new HashMap<>(); + } + //Fix for #71, see http://stackoverflow.com/questions/37800790/hide-strange-unwanted-xcode-8-logs + env.put("OS_ACTIVITY_DT_MODE", ""); + + launcher = new AppLauncher(deviceUdid, appDir) { + protected void log(String s, Object... args) { + log.info(s, args); + } + }.stdout(outStream) + .closeOutOnExit(true) + .args(launchParameters.getArguments(true).toArray(new String[0])) + .env(env) + .forward(forwardPort) + .appLauncherCallback(callback) + .xcodePath(ToolchainUtil.findXcodePath()) + .uploadProgressCallback(new AfcClient.UploadProgressCallback() { + boolean first = true; + public void success() { + log.info("[100%%] Upload complete"); + } + public void progress(File path, int percentComplete) { + if (first) log.info("[ 0%%] Beginning upload..."); + first = false; + log.info("[%3d%%] Uploading %s...", percentComplete, path); + } + public void error(String message) {} + }) + .installStatusCallback(new InstallationProxyClient.StatusCallback() { + boolean first = true; + + public void success() { + log.info("[100%%] Install complete"); + } + + public void progress(String status, int percentComplete) { + if (first) log.info("[ 0%%] Beginning installation..."); + first = false; + log.info("[%3d%%] %s", percentComplete, status); + } + + public void error(String message) {} + }); + + return launcher.launch(); + } + + @Override + public OutputStream getOutputStream() { + return new NullOutputStream(); + } + + @Override + public InputStream getInputStream() { + return in; + } + + @Override + public InputStream getErrorStream() { + return err; + } + + @Override + public int waitFor() throws InterruptedException { + countDownLatch.await(); + return exitCode; + } + + @Override + public int exitValue() { + if (!finished) { + throw new IllegalThreadStateException("Not terminated"); + } + return exitCode; + } + + @Override + public void destroy() { + launcher.kill(); + } + + private class WaitInputStream extends InputStream { + + @Override + public int read() throws IOException { + try { + countDownLatch.await(); + } catch (InterruptedException e) { + throw new InterruptedIOException(); + } + return -1; + } + + } + + /** + * setups additional debug parameters, shall be called BEFORE arguments are extracted from launch params + */ + private AppLauncherCallback setupDebuggerConnection() { + IHooksConnectionUtils.DelegatingFuture requestFuture = launchParameters.getRequestForDebuggerConnection(); + if (requestFuture == null) return null; + + // launching on device using ilibmobiledevice, observe it output for port number + launchParameters.getArguments().add("-rvm:PrintDebugPort"); + + OutputHookPortObserverFuture observeOutputFeature = new OutputHookPortObserverFuture(); + class Impl implements AppLauncherCallback { + IDevice device = null; + @Override + public void setAppLaunchInfo(AppLauncherInfo info) { device = info.getDevice(); } + + @Override + public byte[] filterOutput(byte[] data) { + observeOutputFeature.observeOutput(data, 0, data.length); + return data; + } + } + Impl callback = new Impl(); + + // provide future to debugger + Future connectionFuture = observeOutputFeature + .thenApply(port -> new LibMobileDeviceHooksConnection(callback.device, port)); + requestFuture.setDelegate(connectionFuture); + + return callback; + } + + /** + * implements hooks connection to device over ILibMobileDevice + */ + private static class LibMobileDeviceHooksConnection implements IHooksConnection { + private IDeviceConnection deviceConnection; + private final IDevice device; + private final int hooksPort; + + public LibMobileDeviceHooksConnection(IDevice device, int hooksPort) { + this.device = device; + this.hooksPort = hooksPort; + } + + /** + * waits till port hooks port is available and establish connection + */ + @Override + public void connect() { + deviceConnection = device.connect(hooksPort); + } + + @Override + public void disconnect() { + deviceConnection.close(); + } + + @Override + public InputStream getInputStream() { + return deviceConnection.getInputStream(); + } + + @Override + public OutputStream getOutputStream() { + return deviceConnection.getOutputStream(); + } + } +} diff --git a/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/IOSDeviceLaunchParameters.java b/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/devicelib/IOSDeviceLaunchParameters.java similarity index 82% rename from compiler/compiler/src/main/java/org/robovm/compiler/target/ios/IOSDeviceLaunchParameters.java rename to compiler/compiler/src/main/java/org/robovm/compiler/target/ios/devicelib/IOSDeviceLaunchParameters.java index 5812a95c9..55ba21be3 100755 --- a/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/IOSDeviceLaunchParameters.java +++ b/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/devicelib/IOSDeviceLaunchParameters.java @@ -14,16 +14,18 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.robovm.compiler.target.ios; +package org.robovm.compiler.target.ios.devicelib; import org.robovm.compiler.target.LaunchParameters; +import org.robovm.compiler.target.ios.IIOSLaunchParameters; +import org.robovm.compiler.target.ios.IOSTarget; import org.robovm.libimobiledevice.util.AppLauncherCallback; /** * {@link LaunchParameters} implementation used by {@link IOSTarget} when * launching on device. Also used to receive the remote app path from a device. */ -public class IOSDeviceLaunchParameters extends LaunchParameters { +public class IOSDeviceLaunchParameters extends LaunchParameters implements IIOSLaunchParameters { private AppLauncherCallback appPathCallback; private String deviceId; private int forwardPort = -1; @@ -33,7 +35,7 @@ public String getDeviceId() { } public void setDeviceId(String deviceId) { - this.deviceId = deviceId; + this.deviceId = deviceId != null && !deviceId.isEmpty() ? deviceId : null; } public int getForwardPort() { @@ -43,12 +45,4 @@ public int getForwardPort() { public void setForwardPort(int forwardPort) { this.forwardPort = forwardPort; } - - public AppLauncherCallback getAppPathCallback() { - return appPathCallback; - } - - public void setAppLauncherCallback(AppLauncherCallback appPathCallback) { - this.appPathCallback = appPathCallback; - } } diff --git a/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/DeviceType.java b/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/simulator/DeviceType.java similarity index 61% rename from compiler/compiler/src/main/java/org/robovm/compiler/target/ios/DeviceType.java rename to compiler/compiler/src/main/java/org/robovm/compiler/target/ios/simulator/DeviceType.java index 9c46b8812..4a4b8dc36 100755 --- a/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/DeviceType.java +++ b/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/simulator/DeviceType.java @@ -14,25 +14,15 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.robovm.compiler.target.ios; +package org.robovm.compiler.target.ios.simulator; import org.apache.commons.exec.util.StringUtils; -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; -import org.json.simple.parser.JSONParser; import org.robovm.compiler.config.Arch; import org.robovm.compiler.config.CpuArch; -import org.robovm.compiler.config.Environment; -import org.robovm.compiler.log.Logger; -import org.robovm.compiler.util.Executor; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; /** @@ -40,13 +30,9 @@ * listed by xcrun simctl devices -j list. */ public class DeviceType implements Comparable { - public static final String IOS_VERSION_PREFIX = "com.apple.CoreSimulator.SimRuntime.iOS-"; public static final String PREFERRED_IPHONE_SIM_NAME = "iPhone SE"; public static final String PREFERRED_IPAD_SIM_NAME = "iPad Air"; - public static final String[] ONLY_32BIT_DEVICES = {"iPhone 4", "iPhone 4s", "iPhone 5", "iPhone 5c", "iPad 2"}; - public static final Version ARM64_IOS_VERSION = new Version(14, 0, 0); - public enum DeviceFamily { iPhone, iPad @@ -122,100 +108,8 @@ public String getState() { return state; } - /** - * @return fresh copy -- to receive fresh device state (and paired state) - */ - public DeviceType refresh() { - for (DeviceType t : listDeviceTypes()) { - if (udid.equals(t.udid)) - return t; - } - - return null; - } - public static List listDeviceTypes() { - try { - String capture = new Executor(Logger.NULL_LOGGER, "xcrun").args( - "simctl", "list", "devices", "pairs", "-j").execCapture(); - List types = new ArrayList<>(); - - JSONParser parser = new JSONParser(); - JSONObject root = (JSONObject) parser.parse(capture); - - // parse watch pairs to - Map pairs = new HashMap<>(); - JSONObject pairList = (JSONObject) root.get("pairs"); - if (pairList != null) { - for (Object e : pairList.values()) { - JSONObject entry = (JSONObject) e; - if (entry.containsKey("state") && entry.get("state").toString().contains("unavailable")) - continue; - JSONObject watchEntry = (JSONObject) entry.get("watch"); - JSONObject phoneEntry = (JSONObject) entry.get("phone"); - if (watchEntry != null && phoneEntry != null) { - String phoneUdid = phoneEntry.get("udid").toString(); - String watchUdid = watchEntry.get("udid").toString(); - String watchName = watchEntry.get("name").toString(); - String watchState = watchEntry.get("state").toString(); - if (watchState.contains("unavailable")) - continue; - DeviceType simpleWatch = new DeviceType(watchName, watchUdid, watchState, - new Version(0, 0, 0), Collections.emptySet(), null); - pairs.put(phoneUdid, simpleWatch); - } - } - } - - JSONObject deviceList = (JSONObject) root.get("devices"); - for (Object value : deviceList.entrySet()) { - //noinspection rawtypes - Map.Entry entry = (Map.Entry) value; - String versionKey = entry.getKey().toString(); - if (versionKey.startsWith(IOS_VERSION_PREFIX)) { - // com.apple.CoreSimulator.SimRuntime.iOS- - versionKey = versionKey.replace(IOS_VERSION_PREFIX, "").replace('-', '.'); - } else if (versionKey.startsWith("iOS ")) { - versionKey = versionKey.replace("iOS ", ""); - } else { - // not iOS - continue; - } - JSONArray devices = (JSONArray) entry.getValue(); - for (Object obj : devices) { - JSONObject device = (JSONObject) obj; - boolean isAvailable = false; - if (device.containsKey("isAvailable")) { - Object o = device.get("isAvailable"); - isAvailable = o instanceof Boolean ? (Boolean) o : "true".equals(o.toString()); - } else if (device.containsKey("availability")) - isAvailable = !device.get("availability").toString().contains("unavailable"); - - if (isAvailable) { - final String deviceName = device.get("name").toString(); - final Version version = Version.parse(versionKey); - Set archs = new HashSet<>(); - if (!Arrays.asList(ONLY_32BIT_DEVICES).contains(deviceName)) { - // This is assumption that on M1 ios versions starting from ios14 can run arm64 target - if (DEFAULT_HOST_ARCH == CpuArch.arm64 && version.isSameOrBetter(ARM64_IOS_VERSION)) - archs.add(new Arch(CpuArch.arm64, Environment.Simulator)); - archs.add(new Arch(CpuArch.x86_64, Environment.Simulator)); - } - - String udid = device.get("udid").toString(); - DeviceType watchPair = pairs.get(udid); - types.add(new DeviceType(deviceName, udid, device.get("state").toString(), version, archs, watchPair)); - } - } - } - - // Sort. Make sure that devices that have an id which is a prefix of - // another id comes before in the list. - Collections.sort(types); - return types; - } catch (Exception e) { - throw new RuntimeException(e); - } + return SimCtl.list(); } @Override @@ -252,14 +146,14 @@ private static List filter(List deviceTypes, Arch arch, public static List getSimpleDeviceTypeIds() { List result = new ArrayList<>(); - for (DeviceType type : listDeviceTypes()) { + for (DeviceType type : SimCtl.list()) { result.add(type.getSimpleDeviceTypeId()); } return result; } public static DeviceType getDeviceType(String displayName) { - List types = listDeviceTypes(); + List types = SimCtl.list(); if (displayName == null) { return null; } @@ -302,7 +196,7 @@ public static DeviceType getBestDeviceType(Arch arch, DeviceFamily family, DeviceType bestDefault = null; DeviceType bestAny = null; Version version = deviceVersion != null ? Version.parse(deviceVersion) : null; - List devices = filter(listDeviceTypes(), arch, family, deviceName, version); + List devices = filter(SimCtl.list(), arch, family, deviceName, version); for (DeviceType type : devices) { if (type.getDeviceName().equals(deviceName)) { // match for specified device diff --git a/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/IOSSimulatorLaunchParameters.java b/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/simulator/IOSSimulatorLaunchParameters.java similarity index 88% rename from compiler/compiler/src/main/java/org/robovm/compiler/target/ios/IOSSimulatorLaunchParameters.java rename to compiler/compiler/src/main/java/org/robovm/compiler/target/ios/simulator/IOSSimulatorLaunchParameters.java index 014be0b0e..ccac187dd 100755 --- a/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/IOSSimulatorLaunchParameters.java +++ b/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/simulator/IOSSimulatorLaunchParameters.java @@ -14,15 +14,17 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.robovm.compiler.target.ios; +package org.robovm.compiler.target.ios.simulator; import org.robovm.compiler.target.LaunchParameters; +import org.robovm.compiler.target.ios.IIOSLaunchParameters; +import org.robovm.compiler.target.ios.IOSTarget; /** * {@link LaunchParameters} implementation used by {@link IOSTarget} when * launching on the simulator. */ -public class IOSSimulatorLaunchParameters extends LaunchParameters { +public class IOSSimulatorLaunchParameters extends LaunchParameters implements IIOSLaunchParameters { private DeviceType deviceType; /// if specified will deploy and launch watch kit app as well diff --git a/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/simulator/SimCtl.java b/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/simulator/SimCtl.java new file mode 100644 index 000000000..9a67f14f6 --- /dev/null +++ b/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/simulator/SimCtl.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2025 The MobiVM Contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.robovm.compiler.target.ios.simulator; + +import org.apache.commons.exec.ExecuteException; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.robovm.compiler.log.Logger; +import org.robovm.compiler.util.Executor; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public final class SimCtl { + + + /** + * @return fresh copy -- to receive fresh device state (and paired state) + */ + public static DeviceType refresh(DeviceType orig) { + String udid = orig.getUdid(); + for (DeviceType t : list()) { + if (udid.equals(t.getUdid())) + return t; + } + + return null; + } + + public static List list() { + try { + String capture = new Executor(Logger.NULL_LOGGER, "xcrun").args( + "simctl", "list", "devices", "pairs", "-j").execCapture(); + + JSONParser parser = new JSONParser(); + JSONObject root = (JSONObject) parser.parse(capture); + return SimCtlParsers.parseListResponse(root); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static void boot(Logger log, String udid) throws IOException, ExecuteException { + Executor executor = new Executor(log, "xcrun"); + executor.args("simctl", "boot", udid); + executor.exec(); + } + + public static void show(Logger log, String udid) throws IOException, ExecuteException { + Executor executor = new Executor(log, "open"); + executor.args("-a", "Simulator", "--args", "-CurrentDeviceUDID", udid); + executor.exec(); + } + + public static void install(Logger log, String udid, String localPath) throws IOException, ExecuteException { + Executor executor = new Executor(log, "xcrun"); + executor.args("simctl", "install", udid, localPath); + executor.exec(); + } + + public static void launchAndWait( + Logger log, String udid, String bundleId, + List arguments, + Map env, + OutputStream errStream, + OutputStream outStream + ) throws IOException, ExecuteException { + Executor executor = new Executor(log, "xcrun"); + List args = new ArrayList<>(); + args.add("simctl"); + args.add("launch"); + args.add("--console-pty"); + args.add(udid); + args.add(bundleId); + args.addAll(arguments); + executor.args(args); + if (env != null && !env.isEmpty()) { + Map simEnv = new HashMap<>(); + for (Map.Entry entry : env.entrySet()) { + simEnv.put("SIMCTL_CHILD_" + entry.getKey(), entry.getValue()); + } + executor.env(simEnv); + } + executor.out(outStream).err(errStream).closeOutputStreams(true).inheritEnv(false); + executor.exec(); + } +} diff --git a/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/simulator/SimCtlParsers.java b/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/simulator/SimCtlParsers.java new file mode 100644 index 000000000..5aeaabafb --- /dev/null +++ b/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/simulator/SimCtlParsers.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2013 RoboVM AB + * Copyright (C) 2025 The MobiVM Contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.robovm.compiler.target.ios.simulator; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.robovm.compiler.config.Arch; +import org.robovm.compiler.config.CpuArch; +import org.robovm.compiler.config.Environment; + +import java.util.*; + +public class SimCtlParsers { + public static final String IOS_VERSION_PREFIX = "com.apple.CoreSimulator.SimRuntime.iOS-"; + public static final String[] ONLY_32BIT_DEVICES = {"iPhone 4", "iPhone 4s", "iPhone 5", "iPhone 5c", "iPad 2"}; + public static final DeviceType.Version ARM64_IOS_VERSION = new DeviceType.Version(14, 0, 0); + + public static List parseListResponse(JSONObject root) { + List types = new ArrayList<>(); + // parse watch pairs to + Map pairs = new HashMap<>(); + JSONObject pairList = (JSONObject) root.get("pairs"); + if (pairList != null) { + for (Object e : pairList.values()) { + JSONObject entry = (JSONObject) e; + if (entry.containsKey("state") && entry.get("state").toString().contains("unavailable")) + continue; + JSONObject watchEntry = (JSONObject) entry.get("watch"); + JSONObject phoneEntry = (JSONObject) entry.get("phone"); + if (watchEntry != null && phoneEntry != null) { + String phoneUdid = phoneEntry.get("udid").toString(); + String watchUdid = watchEntry.get("udid").toString(); + String watchName = watchEntry.get("name").toString(); + String watchState = watchEntry.get("state").toString(); + if (watchState.contains("unavailable")) + continue; + DeviceType simpleWatch = new DeviceType(watchName, watchUdid, watchState, + new DeviceType.Version(0, 0, 0), Collections.emptySet(), null); + pairs.put(phoneUdid, simpleWatch); + } + } + } + + JSONObject deviceList = (JSONObject) root.get("devices"); + for (Object value : deviceList.entrySet()) { + //noinspection rawtypes + Map.Entry entry = (Map.Entry) value; + String versionKey = entry.getKey().toString(); + if (versionKey.startsWith(IOS_VERSION_PREFIX)) { + // com.apple.CoreSimulator.SimRuntime.iOS- + versionKey = versionKey.replace(IOS_VERSION_PREFIX, "").replace('-', '.'); + } else if (versionKey.startsWith("iOS ")) { + versionKey = versionKey.replace("iOS ", ""); + } else { + // not iOS + continue; + } + JSONArray devices = (JSONArray) entry.getValue(); + for (Object obj : devices) { + JSONObject device = (JSONObject) obj; + boolean isAvailable = false; + if (device.containsKey("isAvailable")) { + Object o = device.get("isAvailable"); + isAvailable = o instanceof Boolean ? (Boolean) o : "true".equals(o.toString()); + } else if (device.containsKey("availability")) + isAvailable = !device.get("availability").toString().contains("unavailable"); + + if (isAvailable) { + final String deviceName = device.get("name").toString(); + final DeviceType.Version version = DeviceType.Version.parse(versionKey); + Set archs = new HashSet<>(); + if (!Arrays.asList(ONLY_32BIT_DEVICES).contains(deviceName)) { + // This is assumption that on M1 ios versions starting from ios14 can run arm64 target + if (DeviceType.DEFAULT_HOST_ARCH == CpuArch.arm64 && version.isSameOrBetter(ARM64_IOS_VERSION)) + archs.add(new Arch(CpuArch.arm64, Environment.Simulator)); + archs.add(new Arch(CpuArch.x86_64, Environment.Simulator)); + } + + String udid = device.get("udid").toString(); + DeviceType watchPair = pairs.get(udid); + types.add(new DeviceType(deviceName, udid, device.get("state").toString(), version, archs, watchPair)); + } + } + } + + // Sort. Make sure that devices that have an id which is a prefix of + // another id comes before in the list. + Collections.sort(types); + return types; + } +} diff --git a/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/SimLauncherProcess.java b/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/simulator/SimLauncherProcess.java similarity index 63% rename from compiler/compiler/src/main/java/org/robovm/compiler/target/ios/SimLauncherProcess.java rename to compiler/compiler/src/main/java/org/robovm/compiler/target/ios/simulator/SimLauncherProcess.java index 23a500a41..e43923076 100755 --- a/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/SimLauncherProcess.java +++ b/compiler/compiler/src/main/java/org/robovm/compiler/target/ios/simulator/SimLauncherProcess.java @@ -14,27 +14,25 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.robovm.compiler.target.ios; +package org.robovm.compiler.target.ios.simulator; import org.apache.commons.exec.ExecuteException; import org.apache.commons.io.output.NullOutputStream; +import org.robovm.compiler.CompilerException; import org.robovm.compiler.log.ErrorOutputStream; import org.robovm.compiler.log.Logger; import org.robovm.compiler.target.Launcher; -import org.robovm.compiler.util.Executor; import org.robovm.compiler.util.io.OpenOnWriteFileOutputStream; +import org.robovm.debugger.hooks.IHooksConnection; +import org.robovm.debugger.utils.IHooksConnectionUtils; +import org.robovm.debugger.utils.IHooksConnectionUtils.DelegatingFuture; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.InterruptedIOException; -import java.io.OutputStream; -import java.io.PrintStream; +import java.io.*; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicInteger; import static org.apache.commons.exec.Executor.INVALID_EXITVALUE; @@ -47,113 +45,82 @@ public class SimLauncherProcess extends Process implements Launcher { private final CountDownLatch countDownLatch = new CountDownLatch(1); private final AtomicInteger threadCounter = new AtomicInteger(); private final Logger log; - private final DeviceType deviceType; - private final String watchAppName; - private final File wd; private final String bundleId; private final File appDir; - private final List arguments; - private HashMap env; + private final IOSSimulatorLaunchParameters launchParameters; private Thread launcherThread; private volatile boolean finished = false; private volatile int exitCode = -1; - private OutputStream errStream; - private OutputStream outStream; + public SimLauncherProcess(Logger log, File appDir, String bundleId, IOSSimulatorLaunchParameters launchParameters) { this.log = log; - deviceType = launchParameters.getDeviceType(); - watchAppName = launchParameters.getPairedWatchAppName(); - wd = launchParameters.getWorkingDirectory(); this.appDir = appDir; this.bundleId = bundleId; - this.arguments = new ArrayList<>(launchParameters.getArguments(true)); - if (launchParameters.getEnvironment() != null) { - this.env = new HashMap<>(); - for (Map.Entry entry : launchParameters.getEnvironment().entrySet()) { - env.put("SIMCTL_CHILD_" + entry.getKey(), entry.getValue()); - } - } + this.launchParameters = launchParameters; + } + + @Override + public Process execAsync() throws IOException { + // provide debugger connection information if it was requested + // has to be done before argument list is built + setupDebuggerConnection(); + + DeviceType deviceType = launchParameters.getDeviceType(); + String watchAppName = launchParameters.getPairedWatchAppName(); + List arguments = new ArrayList<>(launchParameters.getArguments(true)); + Map env = launchParameters.getEnvironment(); - outStream = System.out; - errStream = System.err; + OutputStream outStream = System.out; + OutputStream errStream = System.err; if (launchParameters.getStdoutFifo() != null) { outStream = new OpenOnWriteFileOutputStream(launchParameters.getStdoutFifo()); } if (launchParameters.getStderrFifo() != null) { errStream = new OpenOnWriteFileOutputStream(launchParameters.getStderrFifo()); } - } + OutputStream outStreamFinal = outStream; + OutputStream errStreamFinal = errStream; - @Override - public Process execAsync() throws IOException { this.launcherThread = new Thread("SimLauncherThread-" + threadCounter.getAndIncrement()) { @Override public void run() { try { - Executor executor; - DeviceType freshState = deviceType.refresh(); - if (freshState != null && "shutdown".equals(freshState.getState().toLowerCase())) { + DeviceType freshState = SimCtl.refresh(deviceType); + if (freshState != null && "shutdown".equalsIgnoreCase(freshState.getState())) { log.info("Booting simulator %s", deviceType.getUdid()); - executor = new Executor(log, "xcrun"); - executor.args("simctl", "boot", deviceType.getUdid()); - executor.exec(); + SimCtl.boot(log, deviceType.getUdid()); } // bringing simulator to front (and showing it if it was just booted) log.info("Showing simulator %s", deviceType.getUdid()); - executor = new Executor(log, "open"); - executor.args("-a", "Simulator", "--args", "-CurrentDeviceUDID", deviceType.getUdid()); - executor.exec(); + SimCtl.show(log, deviceType.getUdid()); log.info("Deploying app %s to simulator %s", appDir.getAbsolutePath(), deviceType.getUdid()); - executor = new Executor(log, "xcrun"); - executor.args("simctl", "install", deviceType.getUdid(), appDir.getAbsolutePath()); - executor.exec(); + SimCtl.install(log, deviceType.getUdid(), appDir.getAbsolutePath()); // launch and deploy to paired watch simulator if (watchAppName != null && freshState != null && freshState.getPair() != null) { DeviceType watchDeviceType = freshState.getPair(); - if ("shutdown".equals(watchDeviceType.getState().toLowerCase())) { + if ("shutdown".equalsIgnoreCase(watchDeviceType.getState())) { log.info("Booting watch simulator %s", watchDeviceType.getUdid()); - executor = new Executor(log, "xcrun"); - executor.args("simctl", "boot", watchDeviceType.getUdid()); - executor.exec(); + SimCtl.boot(log, watchDeviceType.getUdid()); } // bringing simulator to front (and showing it if it was just booted) log.info("Showing watch simulator %s", watchDeviceType.getUdid()); - executor = new Executor(log, "open"); - executor.args("-a", "Simulator", "--args", "-CurrentDeviceUDID", watchDeviceType.getUdid()); - executor.exec(); + SimCtl.show(log, watchDeviceType.getUdid()); File watchAppDir = new File(appDir, "Watch/" + watchAppName); log.info("Deploying app %s to watch simulator %s", watchAppDir.getAbsolutePath(), watchDeviceType.getUdid()); - executor = new Executor(log, "xcrun"); - executor.args("simctl", "install", watchDeviceType.getUdid(), watchAppDir.getAbsolutePath()); - executor.exec(); + SimCtl.install(log, watchDeviceType.getUdid(), watchAppDir.getAbsolutePath()); } log.info("Launching app %s on simulator %s", appDir.getAbsolutePath(), deviceType.getUdid()); - executor = new Executor(log, "xcrun"); - List args = new ArrayList<>(); - args.add("simctl"); - args.add("launch"); - args.add("--console-pty"); - args.add(deviceType.getUdid()); - args.add(bundleId); - args.addAll(arguments); - executor.args(args); - - if (env != null) { - executor.env(env); - } - - executor.wd(wd).out(outStream).err(errStream).closeOutputStreams(true).inheritEnv(false); - executor.exec(); + SimCtl.launchAndWait(log, deviceType.getUdid(), bundleId, arguments, env, outStreamFinal, errStreamFinal); exitCode = 0; } catch (ExecuteException e) { exitCode = e.getExitValue(); @@ -223,4 +190,26 @@ public int read() throws IOException { return -1; } }; + + /** + * setups additional debug parameters, shall be called BEFORE arguments are extracted from launch params + */ + private void setupDebuggerConnection() { + DelegatingFuture requestFuture = launchParameters.getRequestForDebuggerConnection(); + if (requestFuture == null) return; + + // launching on simulator, it can write down port number to file on local system + File hooksPortFile; + try { + hooksPortFile = File.createTempFile("robovm-dbg-sim", ".port"); + } catch (IOException e) { + throw new CompilerException("Failed to create simulator debugger port file", e); + } + launchParameters.getArguments().add("-rvm:PrintDebugPort=" + hooksPortFile.getAbsolutePath()); + + // provide future to debugger + Future connectionFuture = IHooksConnectionUtils.waitForPortFromFile(hooksPortFile) + .thenApply(IHooksConnectionUtils.SocketHooksConnection::new); + requestFuture.setDelegate(connectionFuture); + } } diff --git a/compiler/compiler/src/main/java/org/robovm/compiler/util/Executor.java b/compiler/compiler/src/main/java/org/robovm/compiler/util/Executor.java index f1e628ada..17c264ae5 100755 --- a/compiler/compiler/src/main/java/org/robovm/compiler/util/Executor.java +++ b/compiler/compiler/src/main/java/org/robovm/compiler/util/Executor.java @@ -44,7 +44,7 @@ * Builder style wrapper around commons-exec which also adds support for asynchronous * execution. */ -public class Executor implements Launcher { +public class Executor { private final String cmd; private final Logger logger; private List args = new ArrayList(); @@ -132,7 +132,8 @@ public Executor env(Map env) { /** * Adds a single environment variable. * - * @param env the environment variables. + * @param name the name of environment variable + * @param value the value of environment variable * @return this {@link Executor}. */ public Executor addEnv(String name, String value) { diff --git a/compiler/compiler/src/test/java/org/robovm/compiler/config/ConfigTest.java b/compiler/compiler/src/test/java/org/robovm/compiler/config/ConfigTest.java index 0f5cf7434..8cf57ac57 100755 --- a/compiler/compiler/src/test/java/org/robovm/compiler/config/ConfigTest.java +++ b/compiler/compiler/src/test/java/org/robovm/compiler/config/ConfigTest.java @@ -24,7 +24,7 @@ import org.robovm.compiler.config.Config.Builder; import org.robovm.compiler.config.Config.Home; import org.robovm.compiler.config.Config.Lib; -import org.robovm.compiler.target.ConsoleTarget; +import org.robovm.compiler.target.console.ConsoleTarget; import org.robovm.compiler.target.ios.IOSTarget; import org.zeroturnaround.zip.ZipUtil; diff --git a/compiler/vm/core/src/init.c b/compiler/vm/core/src/init.c index 6adca2fe3..f769c0f4f 100755 --- a/compiler/vm/core/src/init.c +++ b/compiler/vm/core/src/init.c @@ -247,8 +247,12 @@ jboolean rvmInitOptions(int argc, char* argv[], Options* options, jboolean ignor if (argc > 0) { jint firstJavaArg = 1; + jint skippedDash = 0; for (jint i = 1; i < argc; i++) { - if (startsWith(argv[i], "-rvm:")) { + if (i == 1 && strcmp("--", argv[i]) == 0) { + // dkimitsa: workaround for devicectl bug, passing -- as first param + skippedDash = 1; + } else if (startsWith(argv[i], "-rvm:")) { if (!ignoreRvmArgs) { char* arg = &argv[i][5]; rvmParseOption(arg, options); @@ -258,6 +262,10 @@ jboolean rvmInitOptions(int argc, char* argv[], Options* options, jboolean ignor break; } } + if (firstJavaArg > 1) { + // consider skippedDash workaround only in case there was any "-rvm:" + firstJavaArg += skippedDash; + } options->commandLineArgs = NULL; options->commandLineArgsCount = argc - firstJavaArg; diff --git a/plugins/debugger/src/main/java/org/robovm/debugger/Debugger.java b/plugins/debugger/src/main/java/org/robovm/debugger/Debugger.java index 77fcf627e..4d3048d17 100644 --- a/plugins/debugger/src/main/java/org/robovm/debugger/Debugger.java +++ b/plugins/debugger/src/main/java/org/robovm/debugger/Debugger.java @@ -83,7 +83,7 @@ public Debugger(Process process, DebuggerConfig config) { this.delegates = new AllDelegates(this, state); this.jdwpServer = new JdwpDebugServer(delegates, this, config.jdwpClienMode(), config.jdwpPort()) ; - this.hooksChannel = new HooksChannel(delegates, !config.arch().is32Bit(), config.hooksConnection(), this); + this.hooksChannel = new HooksChannel(delegates, !config.arch().is32Bit(), config.getHooksConnectionPromise(), this); } diff --git a/plugins/debugger/src/main/java/org/robovm/debugger/DebuggerConfig.java b/plugins/debugger/src/main/java/org/robovm/debugger/DebuggerConfig.java index 55354227e..1ddbb6fa3 100644 --- a/plugins/debugger/src/main/java/org/robovm/debugger/DebuggerConfig.java +++ b/plugins/debugger/src/main/java/org/robovm/debugger/DebuggerConfig.java @@ -15,19 +15,18 @@ */ package org.robovm.debugger; -import org.robovm.debugger.hooks.HooksChannel; import org.robovm.debugger.hooks.IHooksConnection; +import org.robovm.debugger.utils.IHooksConnectionUtils; +import org.robovm.debugger.utils.IHooksConnectionUtils.SocketHooksConnection; import org.robovm.debugger.utils.macho.MachOConsts; import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.IOException; import java.io.PrintStream; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.util.ArrayList; import java.util.List; -import java.util.function.IntSupplier; +import java.util.concurrent.Future; /** * @author Demyan Kimitsas @@ -75,7 +74,7 @@ public int getMachoValue() { private boolean jdwpClienMode; private int jdwpPort = -1; private boolean standalone; - private IHooksConnection hooksConnection; + private Future hooksConnectionPromise; private DebuggerConfig() { } @@ -108,8 +107,8 @@ public int jdwpPort() { return jdwpPort; } - public IHooksConnection hooksConnection() { - return hooksConnection; + public Future getHooksConnectionPromise() { + return hooksConnectionPromise; } public boolean isStandalone() { @@ -120,7 +119,7 @@ public static class Builder { private final DebuggerConfig config = new DebuggerConfig(); public DebuggerConfig build() { - if (config.arch == null || config.appfile == null || config.jdwpPort < 0 || config.hooksConnection == null) + if (config.arch == null || config.appfile == null || config.jdwpPort < 0 || config.hooksConnectionPromise == null) throw new DebuggerException("Missing required parameters in config"); return config; } @@ -153,30 +152,8 @@ public void setJdwpPort(int jdwpPort) { config.jdwpPort = jdwpPort; } - public void setHooksPortFile(File portFile) { - IntSupplier portSupplier = () -> { - try { - long ts = System.currentTimeMillis(); - while (!portFile.exists() || portFile.length() == 0) { - if (System.currentTimeMillis() - ts > DebuggerConfig.TARGET_WAIT_TIMEOUT) - throw new DebuggerException("Timeout while waiting simulator port file"); - Thread.sleep(200); - } - return Integer.parseInt(new String(Files.readAllBytes(portFile.toPath()))); - } catch (InterruptedException | IOException e) { - throw new DebuggerException(e); - } - }; - - config.hooksConnection = new HooksChannel.SocketHooksConnection(portSupplier); - } - - public void setHooksPort(int hooksPort) { - config.hooksConnection = new HooksChannel.SocketHooksConnection(() -> hooksPort); - } - - public void setHooksConnection(IHooksConnection conn) { - config.hooksConnection = conn; + public void setHooksConnectionPromise(Future hooksConnectionPromise) { + config.hooksConnectionPromise = hooksConnectionPromise; } private void setStandalone(boolean b) { @@ -248,10 +225,13 @@ public static DebuggerConfig fromCommandLine(String[] args) { builder.setLogToConsole(logToConsole); builder.setJdwpClienMode(jdwpClienMode); builder.setJdwpPort(jdwpPort); - if (hooksPortFile != null) - builder.setHooksPortFile(hooksPortFile); - else if (hooksPort != -1) - builder.setHooksPort(hooksPort); + if (hooksPortFile != null) { + builder.setHooksConnectionPromise( + IHooksConnectionUtils.waitForPortFromFile(hooksPortFile).thenApply(SocketHooksConnection::new) + ); + } else if (hooksPort != -1) { + builder.setHooksConnectionPromise(IHooksConnectionUtils.constantFuture(new SocketHooksConnection(hooksPort))); + } builder.setStandalone(true); return builder.build(); diff --git a/plugins/debugger/src/main/java/org/robovm/debugger/hooks/HooksChannel.java b/plugins/debugger/src/main/java/org/robovm/debugger/hooks/HooksChannel.java index ebbe7caba..0193b3e4e 100644 --- a/plugins/debugger/src/main/java/org/robovm/debugger/hooks/HooksChannel.java +++ b/plugins/debugger/src/main/java/org/robovm/debugger/hooks/HooksChannel.java @@ -16,15 +16,10 @@ package org.robovm.debugger.hooks; import org.robovm.debugger.DebuggerException; -import org.robovm.debugger.hooks.payloads.HooksCallStackEntry; -import org.robovm.debugger.hooks.payloads.HooksClassLoadedEventPayload; -import org.robovm.debugger.hooks.payloads.HooksCmdResponse; -import org.robovm.debugger.hooks.payloads.HooksEventPayload; -import org.robovm.debugger.hooks.payloads.HooksSuspendThreadPayload; -import org.robovm.debugger.hooks.payloads.HooksThreadEventPayload; -import org.robovm.debugger.hooks.payloads.HooksThreadStoppedEventPayload; +import org.robovm.debugger.hooks.payloads.*; import org.robovm.debugger.utils.DbgLogger; import org.robovm.debugger.utils.IDebuggerToolbox; +import org.robovm.debugger.utils.IHooksConnectionUtils.SocketHooksConnection; import org.robovm.debugger.utils.bytebuffer.DataBufferReader; import org.robovm.debugger.utils.bytebuffer.DataBufferReaderWriter; import org.robovm.debugger.utils.bytebuffer.DataByteBufferWriter; @@ -33,13 +28,11 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.net.InetSocketAddress; -import java.net.Socket; import java.nio.ByteOrder; import java.nio.file.Files; import java.util.HashMap; import java.util.Map; -import java.util.function.IntSupplier; +import java.util.concurrent.Future; /** * @author Demyan Kimitsa @@ -50,14 +43,15 @@ public class HooksChannel implements IHooksApi { private final static int DEFAULT_TIMEOUT = 5000; private final Thread socketThread; private final boolean is64bit; + private final Future hooksConnectionPromise; private IHooksConnection hooksConnection; private long reqIdCounter = 100; private final Map requestsInProgress = new HashMap<>(); private final DataBufferReaderWriter headerBuffer; private final IHooksEventsHandler eventsHandler; - public HooksChannel(IDebuggerToolbox toolbox, boolean is64bit, IHooksConnection connection, IHooksEventsHandler eventsHandler) { - this.hooksConnection = connection; + public HooksChannel(IDebuggerToolbox toolbox, boolean is64bit, Future hooksConnectionPromise, IHooksEventsHandler eventsHandler) { + this.hooksConnectionPromise = hooksConnectionPromise; this.is64bit = is64bit; this.eventsHandler = eventsHandler; this.socketThread = toolbox.createThread(this::doSocketWork, "HooksChannel socket thread"); @@ -84,6 +78,7 @@ public void shutdown() { private void doSocketWork() { // establish connection try { + hooksConnection = hooksConnectionPromise.get(); hooksConnection.connect(); InputStream inputStream = hooksConnection.getInputStream(); OutputStream outputStream = hooksConnection.getOutputStream(); @@ -513,42 +508,6 @@ private boolean isEvent(byte cmd) { } - /** - * Connection for socket case (local host simulator) - */ - public static class SocketHooksConnection implements IHooksConnection { - private final IntSupplier hooksPortSupplier; - private Socket socket; - - public SocketHooksConnection(IntSupplier hooksPortSupplier) { - this.hooksPortSupplier = hooksPortSupplier; - } - - @Override - public void connect() throws IOException { - int port = hooksPortSupplier.getAsInt(); - socket = new Socket(); - socket.connect(new InetSocketAddress("127.0.0.1", port), 1000); - socket.setTcpNoDelay(true); - } - - @Override - public void disconnect() throws IOException { - if (socket != null && socket.isClosed()) - socket.close(); - } - - @Override - public InputStream getInputStream() throws IOException { - return socket.getInputStream(); - } - - @Override - public OutputStream getOutputStream() throws IOException { - return socket.getOutputStream(); - } - } - public static void main(String[] argv) { int port; @@ -569,7 +528,8 @@ public static void main(String[] argv) { } IDebuggerToolbox toolbox = Thread::new; - final HooksChannel hooksChannel = new HooksChannel(toolbox, true, new SocketHooksConnection(() -> port), + final Future promise = SocketHooksConnection.constantFuture(port); + final HooksChannel hooksChannel = new HooksChannel(toolbox, true, promise, new IHooksEventsHandler() { @Override public void onHooksTargetAttached(IHooksApi api, long robovmBaseSymbol) { diff --git a/plugins/debugger/src/main/java/org/robovm/debugger/utils/IHooksConnectionUtils.java b/plugins/debugger/src/main/java/org/robovm/debugger/utils/IHooksConnectionUtils.java new file mode 100644 index 000000000..4639c7a0d --- /dev/null +++ b/plugins/debugger/src/main/java/org/robovm/debugger/utils/IHooksConnectionUtils.java @@ -0,0 +1,350 @@ +/* + * Copyright (C) 2025 The MobiVM Contributors + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.robovm.debugger.utils; + +import org.robovm.debugger.DebuggerException; +import org.robovm.debugger.hooks.IHooksConnection; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.concurrent.*; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Utilities for getting IHooksConnection: + */ +public final class IHooksConnectionUtils { + private IHooksConnectionUtils() { + } + + /** + * @return constants Feature for already resolved connection + */ + public static Future constantFuture(IHooksConnection resolved) { + CompletableFuture f = new CompletableFuture<>(); + f.complete(resolved); + return f; + } + + + /** + * Connection for socket case (simulator/ios device over tunnel) + */ + public static class SocketHooksConnection implements IHooksConnection { + private final SocketAddress socketAddress; + private Socket socket; + + public SocketHooksConnection(SocketAddress socketAddress) { + this.socketAddress = socketAddress; + } + + /** + * Connection to local host at specific port number + */ + public SocketHooksConnection(int port) { + this.socketAddress = new InetSocketAddress("127.0.0.1", port); + } + + @Override + public void connect() throws IOException { + socket = new Socket(); + socket.connect(socketAddress, 1000); + socket.setTcpNoDelay(true); + } + + @Override + public void disconnect() throws IOException { + if (socket != null && socket.isClosed()) + socket.close(); + } + + @Override + public InputStream getInputStream() throws IOException { + return socket.getInputStream(); + } + + @Override + public OutputStream getOutputStream() throws IOException { + return socket.getOutputStream(); + } + + public static Future constantFuture(SocketAddress address) { + return IHooksConnectionUtils.constantFuture(new SocketHooksConnection(address)); + } + + public static Future constantFuture(int port) { + return IHooksConnectionUtils.constantFuture(new SocketHooksConnection(port)); + } + } + + + /** + * Feature that waits for file with port number to appear and then completes + */ + public static PollingFuture waitForPortFromFile(File portFile) { + return new PollingFuture<>(() -> portFromFile(portFile)); + } + + /** + * tries to read port number from file, if file doesn't exist -- returns null + */ + public static Integer portFromFile(File portFile) { + if (portFile.exists() && portFile.length() != 0) { + try { + return Integer.parseInt(new String(Files.readAllBytes(portFile.toPath()))); + } catch (IOException e) { + throw new DebuggerException(e); + } + } + + return null; + } + + /** + * Completable Future that observe Output for "hooks: debugPort=" line to capture port number from it + */ + public static class OutputHookPortObserverFuture extends CompletableFuture { + private String incompleteLine; + private final static String tag = "[DEBUG] hooks: debugPort="; + + public void observeOutput(byte[] data, int offset, int length) { + if (!isDone()) { + // port is not received yet, keep working + String str = new String(data, offset, length, StandardCharsets.UTF_8); + if (incompleteLine != null) { + str = incompleteLine + str; + incompleteLine = null; + } + + int lookingPos = 0; + int newLineIdx = str.indexOf('\n'); + while (newLineIdx >= 0) { + // get next new line + if (str.startsWith(tag, lookingPos)) { + // got it + this.complete(Integer.parseInt(str.substring(lookingPos + tag.length(), newLineIdx).trim())); + break; + } else { + // move to next line + lookingPos = newLineIdx + 1; + newLineIdx = str.indexOf('\n', newLineIdx + 1); + } + } + + // keep trailing line (without eol) + if (!isDone() && lookingPos < str.length()) { + incompleteLine = lookingPos != 0 ? str.substring(lookingPos) : str; + } + } + } + } + + + /** + * Features that periodically polls supplier for value + * Useful for periodical check for value, e.g. file on disk + */ + public static class PollingFuture implements Future { + private final CompletableFuture internalFuture = new CompletableFuture<>(); + private final Long pollingTimeoutNano; + private final Supplier tryGet; + + PollingFuture(Long pollingTimeoutNano, Supplier tryGet) { + this.tryGet = tryGet; + this.pollingTimeoutNano = pollingTimeoutNano; + } + + PollingFuture(Supplier tryGet) { + this.tryGet = tryGet; + this.pollingTimeoutNano = TimeUnit.MILLISECONDS.toNanos(50); + } + + public ChainingFuture thenApply(Function fn) { + return new ChainingFuture<>(this, fn); + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return internalFuture.cancel(mayInterruptIfRunning); + } + + @Override + public boolean isCancelled() { + return internalFuture.isCancelled(); + } + + @Override + public boolean isDone() { + return internalFuture.isDone(); + } + + @Override + public T get() throws InterruptedException, ExecutionException { + try { + return get(Long.MAX_VALUE, TimeUnit.NANOSECONDS); + } catch (TimeoutException e) { + throw new ExecutionException(e); + } + } + + @Override + public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + long nanoTimeout = unit.toNanos(timeout); + long ts = System.currentTimeMillis(); + long deadLine = ts + nanoTimeout; + long remain = deadLine - ts; + while (remain > 0) { + synchronized (internalFuture) { + // check if complete by another thread + if (isDone()) return internalFuture.get(); + if (isCancelled()) throw new CancellationException(); + try { + T result = tryGet.get(); + if (result != null) { + internalFuture.complete(result); + return result; + } + } catch (Exception e) { + internalFuture.completeExceptionally(e); + throw e; + } + } + // use internal future for sleep: + // it allows to get canceled if cancel() is called + try { + T result = internalFuture.get(Long.min(remain, pollingTimeoutNano), TimeUnit.NANOSECONDS); + if (result != null) return result; + } catch (TimeoutException ignored) { + } + + ts = System.currentTimeMillis(); + remain = deadLine - ts; + } + if (isCancelled()) throw new CancellationException(); + throw new TimeoutException(); + } + } + + /** + * Wrapper around another Future. + * Used as request to provide a Future that will produce IHooksConnection + */ + public static class DelegatingFuture implements Future { + volatile Future delegate = null; + public void setDelegate(Future delegate) { + this.delegate = delegate; + } + private Future getDelegate() { + Future d = this.delegate; + if (d == null) throw new DebuggerException("Debugger support is not available!"); + return d; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return getDelegate().cancel(mayInterruptIfRunning); + } + + @Override + public boolean isCancelled() { + return getDelegate().isCancelled(); + } + + @Override + public boolean isDone() { + return getDelegate().isDone(); + } + + @Override + public T get() throws InterruptedException, ExecutionException { + return getDelegate().get(); + } + + @Override + public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + return getDelegate().get(timeout, unit); + } + } + + /** + * Wrapper around another Future with map functionality . + * will wait another future to complete and then map result + */ + public static class ChainingFuture implements Future { + private final Future dependency; + private final Function mutator; + private final CompletableFuture internalFuture = new CompletableFuture<>(); + + public ChainingFuture(Future dependency, Function mutator) { + this.dependency = dependency; + this.mutator = mutator; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return dependency.cancel(mayInterruptIfRunning); + } + + @Override + public boolean isCancelled() { + return dependency.isCancelled(); + } + + @Override + public boolean isDone() { + return internalFuture.isDone(); + } + + @Override + public U get() throws InterruptedException, ExecutionException { + synchronized (internalFuture) { + if (internalFuture.isDone()) return internalFuture.get(); + } + + T intermediate = dependency.get(); + synchronized (internalFuture) { + if (!internalFuture.isDone()) internalFuture.complete(mutator.apply(intermediate)); + return internalFuture.get(); + } + } + + @Override + public U get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + synchronized (internalFuture) { + if (internalFuture.isDone()) return internalFuture.get(); + } + + T intermediate = dependency.get(timeout, unit); + synchronized (internalFuture) { + if (!internalFuture.isDone()) internalFuture.complete(mutator.apply(intermediate)); + return internalFuture.get(); + } + } + + public ChainingFuture thenApply(Function fn) { + return new ChainingFuture<>(this, fn); + } + } +} diff --git a/plugins/gradle/src/main/java/org/robovm/gradle/tasks/AbstractIOSSimulatorTask.java b/plugins/gradle/src/main/java/org/robovm/gradle/tasks/AbstractIOSSimulatorTask.java index 6816c2759..f14e87e24 100755 --- a/plugins/gradle/src/main/java/org/robovm/gradle/tasks/AbstractIOSSimulatorTask.java +++ b/plugins/gradle/src/main/java/org/robovm/gradle/tasks/AbstractIOSSimulatorTask.java @@ -19,8 +19,8 @@ import org.robovm.compiler.config.CpuArch; import org.robovm.compiler.config.Environment; import org.robovm.compiler.config.OS; -import org.robovm.compiler.target.ios.DeviceType; import org.robovm.compiler.target.ios.IOSTarget; +import org.robovm.compiler.target.ios.simulator.DeviceType; /** * diff --git a/plugins/gradle/src/main/java/org/robovm/gradle/tasks/AbstractSimulatorTask.java b/plugins/gradle/src/main/java/org/robovm/gradle/tasks/AbstractSimulatorTask.java index 508841ce1..ee2a5b4ae 100755 --- a/plugins/gradle/src/main/java/org/robovm/gradle/tasks/AbstractSimulatorTask.java +++ b/plugins/gradle/src/main/java/org/robovm/gradle/tasks/AbstractSimulatorTask.java @@ -22,8 +22,8 @@ import org.robovm.compiler.config.Arch; import org.robovm.compiler.config.Config; import org.robovm.compiler.config.OS; -import org.robovm.compiler.target.ios.DeviceType; -import org.robovm.compiler.target.ios.IOSSimulatorLaunchParameters; +import org.robovm.compiler.target.ios.simulator.DeviceType; +import org.robovm.compiler.target.ios.simulator.IOSSimulatorLaunchParameters; import org.robovm.gradle.RoboVMGradleException; import java.io.File; diff --git a/plugins/gradle/src/main/java/org/robovm/gradle/tasks/ConsoleTask.java b/plugins/gradle/src/main/java/org/robovm/gradle/tasks/ConsoleTask.java index 3058873ef..3ee601e15 100755 --- a/plugins/gradle/src/main/java/org/robovm/gradle/tasks/ConsoleTask.java +++ b/plugins/gradle/src/main/java/org/robovm/gradle/tasks/ConsoleTask.java @@ -21,8 +21,8 @@ import org.robovm.compiler.config.Arch; import org.robovm.compiler.config.Config; import org.robovm.compiler.config.OS; -import org.robovm.compiler.target.ConsoleTarget; import org.robovm.compiler.target.LaunchParameters; +import org.robovm.compiler.target.console.ConsoleTarget; import org.robovm.gradle.RoboVMGradleException; import java.util.Arrays; diff --git a/plugins/gradle/src/main/java/org/robovm/gradle/tasks/IOSDeviceTask.java b/plugins/gradle/src/main/java/org/robovm/gradle/tasks/IOSDeviceTask.java index c867067b3..076fbcc26 100755 --- a/plugins/gradle/src/main/java/org/robovm/gradle/tasks/IOSDeviceTask.java +++ b/plugins/gradle/src/main/java/org/robovm/gradle/tasks/IOSDeviceTask.java @@ -21,8 +21,8 @@ import org.robovm.compiler.config.Arch; import org.robovm.compiler.config.Config; import org.robovm.compiler.config.OS; -import org.robovm.compiler.target.ios.IOSDeviceLaunchParameters; import org.robovm.compiler.target.ios.IOSTarget; +import org.robovm.compiler.target.ios.devicelib.IOSDeviceLaunchParameters; import org.robovm.gradle.RoboVMGradleException; import java.util.Arrays; diff --git a/plugins/gradle/src/main/java/org/robovm/gradle/tasks/IPadSimulatorTask.java b/plugins/gradle/src/main/java/org/robovm/gradle/tasks/IPadSimulatorTask.java index 0f2965ece..c1d0b67f3 100755 --- a/plugins/gradle/src/main/java/org/robovm/gradle/tasks/IPadSimulatorTask.java +++ b/plugins/gradle/src/main/java/org/robovm/gradle/tasks/IPadSimulatorTask.java @@ -15,7 +15,7 @@ */ package org.robovm.gradle.tasks; -import org.robovm.compiler.target.ios.DeviceType.DeviceFamily; +import org.robovm.compiler.target.ios.simulator.DeviceType.DeviceFamily; /** * diff --git a/plugins/gradle/src/main/java/org/robovm/gradle/tasks/IPhoneSimulatorTask.java b/plugins/gradle/src/main/java/org/robovm/gradle/tasks/IPhoneSimulatorTask.java index daaa588a9..1c39185f2 100755 --- a/plugins/gradle/src/main/java/org/robovm/gradle/tasks/IPhoneSimulatorTask.java +++ b/plugins/gradle/src/main/java/org/robovm/gradle/tasks/IPhoneSimulatorTask.java @@ -15,7 +15,7 @@ */ package org.robovm.gradle.tasks; -import org.robovm.compiler.target.ios.DeviceType.DeviceFamily; +import org.robovm.compiler.target.ios.simulator.DeviceType.DeviceFamily; /** * diff --git a/plugins/idea/src/main/java/org/robovm/idea/compilation/RoboVmCompileTask.java b/plugins/idea/src/main/java/org/robovm/idea/compilation/RoboVmCompileTask.java index 88fd0721d..c470c3aad 100644 --- a/plugins/idea/src/main/java/org/robovm/idea/compilation/RoboVmCompileTask.java +++ b/plugins/idea/src/main/java/org/robovm/idea/compilation/RoboVmCompileTask.java @@ -39,7 +39,7 @@ import org.robovm.compiler.config.Environment; import org.robovm.compiler.config.OS; import org.robovm.compiler.plugin.PluginArgument; -import org.robovm.compiler.target.ConsoleTarget; +import org.robovm.compiler.target.console.ConsoleTarget; import org.robovm.compiler.target.ios.IOSTarget; import org.robovm.compiler.target.ios.ProvisioningProfile; import org.robovm.compiler.target.ios.SigningIdentity; diff --git a/plugins/idea/src/main/java/org/robovm/idea/running/RoboVmConsoleRunConfigurationSettingsEditor.java b/plugins/idea/src/main/java/org/robovm/idea/running/RoboVmConsoleRunConfigurationSettingsEditor.java index 67bc62f3d..d03907e65 100755 --- a/plugins/idea/src/main/java/org/robovm/idea/running/RoboVmConsoleRunConfigurationSettingsEditor.java +++ b/plugins/idea/src/main/java/org/robovm/idea/running/RoboVmConsoleRunConfigurationSettingsEditor.java @@ -25,8 +25,8 @@ import com.intellij.openapi.vfs.VirtualFile; import org.jetbrains.annotations.NotNull; import org.robovm.compiler.config.CpuArch; -import org.robovm.compiler.target.ConsoleTarget; -import org.robovm.compiler.target.ios.DeviceType; +import org.robovm.compiler.target.console.ConsoleTarget; +import org.robovm.compiler.target.ios.simulator.DeviceType; import org.robovm.idea.RoboVmPlugin; import javax.swing.*; diff --git a/plugins/idea/src/main/java/org/robovm/idea/running/RoboVmIOSRunConfigurationSettingsEditor.java b/plugins/idea/src/main/java/org/robovm/idea/running/RoboVmIOSRunConfigurationSettingsEditor.java index 3435c5896..c217615fd 100755 --- a/plugins/idea/src/main/java/org/robovm/idea/running/RoboVmIOSRunConfigurationSettingsEditor.java +++ b/plugins/idea/src/main/java/org/robovm/idea/running/RoboVmIOSRunConfigurationSettingsEditor.java @@ -25,10 +25,11 @@ import org.robovm.compiler.config.Arch; import org.robovm.compiler.config.Config; import org.robovm.compiler.config.CpuArch; -import org.robovm.compiler.target.ios.DeviceType; +import org.robovm.compiler.target.ios.simulator.DeviceType; import org.robovm.compiler.target.ios.IOSTarget; import org.robovm.compiler.target.ios.ProvisioningProfile; import org.robovm.compiler.target.ios.SigningIdentity; +import org.robovm.compiler.target.ios.simulator.SimCtl; import org.robovm.compiler.util.InfoPList; import org.robovm.idea.RoboVmPlugin; import org.robovm.idea.running.RoboVmRunConfiguration.EntryType; @@ -280,7 +281,7 @@ private void populateDevices () { } private void populateSimulators() { - this.simDeviceTypes = DeviceType.listDeviceTypes().stream() + this.simDeviceTypes = SimCtl.list().stream() .map(SimTypeDecorator::new) .collect(Collectors.toList()); diff --git a/plugins/idea/src/main/java/org/robovm/idea/running/RoboVmRunConfiguration.java b/plugins/idea/src/main/java/org/robovm/idea/running/RoboVmRunConfiguration.java index c54717754..b0ac2e91c 100755 --- a/plugins/idea/src/main/java/org/robovm/idea/running/RoboVmRunConfiguration.java +++ b/plugins/idea/src/main/java/org/robovm/idea/running/RoboVmRunConfiguration.java @@ -35,10 +35,9 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.robovm.compiler.AppCompiler; -import org.robovm.compiler.config.Arch; import org.robovm.compiler.config.Config; import org.robovm.compiler.config.CpuArch; -import org.robovm.compiler.target.ios.DeviceType; +import org.robovm.compiler.target.ios.simulator.DeviceType; import org.robovm.idea.RoboVmPlugin; import java.util.Collection; diff --git a/plugins/idea/src/main/java/org/robovm/idea/running/RoboVmRunConfigurationUtils.java b/plugins/idea/src/main/java/org/robovm/idea/running/RoboVmRunConfigurationUtils.java index b739a503d..ab2268562 100755 --- a/plugins/idea/src/main/java/org/robovm/idea/running/RoboVmRunConfigurationUtils.java +++ b/plugins/idea/src/main/java/org/robovm/idea/running/RoboVmRunConfigurationUtils.java @@ -16,7 +16,7 @@ */ package org.robovm.idea.running; -import org.robovm.compiler.target.ios.DeviceType; +import org.robovm.compiler.target.ios.simulator.DeviceType; import org.robovm.compiler.target.ios.ProvisioningProfile; import org.robovm.compiler.target.ios.SigningIdentity; import org.robovm.idea.running.RoboVmRunConfiguration.EntryType; diff --git a/plugins/idea/src/main/java/org/robovm/idea/running/RoboVmRunProfileState.java b/plugins/idea/src/main/java/org/robovm/idea/running/RoboVmRunProfileState.java index d91c1ce65..f42aafc23 100755 --- a/plugins/idea/src/main/java/org/robovm/idea/running/RoboVmRunProfileState.java +++ b/plugins/idea/src/main/java/org/robovm/idea/running/RoboVmRunProfileState.java @@ -28,11 +28,12 @@ import org.jetbrains.annotations.NotNull; import org.robovm.compiler.AppCompiler; import org.robovm.compiler.config.Config; -import org.robovm.compiler.config.OS; import org.robovm.compiler.target.LaunchParameters; -import org.robovm.compiler.target.ios.DeviceType; -import org.robovm.compiler.target.ios.IOSDeviceLaunchParameters; -import org.robovm.compiler.target.ios.IOSSimulatorLaunchParameters; +import org.robovm.compiler.target.console.ConsoleLaunchParameters; +import org.robovm.compiler.target.ios.devicectl.IOSDeviceCtlLaunchParameters; +import org.robovm.compiler.target.ios.simulator.DeviceType; +import org.robovm.compiler.target.ios.devicelib.IOSDeviceLaunchParameters; +import org.robovm.compiler.target.ios.simulator.IOSSimulatorLaunchParameters; import org.robovm.compiler.util.io.Fifos; import org.robovm.compiler.util.io.OpenOnReadFileInputStream; import org.robovm.idea.RoboVmPlugin; @@ -92,24 +93,25 @@ protected void customizeLaunchParameters(RoboVmRunConfiguration runConfig, Confi launchParameters.setStdoutFifo(Fifos.mkfifo("stdout")); launchParameters.setStderrFifo(Fifos.mkfifo("stderr")); - if (config.getOs() != OS.ios) { + if (launchParameters instanceof ConsoleLaunchParameters) { if (runConfig.getWorkingDir() != null && !runConfig.getWorkingDir().isEmpty()) { launchParameters.setWorkingDirectory(new File(runConfig.getWorkingDir())); } - } else { - if (launchParameters instanceof IOSSimulatorLaunchParameters) { - IOSSimulatorLaunchParameters simParams = (IOSSimulatorLaunchParameters) launchParameters; - // finding exact simulator to run at - DeviceType exactType = RoboVmRunConfigurationUtils.getSimulator(runConfig); - if (exactType == null) - throw new ExecutionException("Simulator type is not set or is not available anymore!"); - simParams.setDeviceType(exactType); - simParams.setPairedWatchAppName(config.getWatchKitApp() != null && runConfig.simulatorLaunchWatch() - ? config.getWatchKitApp().getWatchAppName() : null); - } else if (launchParameters instanceof IOSDeviceLaunchParameters) { - IOSDeviceLaunchParameters deviceParams = (IOSDeviceLaunchParameters) launchParameters; - deviceParams.setDeviceId(runConfig.getTargetDeviceUDID()); - } + } else if (launchParameters instanceof IOSSimulatorLaunchParameters) { + IOSSimulatorLaunchParameters simParams = (IOSSimulatorLaunchParameters) launchParameters; + // finding exact simulator to run at + DeviceType exactType = RoboVmRunConfigurationUtils.getSimulator(runConfig); + if (exactType == null) + throw new ExecutionException("Simulator type is not set or is not available anymore!"); + simParams.setDeviceType(exactType); + simParams.setPairedWatchAppName(config.getWatchKitApp() != null && runConfig.simulatorLaunchWatch() + ? config.getWatchKitApp().getWatchAppName() : null); + } else if (launchParameters instanceof IOSDeviceLaunchParameters) { + IOSDeviceLaunchParameters deviceParams = (IOSDeviceLaunchParameters) launchParameters; + deviceParams.setDeviceId(runConfig.getTargetDeviceUDID()); + } else if (launchParameters instanceof IOSDeviceCtlLaunchParameters) { + IOSDeviceCtlLaunchParameters deviceParams = (IOSDeviceCtlLaunchParameters) launchParameters; + deviceParams.setDeviceId(runConfig.getTargetDeviceUDID()); } } diff --git a/plugins/junit/client/src/main/java/org/robovm/junit/client/TestClient.java b/plugins/junit/client/src/main/java/org/robovm/junit/client/TestClient.java index 0f491c9dd..f0fb9351c 100755 --- a/plugins/junit/client/src/main/java/org/robovm/junit/client/TestClient.java +++ b/plugins/junit/client/src/main/java/org/robovm/junit/client/TestClient.java @@ -237,13 +237,14 @@ public void call(Subscriber subscriber) { if (config.getTarget() instanceof IOSTarget && IOSTarget.isDeviceArch(config.getArch())) { // iOS device launch. Use libimobiledevice to set up the // connection. - IDevice device = ((IOSTarget) config.getTarget()).getDevice(); - config.getLogger().debug("Connecting to test server running on port %d " - + "on device with id %s", port, device.getUdid()); - try (IDeviceConnection conn = device.connect(port)) { - config.getLogger().debug("Connected to test server on device %s", device.getUdid()); - runTests(config, subscriber, conn.getInputStream(), conn.getOutputStream()); - } +// FIXME: temporaly commented out in sake of ios17+ deployment testing +// IDevice device = ((IOSTarget) config.getTarget()).getDevice(); +// config.getLogger().debug("Connecting to test server running on port %d " +// + "on device with id %s", port, device.getUdid()); +// try (IDeviceConnection conn = device.connect(port)) { +// config.getLogger().debug("Connected to test server on device %s", device.getUdid()); +// runTests(config, subscriber, conn.getInputStream(), conn.getOutputStream()); +// } } else { // Local launch. Use sockets. config.getLogger().debug("Connecting to test server running on localhost:%d", port); diff --git a/plugins/maven/plugin/src/main/java/org/robovm/maven/plugin/AbstractIOSSimulatorMojo.java b/plugins/maven/plugin/src/main/java/org/robovm/maven/plugin/AbstractIOSSimulatorMojo.java index 22ae5d38b..c4ed58150 100644 --- a/plugins/maven/plugin/src/main/java/org/robovm/maven/plugin/AbstractIOSSimulatorMojo.java +++ b/plugins/maven/plugin/src/main/java/org/robovm/maven/plugin/AbstractIOSSimulatorMojo.java @@ -23,10 +23,10 @@ import org.robovm.compiler.config.Config; import org.robovm.compiler.config.Environment; import org.robovm.compiler.config.OS; -import org.robovm.compiler.target.ios.DeviceType; -import org.robovm.compiler.target.ios.DeviceType.DeviceFamily; -import org.robovm.compiler.target.ios.IOSSimulatorLaunchParameters; import org.robovm.compiler.target.ios.IOSTarget; +import org.robovm.compiler.target.ios.simulator.DeviceType; +import org.robovm.compiler.target.ios.simulator.DeviceType.DeviceFamily; +import org.robovm.compiler.target.ios.simulator.IOSSimulatorLaunchParameters; public abstract class AbstractIOSSimulatorMojo extends AbstractRoboVMMojo { diff --git a/plugins/maven/plugin/src/main/java/org/robovm/maven/plugin/ConsoleMojo.java b/plugins/maven/plugin/src/main/java/org/robovm/maven/plugin/ConsoleMojo.java index b3bb755b5..f2e3ea004 100644 --- a/plugins/maven/plugin/src/main/java/org/robovm/maven/plugin/ConsoleMojo.java +++ b/plugins/maven/plugin/src/main/java/org/robovm/maven/plugin/ConsoleMojo.java @@ -23,10 +23,9 @@ import org.robovm.compiler.AppCompiler; import org.robovm.compiler.config.Arch; import org.robovm.compiler.config.Config; -import org.robovm.compiler.config.Environment; import org.robovm.compiler.config.OS; -import org.robovm.compiler.target.ConsoleTarget; import org.robovm.compiler.target.LaunchParameters; +import org.robovm.compiler.target.console.ConsoleTarget; /** * Compiles your application and runs it as a console application on the current diff --git a/plugins/maven/plugin/src/main/java/org/robovm/maven/plugin/IPadSimMojo.java b/plugins/maven/plugin/src/main/java/org/robovm/maven/plugin/IPadSimMojo.java index f1f2591aa..63b541551 100644 --- a/plugins/maven/plugin/src/main/java/org/robovm/maven/plugin/IPadSimMojo.java +++ b/plugins/maven/plugin/src/main/java/org/robovm/maven/plugin/IPadSimMojo.java @@ -18,7 +18,7 @@ import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.ResolutionScope; -import org.robovm.compiler.target.ios.DeviceType.DeviceFamily; +import org.robovm.compiler.target.ios.simulator.DeviceType.DeviceFamily; /** * Compiles your application and deploys it to the iPad simulator. diff --git a/plugins/maven/plugin/src/main/java/org/robovm/maven/plugin/IPhoneSimMojo.java b/plugins/maven/plugin/src/main/java/org/robovm/maven/plugin/IPhoneSimMojo.java index f467e93b4..ee50199fe 100644 --- a/plugins/maven/plugin/src/main/java/org/robovm/maven/plugin/IPhoneSimMojo.java +++ b/plugins/maven/plugin/src/main/java/org/robovm/maven/plugin/IPhoneSimMojo.java @@ -18,7 +18,7 @@ import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.ResolutionScope; -import org.robovm.compiler.target.ios.DeviceType.DeviceFamily; +import org.robovm.compiler.target.ios.simulator.DeviceType.DeviceFamily; /** * Compiles your application and deploys it to the iPhone simulator. diff --git a/plugins/maven/surefire/src/main/java/org/robovm/maven/surefire/RoboVMSurefireProvider.java b/plugins/maven/surefire/src/main/java/org/robovm/maven/surefire/RoboVMSurefireProvider.java index 7874cb1ba..aefd9294d 100644 --- a/plugins/maven/surefire/src/main/java/org/robovm/maven/surefire/RoboVMSurefireProvider.java +++ b/plugins/maven/surefire/src/main/java/org/robovm/maven/surefire/RoboVMSurefireProvider.java @@ -41,14 +41,13 @@ import org.robovm.compiler.config.Arch; import org.robovm.compiler.config.Config; import org.robovm.compiler.config.Config.Home; -import org.robovm.compiler.config.Environment; import org.robovm.compiler.config.OS; import org.robovm.compiler.log.Logger; import org.robovm.compiler.target.LaunchParameters; -import org.robovm.compiler.target.ios.DeviceType; -import org.robovm.compiler.target.ios.IOSSimulatorLaunchParameters; import org.robovm.compiler.target.ios.ProvisioningProfile; import org.robovm.compiler.target.ios.SigningIdentity; +import org.robovm.compiler.target.ios.simulator.DeviceType; +import org.robovm.compiler.target.ios.simulator.IOSSimulatorLaunchParameters; import org.robovm.junit.client.TestClient; import org.robovm.maven.resolver.RoboVMResolver; From 8ca1374e48a7fccc160d18b810da3603b79986b4 Mon Sep 17 00:00:00 2001 From: dkimitsa Date: Tue, 25 Nov 2025 10:55:12 +0200 Subject: [PATCH 2/2] * forgotten files --- .../eclipse/internal/ConsoleLaunchConfigurationDelegate.java | 2 +- .../internal/IOSSimulatorLaunchConfigurationDelegate.java | 4 ++-- .../internal/IOSSimulatorLaunchConfigurationTabGroup.java | 2 +- .../robovm/eclipse/internal/IOSSimulatorLaunchShortcut.java | 4 ++-- .../robovm/eclipse/internal/IPadSimulatorLaunchShortcut.java | 2 +- .../eclipse/internal/IPhoneSimulatorLaunchShortcut.java | 2 +- .../ui/src/org/robovm/eclipse/internal/NewProjectWizard.java | 2 +- .../ui/src/org/robovm/eclipse/internal/TemplateChooser.java | 2 +- .../junit/ConsoleJUnitLaunchConfigurationDelegate.java | 2 +- .../junit/IOSSimulatorJUnitLaunchConfigurationDelegate.java | 4 ++-- .../internal/junit/IOSSimulatorJUnitLaunchShortcut.java | 4 ++-- 11 files changed, 15 insertions(+), 15 deletions(-) diff --git a/plugins/eclipse/ui/src/org/robovm/eclipse/internal/ConsoleLaunchConfigurationDelegate.java b/plugins/eclipse/ui/src/org/robovm/eclipse/internal/ConsoleLaunchConfigurationDelegate.java index bf3bf57ff..d325a53f3 100755 --- a/plugins/eclipse/ui/src/org/robovm/eclipse/internal/ConsoleLaunchConfigurationDelegate.java +++ b/plugins/eclipse/ui/src/org/robovm/eclipse/internal/ConsoleLaunchConfigurationDelegate.java @@ -23,7 +23,7 @@ import org.robovm.compiler.config.Config; import org.robovm.compiler.config.Environment; import org.robovm.compiler.config.OS; -import org.robovm.compiler.target.ConsoleTarget; +import org.robovm.compiler.target.console.ConsoleTarget; import org.robovm.eclipse.RoboVMPlugin; /** diff --git a/plugins/eclipse/ui/src/org/robovm/eclipse/internal/IOSSimulatorLaunchConfigurationDelegate.java b/plugins/eclipse/ui/src/org/robovm/eclipse/internal/IOSSimulatorLaunchConfigurationDelegate.java index c0f38601e..07bdff824 100755 --- a/plugins/eclipse/ui/src/org/robovm/eclipse/internal/IOSSimulatorLaunchConfigurationDelegate.java +++ b/plugins/eclipse/ui/src/org/robovm/eclipse/internal/IOSSimulatorLaunchConfigurationDelegate.java @@ -26,8 +26,8 @@ import org.robovm.compiler.config.Environment; import org.robovm.compiler.config.OS; import org.robovm.compiler.target.LaunchParameters; -import org.robovm.compiler.target.ios.DeviceType; -import org.robovm.compiler.target.ios.IOSSimulatorLaunchParameters; +import org.robovm.compiler.target.ios.simulator.DeviceType; +import org.robovm.compiler.target.ios.simulator.IOSSimulatorLaunchParameters; import org.robovm.compiler.target.ios.IOSTarget; import org.robovm.eclipse.RoboVMPlugin; diff --git a/plugins/eclipse/ui/src/org/robovm/eclipse/internal/IOSSimulatorLaunchConfigurationTabGroup.java b/plugins/eclipse/ui/src/org/robovm/eclipse/internal/IOSSimulatorLaunchConfigurationTabGroup.java index a99a64f71..34a18fe7c 100755 --- a/plugins/eclipse/ui/src/org/robovm/eclipse/internal/IOSSimulatorLaunchConfigurationTabGroup.java +++ b/plugins/eclipse/ui/src/org/robovm/eclipse/internal/IOSSimulatorLaunchConfigurationTabGroup.java @@ -39,7 +39,7 @@ import org.eclipse.swt.widgets.Label; import org.robovm.compiler.config.Arch; import org.robovm.compiler.config.CpuArch; -import org.robovm.compiler.target.ios.DeviceType; +import org.robovm.compiler.target.ios.simulator.DeviceType; import org.robovm.eclipse.RoboVMPlugin; /** diff --git a/plugins/eclipse/ui/src/org/robovm/eclipse/internal/IOSSimulatorLaunchShortcut.java b/plugins/eclipse/ui/src/org/robovm/eclipse/internal/IOSSimulatorLaunchShortcut.java index 1d8a71071..6b4b330ae 100755 --- a/plugins/eclipse/ui/src/org/robovm/eclipse/internal/IOSSimulatorLaunchShortcut.java +++ b/plugins/eclipse/ui/src/org/robovm/eclipse/internal/IOSSimulatorLaunchShortcut.java @@ -22,8 +22,8 @@ import org.eclipse.core.runtime.CoreException; import org.eclipse.debug.core.ILaunchConfiguration; import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; -import org.robovm.compiler.target.ios.DeviceType; -import org.robovm.compiler.target.ios.DeviceType.DeviceFamily; +import org.robovm.compiler.target.ios.simulator.DeviceType; +import org.robovm.compiler.target.ios.simulator.DeviceType.DeviceFamily; /** * @author niklas diff --git a/plugins/eclipse/ui/src/org/robovm/eclipse/internal/IPadSimulatorLaunchShortcut.java b/plugins/eclipse/ui/src/org/robovm/eclipse/internal/IPadSimulatorLaunchShortcut.java index 5b4d8a450..1d349c02b 100755 --- a/plugins/eclipse/ui/src/org/robovm/eclipse/internal/IPadSimulatorLaunchShortcut.java +++ b/plugins/eclipse/ui/src/org/robovm/eclipse/internal/IPadSimulatorLaunchShortcut.java @@ -16,7 +16,7 @@ */ package org.robovm.eclipse.internal; -import org.robovm.compiler.target.ios.DeviceType.DeviceFamily; +import org.robovm.compiler.target.ios.simulator.DeviceType.DeviceFamily; /** * @author niklas diff --git a/plugins/eclipse/ui/src/org/robovm/eclipse/internal/IPhoneSimulatorLaunchShortcut.java b/plugins/eclipse/ui/src/org/robovm/eclipse/internal/IPhoneSimulatorLaunchShortcut.java index 8b194de60..c277194cd 100755 --- a/plugins/eclipse/ui/src/org/robovm/eclipse/internal/IPhoneSimulatorLaunchShortcut.java +++ b/plugins/eclipse/ui/src/org/robovm/eclipse/internal/IPhoneSimulatorLaunchShortcut.java @@ -16,7 +16,7 @@ */ package org.robovm.eclipse.internal; -import org.robovm.compiler.target.ios.DeviceType.DeviceFamily; +import org.robovm.compiler.target.ios.simulator.DeviceType.DeviceFamily; /** * @author niklas diff --git a/plugins/eclipse/ui/src/org/robovm/eclipse/internal/NewProjectWizard.java b/plugins/eclipse/ui/src/org/robovm/eclipse/internal/NewProjectWizard.java index 43ea6cc39..68479dbbe 100755 --- a/plugins/eclipse/ui/src/org/robovm/eclipse/internal/NewProjectWizard.java +++ b/plugins/eclipse/ui/src/org/robovm/eclipse/internal/NewProjectWizard.java @@ -40,7 +40,7 @@ import org.eclipse.ui.INewWizard; import org.eclipse.ui.IWorkbench; import org.osgi.service.prefs.BackingStoreException; -import org.robovm.compiler.target.ConsoleTarget; +import org.robovm.compiler.target.console.ConsoleTarget; import org.robovm.eclipse.RoboVMPlugin; import org.robovm.templater.Templater; diff --git a/plugins/eclipse/ui/src/org/robovm/eclipse/internal/TemplateChooser.java b/plugins/eclipse/ui/src/org/robovm/eclipse/internal/TemplateChooser.java index 39edbb0c8..bac9ce017 100755 --- a/plugins/eclipse/ui/src/org/robovm/eclipse/internal/TemplateChooser.java +++ b/plugins/eclipse/ui/src/org/robovm/eclipse/internal/TemplateChooser.java @@ -33,7 +33,7 @@ import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Group; import org.eclipse.swt.widgets.Label; -import org.robovm.compiler.target.ConsoleTarget; +import org.robovm.compiler.target.console.ConsoleTarget; import org.robovm.compiler.target.ios.IOSTarget; /** diff --git a/plugins/eclipse/ui/src/org/robovm/eclipse/internal/junit/ConsoleJUnitLaunchConfigurationDelegate.java b/plugins/eclipse/ui/src/org/robovm/eclipse/internal/junit/ConsoleJUnitLaunchConfigurationDelegate.java index ec90bedf0..f2013e138 100755 --- a/plugins/eclipse/ui/src/org/robovm/eclipse/internal/junit/ConsoleJUnitLaunchConfigurationDelegate.java +++ b/plugins/eclipse/ui/src/org/robovm/eclipse/internal/junit/ConsoleJUnitLaunchConfigurationDelegate.java @@ -24,7 +24,7 @@ import org.robovm.compiler.config.Config; import org.robovm.compiler.config.Environment; import org.robovm.compiler.config.OS; -import org.robovm.compiler.target.ConsoleTarget; +import org.robovm.compiler.target.console.ConsoleTarget; import org.robovm.eclipse.RoboVMPlugin; /** diff --git a/plugins/eclipse/ui/src/org/robovm/eclipse/internal/junit/IOSSimulatorJUnitLaunchConfigurationDelegate.java b/plugins/eclipse/ui/src/org/robovm/eclipse/internal/junit/IOSSimulatorJUnitLaunchConfigurationDelegate.java index a85656232..ee326beb4 100755 --- a/plugins/eclipse/ui/src/org/robovm/eclipse/internal/junit/IOSSimulatorJUnitLaunchConfigurationDelegate.java +++ b/plugins/eclipse/ui/src/org/robovm/eclipse/internal/junit/IOSSimulatorJUnitLaunchConfigurationDelegate.java @@ -26,8 +26,8 @@ import org.robovm.compiler.config.Environment; import org.robovm.compiler.config.OS; import org.robovm.compiler.target.LaunchParameters; -import org.robovm.compiler.target.ios.DeviceType; -import org.robovm.compiler.target.ios.IOSSimulatorLaunchParameters; +import org.robovm.compiler.target.ios.simulator.DeviceType; +import org.robovm.compiler.target.ios.simulator.IOSSimulatorLaunchParameters; import org.robovm.compiler.target.ios.IOSTarget; import org.robovm.eclipse.internal.IOSSimulatorLaunchConfigurationDelegate; diff --git a/plugins/eclipse/ui/src/org/robovm/eclipse/internal/junit/IOSSimulatorJUnitLaunchShortcut.java b/plugins/eclipse/ui/src/org/robovm/eclipse/internal/junit/IOSSimulatorJUnitLaunchShortcut.java index e4d60a347..88a2fcda0 100755 --- a/plugins/eclipse/ui/src/org/robovm/eclipse/internal/junit/IOSSimulatorJUnitLaunchShortcut.java +++ b/plugins/eclipse/ui/src/org/robovm/eclipse/internal/junit/IOSSimulatorJUnitLaunchShortcut.java @@ -17,8 +17,8 @@ package org.robovm.eclipse.internal.junit; import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; -import org.robovm.compiler.target.ios.DeviceType; -import org.robovm.compiler.target.ios.DeviceType.DeviceFamily; +import org.robovm.compiler.target.ios.simulator.DeviceType; +import org.robovm.compiler.target.ios.simulator.DeviceType.DeviceFamily; /** *