From 607033aea4675f943614329b233324a1d27d2af3 Mon Sep 17 00:00:00 2001 From: dkimitsa Date: Wed, 3 May 2023 10:16:25 +0300 Subject: [PATCH] * libmobiledevice maintenance: waiting for device and error handling reason: libmobiledevice was failing on every situation then: device is not connected, blocked or pairing not complete. considering build process and deployment might take minutes its sad to fail just due to blocked device. changes: mitigation for these cases done by allowing 20 seconds for user to act to solve the situation: - if no device connected -- waiting for device - if device is locked -- waiting for unlock - if pairing in progress -- waiting to complete other changes: - changed logic for picking dev image -- all version are parsed and picked exact or lower (fix for #517) - idea plugin: install was moved inside Run process. As before it was happening on UI thread and entire Idea was frozen (there was no update or locked device retries message); - using Java9 language level for RoboVM compiler/plugins. - amount for TODO added to point for code fragments that to be refactored (like Launchers, junit client depends on old structure) --- .../compiler/config/ResolvedLocations.java | 2 +- .../plugin/debug/DebuggerLaunchPlugin.java | 12 +- .../target/ios/AppLauncherProcess.java | 2 +- .../robovm/compiler/target/ios/IOSTarget.java | 30 +- .../libimobiledevice/LockdowndClient.java | 8 +- .../MobileImageMounterClient.java | 17 +- .../libimobiledevice/util/AppLauncher.java | 670 ++++++++++-------- .../util/AppLauncherCallback.java | 8 +- .../util/DeveloperImageResolver.java | 122 ++++ .../robovm/libimobiledevice/util/Lambdas.java | 21 + .../robovm/libimobiledevice/util/Version.java | 63 ++ ...t.java => DeveloperImageResolverTest.java} | 32 +- .../java/org/robovm/debugger/Debugger.java | 2 +- .../org/robovm/debugger/DebuggerConfig.java | 3 +- .../idea/running/RoboVmRunProfileState.java | 1 - pom.xml | 4 +- 16 files changed, 648 insertions(+), 349 deletions(-) create mode 100644 compiler/libimobiledevice/src/main/java/org/robovm/libimobiledevice/util/DeveloperImageResolver.java create mode 100644 compiler/libimobiledevice/src/main/java/org/robovm/libimobiledevice/util/Lambdas.java create mode 100644 compiler/libimobiledevice/src/main/java/org/robovm/libimobiledevice/util/Version.java rename compiler/libimobiledevice/src/test/java/org/robovm/libimobiledevice/util/{AppLauncherTest.java => DeveloperImageResolverTest.java} (60%) diff --git a/compiler/compiler/src/main/java/org/robovm/compiler/config/ResolvedLocations.java b/compiler/compiler/src/main/java/org/robovm/compiler/config/ResolvedLocations.java index 451389161..6e5e85828 100644 --- a/compiler/compiler/src/main/java/org/robovm/compiler/config/ResolvedLocations.java +++ b/compiler/compiler/src/main/java/org/robovm/compiler/config/ResolvedLocations.java @@ -94,7 +94,7 @@ ResolvedLocations resolve() { } if (xcFrameworks != null && !xcFrameworks.isEmpty()) { - // there is xcframeworks, expand them + // there are xcframeworks, expand them handleXCFrameworks(); } 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 a2faf834a..607a89921 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 @@ -252,10 +252,20 @@ public byte[] filterOutput(byte[] data) { @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 simulator port file"); + throw new DebuggerException("Timeout while waiting app is responding on device"); Thread.sleep(200); } 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 index 7a2481299..2e5b6c23f 100755 --- 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 @@ -59,11 +59,11 @@ public AppLauncherProcess(Logger log, AppLauncher launcher, LaunchParameters lau @Override public Process execAsync() throws IOException { - launcher.install(); 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()); 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 8c734c448..846ad9fc3 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 @@ -94,6 +94,7 @@ public class IOSTarget extends AbstractTarget { private File entitlementsPList; private SigningIdentity signIdentity; private ProvisioningProfile provisioningProfile; + @Deprecated private IDevice device; private File partialPListDir; @@ -175,21 +176,24 @@ private Launcher createIOSDevLauncher(LaunchParameters launchParameters) throws IOException { IOSDeviceLaunchParameters deviceLaunchParameters = (IOSDeviceLaunchParameters) launchParameters; - String deviceId = deviceLaunchParameters.getDeviceId(); + String deviceUdid = deviceLaunchParameters.getDeviceId(); int forwardPort = deviceLaunchParameters.getForwardPort(); - AppLauncherCallback callback = deviceLaunchParameters.getAppPathCallback(); - if (deviceId == null || deviceId.isEmpty()) { - String[] udids = IDevice.listUdids(); - if (udids.length == 0) { - throw new RuntimeException("No devices connected"); + + // 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); } - if (udids.length > 1) { - config.getLogger().warn("More than 1 device connected (%s). " - + "Using %s.", Arrays.asList(udids), udids[0]); + + @Override + public byte[] filterOutput(byte[] data) { + return delegate.filterOutput(data); } - deviceId = udids[0]; - } - device = new IDevice(deviceId); + } : null; OutputStream out = null; if (launchParameters.getStdoutFifo() != null) { @@ -205,7 +209,7 @@ private Launcher createIOSDevLauncher(LaunchParameters launchParameters) //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(device, getAppDir()) { + AppLauncher launcher = new AppLauncher(deviceUdid, getAppDir()) { protected void log(String s, Object... args) { config.getLogger().info(s, args); } diff --git a/compiler/libimobiledevice/src/main/java/org/robovm/libimobiledevice/LockdowndClient.java b/compiler/libimobiledevice/src/main/java/org/robovm/libimobiledevice/LockdowndClient.java index a966b609e..63b73a6eb 100755 --- a/compiler/libimobiledevice/src/main/java/org/robovm/libimobiledevice/LockdowndClient.java +++ b/compiler/libimobiledevice/src/main/java/org/robovm/libimobiledevice/LockdowndClient.java @@ -34,6 +34,7 @@ */ public class LockdowndClient implements AutoCloseable { protected LockdowndClientRef ref; + private IDevice device; LockdowndClient(LockdowndClientRef ref) { this.ref = ref; @@ -58,6 +59,7 @@ public LockdowndClient(IDevice device, String label, boolean handshake) { LibIMobileDevice.lockdownd_client_new_with_handshake(device.getRef(), refOut, label) : LibIMobileDevice.lockdownd_client_new(device.getRef(), refOut, label)); this.ref = refOut.getValue(); + this.device = device; } finally { refOut.delete(); } @@ -67,7 +69,11 @@ protected LockdowndClientRef getRef() { checkDisposed(); return ref; } - + + public IDevice getDevice() { + return device; + } + /** * Requests to start a service. * diff --git a/compiler/libimobiledevice/src/main/java/org/robovm/libimobiledevice/MobileImageMounterClient.java b/compiler/libimobiledevice/src/main/java/org/robovm/libimobiledevice/MobileImageMounterClient.java index 3c064e651..b0c5680ec 100755 --- a/compiler/libimobiledevice/src/main/java/org/robovm/libimobiledevice/MobileImageMounterClient.java +++ b/compiler/libimobiledevice/src/main/java/org/robovm/libimobiledevice/MobileImageMounterClient.java @@ -22,6 +22,7 @@ import java.nio.file.Files; import java.util.Arrays; +import com.dd.plist.NSString; import org.robovm.libimobiledevice.binding.LibIMobileDevice; import org.robovm.libimobiledevice.binding.LibIMobileDeviceConstants; import org.robovm.libimobiledevice.binding.LockdowndServiceDescriptorStruct; @@ -34,6 +35,9 @@ import com.dd.plist.NSDictionary; import com.dd.plist.NSObject; +import static org.robovm.libimobiledevice.binding.MobileImageMounterError.MOBILE_IMAGE_MOUNTER_E_DEVICE_LOCKED; +import static org.robovm.libimobiledevice.binding.MobileImageMounterError.MOBILE_IMAGE_MOUNTER_E_UNKNOWN_ERROR; + /** * Mounts developer/debug disk images on the device. */ @@ -153,7 +157,18 @@ public NSDictionary lookupImage(String imageType) throws IOException { try { checkResult(LibIMobileDevice.mobile_image_mounter_lookup_image(getRef(), imageType, plistOut)); PlistRef plist = plistOut.getValue(); - return (NSDictionary) PlistUtil.toJavaPlist(plist); + NSDictionary dict = (NSDictionary) PlistUtil.toJavaPlist(plist); + // TODO: mobile_image_mounter_lookup_image doesn't check plist for possible error, this might + // happen when device is locked + NSString possibleError = dict != null ? (NSString) dict.objectForKey("Error") : null; + if (possibleError != null) { + if ("DeviceLocked".equals(possibleError.toString())) + throw new LibIMobileDeviceException(MOBILE_IMAGE_MOUNTER_E_DEVICE_LOCKED.swigValue(), + MOBILE_IMAGE_MOUNTER_E_DEVICE_LOCKED.name()); + throw new LibIMobileDeviceException(MOBILE_IMAGE_MOUNTER_E_UNKNOWN_ERROR.swigValue(), + MOBILE_IMAGE_MOUNTER_E_UNKNOWN_ERROR.name()); + } + return dict; } finally { plistOut.delete(); } diff --git a/compiler/libimobiledevice/src/main/java/org/robovm/libimobiledevice/util/AppLauncher.java b/compiler/libimobiledevice/src/main/java/org/robovm/libimobiledevice/util/AppLauncher.java index a4006fe5f..091fd1dd2 100755 --- a/compiler/libimobiledevice/src/main/java/org/robovm/libimobiledevice/util/AppLauncher.java +++ b/compiler/libimobiledevice/src/main/java/org/robovm/libimobiledevice/util/AppLauncher.java @@ -16,6 +16,30 @@ */ package org.robovm.libimobiledevice.util; +import com.dd.plist.NSArray; +import com.dd.plist.NSDictionary; +import com.dd.plist.NSNumber; +import com.dd.plist.NSString; +import com.dd.plist.PropertyListParser; +import org.robovm.libimobiledevice.AfcClient; +import org.robovm.libimobiledevice.AfcClient.UploadProgressCallback; +import org.robovm.libimobiledevice.DebugServerClient; +import org.robovm.libimobiledevice.IDevice; +import org.robovm.libimobiledevice.InstallationProxyClient; +import org.robovm.libimobiledevice.InstallationProxyClient.Options; +import org.robovm.libimobiledevice.InstallationProxyClient.Options.PackageType; +import org.robovm.libimobiledevice.InstallationProxyClient.StatusCallback; +import org.robovm.libimobiledevice.LibIMobileDeviceException; +import org.robovm.libimobiledevice.LockdowndClient; +import org.robovm.libimobiledevice.LockdowndServiceDescriptor; +import org.robovm.libimobiledevice.MobileImageMounterClient; +import org.robovm.libimobiledevice.binding.LockdowndError; +import org.robovm.libimobiledevice.binding.MobileImageMounterError; +import org.robovm.libimobiledevice.util.AppLauncherCallback.AppLauncherInfo; +import org.robovm.libimobiledevice.util.Lambdas.CheckedConsumer; +import org.robovm.libimobiledevice.util.Lambdas.CheckedFunction; +import org.robovm.libimobiledevice.util.Lambdas.CheckedSupplier; + import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; @@ -23,8 +47,6 @@ import java.io.InputStream; import java.io.InterruptedIOException; import java.io.OutputStream; -import java.io.UnsupportedEncodingException; -import java.lang.ProcessBuilder.Redirect; import java.net.ServerSocket; import java.net.Socket; import java.nio.charset.StandardCharsets; @@ -43,27 +65,6 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipFile; -import org.robovm.libimobiledevice.AfcClient; -import org.robovm.libimobiledevice.AfcClient.UploadProgressCallback; -import org.robovm.libimobiledevice.DebugServerClient; -import org.robovm.libimobiledevice.IDevice; -import org.robovm.libimobiledevice.InstallationProxyClient; -import org.robovm.libimobiledevice.InstallationProxyClient.Options; -import org.robovm.libimobiledevice.InstallationProxyClient.Options.PackageType; -import org.robovm.libimobiledevice.InstallationProxyClient.StatusCallback; -import org.robovm.libimobiledevice.LibIMobileDeviceException; -import org.robovm.libimobiledevice.LockdowndClient; -import org.robovm.libimobiledevice.LockdowndServiceDescriptor; -import org.robovm.libimobiledevice.MobileImageMounterClient; -import org.robovm.libimobiledevice.binding.LockdowndError; -import org.robovm.libimobiledevice.util.AppLauncherCallback.AppLauncherInfo; - -import com.dd.plist.NSArray; -import com.dd.plist.NSDictionary; -import com.dd.plist.NSNumber; -import com.dd.plist.NSString; -import com.dd.plist.PropertyListParser; - import static org.robovm.libimobiledevice.binding.LibIMobileDeviceConstants.DEBUGSERVER_SECURE_SERVICE_NAME; import static org.robovm.libimobiledevice.binding.LibIMobileDeviceConstants.DEBUGSERVER_SERVICE_NAME; @@ -82,7 +83,8 @@ public class AppLauncher { private byte[] buffer = new byte[4096]; private StringBuilder bufferedResponses = new StringBuilder(4096); - private final IDevice device; + private final String deviceUdid; + private IDevice resolvedDevice; private final String appId; private final File localAppPath; private boolean installed = false; @@ -96,45 +98,81 @@ public class AppLauncher { private volatile boolean killed = false; private StatusCallback installStatusCallback; private UploadProgressCallback uploadProgressCallback; - private String xcodePath; - private int launchOnLockedRetries = 5; - private int secondsBetweenLaunchOnLockedRetries = 5; + private String xcodePath; + private int launchOnLockedRetries = 20; + private int secondsBetweenLaunchOnLockedRetries = 1; /** * Creates a new {@link AppLauncher} which will launch an already installed * app with the specified id. * - * @param device the device to connect to. + * @param deviceUdid the device's UDID to connect to (or null to auto pick one). * @param appId the id (CFBundleIdentifier) of the app to run. */ - public AppLauncher(IDevice device, String appId) { - this(device, appId, null); + public AppLauncher(String deviceUdid, String appId) { + this(deviceUdid, appId, null); } /** * Creates a new {@link AppLauncher} which will install the app from the * specified IPA file or app bundle dir and launch it. - * - * @param device the device to connect to. + * + * @param deviceUdid the device's UDID to connect to (or null to auto pick one). * @param localAppPath the IPA file of app bundle dir containing the app to * install and launch. */ - public AppLauncher(IDevice device, File localAppPath) throws IOException { - this(device, getAppId(localAppPath), localAppPath); + public AppLauncher(String deviceUdid, File localAppPath) throws IOException { + this(deviceUdid, getAppId(localAppPath), localAppPath); } - private AppLauncher(IDevice device, String appId, File localAppPath) { - if (device == null) { - throw new NullPointerException("device"); - } + private AppLauncher(String deviceUdid, String appId, File localAppPath) { if (appId == null) { throw new NullPointerException("appId"); } - this.device = device; + this.deviceUdid = deviceUdid != null && !deviceUdid.isEmpty() ? deviceUdid : null; this.appId = appId; this.localAppPath = localAppPath; } - + + private IDevice waitForDevice(String deviceUdid) throws Exception{ + final int retries = launchOnLockedRetries; + int retriesLeft = retries; + int secondsBetweenRetries = secondsBetweenLaunchOnLockedRetries; + + while (true) { + String[] udids = IDevice.listUdids(); + if (udids.length == 1 && (deviceUdid == null || deviceUdid.equals(udids[0]))) { + // single device and it's a match + return new IDevice(udids[0]); + } + + String message; + if (udids.length == 0) { + message = "No devices connected"; + } else if (deviceUdid != null) { + message = String.format("Required %s is not connected (%s)", deviceUdid, Arrays.asList(udids)); + } else { + message = String.format("More than 1 device connected (%s)", Arrays.asList(udids)); + } + + if (retriesLeft > 0) { + retriesLeft -= 1; + log("Waiting for device: %s. (retry %d of %d)...", message, (retries - retriesLeft), retries); + Thread.sleep(secondsBetweenRetries * 1000L); + } else throw new LibIMobileDeviceException(message); + } + } + + /** + * Looks for connected device + */ + private IDevice findDevice() throws Exception{ + if (resolvedDevice == null) + resolvedDevice = waitForDevice(deviceUdid); + + return resolvedDevice; + } + private static String getAppId(File f) throws IOException { if (f == null) { throw new NullPointerException("localAppPath"); @@ -516,7 +554,7 @@ private String encodeArgs(String appPath) { private String getAppPath(LockdowndClient lockdowndClient, String appId) throws IOException { LockdowndServiceDescriptor instService = lockdowndClient.startService(InstallationProxyClient.SERVICE_NAME); - try (InstallationProxyClient instClient = new InstallationProxyClient(device, instService)) { + try (InstallationProxyClient instClient = new InstallationProxyClient(lockdowndClient.getDevice(), instService)) { NSArray apps = instClient.browse(); for (int i = 0; i < apps.count(); i++) { NSDictionary appInfo = (NSDictionary) apps.objectAtIndex(i); @@ -538,16 +576,14 @@ private String getAppPath(LockdowndClient lockdowndClient, String appId) throws throw new RuntimeException("No app with id '" + appId + "' found on device"); } } - + public void install() throws IOException { if (!installed) { - try (LockdowndClient lockdowndClient = new LockdowndClient(device, getClass().getSimpleName(), true)) { - uploadInternal(); - if (uploadProgressCallback == null) { - log("[ 50%%] Upload done. Installing app..."); - } - installInternal(); - installed = true; + Retrying lockdownRetrying = new Retrying<>(() -> new LockdowndClient(findDevice(), getClass().getSimpleName(), true)); + try (lockdownRetrying) { + // get lockdown client, retry if password protected + LockdowndClient lockdowndClient = lockdownRetrying.perform((client) -> client); + install(lockdowndClient); } catch (IOException | RuntimeException e) { throw e; } catch (Exception e) { @@ -555,221 +591,163 @@ public void install() throws IOException { } } } - - private File getXcodePath() throws Exception { - if (xcodePath != null) { - return new File(xcodePath); - } - - File tmpFile = File.createTempFile(this.getClass().getSimpleName(), ".tmp"); - try { - int ret = new ProcessBuilder("xcode-select", "-print-path") - .redirectErrorStream(true) - .redirectOutput(Redirect.to(tmpFile)) - .start().waitFor(); - if (ret != 0) { - throw new IOException("xcode-select failed with error code: " + ret); - } - - return new File(new String(Files.readAllBytes(tmpFile.toPath()), StandardCharsets.UTF_8).trim()); - } finally { - tmpFile.delete(); - } - } - - static File findDeveloperImage(File dsDir, String productVersion, String buildVersion) - throws FileNotFoundException { - - String[] versionParts = getProductVersionParts(productVersion); - - String[] patterns = new String[] { - // 7.0.3 (11B508) - String.format("%s\\.%s\\.%s \\(%s\\)", versionParts[0], versionParts[1], versionParts[2], buildVersion), - // 7.0.3 (*) - String.format("%s\\.%s\\.%s \\(.*\\)", versionParts[0], versionParts[1], versionParts[2]), - // 7.0.3 - String.format("%s\\.%s\\.%s", versionParts[0], versionParts[1], versionParts[2]), - // 7.0 (11A465) - String.format("%s\\.%s \\(%s\\)", versionParts[0], versionParts[1], buildVersion), - // 7.0 (*) - String.format("%s\\.%s \\(.*\\)", versionParts[0], versionParts[1]), - // 7.0 - String.format("%s\\.%s", versionParts[0], versionParts[1]), - // - // wildcard versions to allow newer devices to work with older sdk as long as major version matches - // 7.0.* (*) - String.format("%s\\.%s\\.\\d+ \\(.*\\)", versionParts[0], versionParts[1]), - // 7.0.* - String.format("%s\\.%s\\.\\d+", versionParts[0], versionParts[1]), - // 7.*.* (*) - String.format("%s\\.\\d+\\.\\d+ \\(.*\\)", versionParts[0]), - // 7.*.* - String.format("%s\\.\\d+\\.\\d+", versionParts[0]), - // 7.* (*) - String.format("%s\\.\\d+ \\(.*\\)", versionParts[0]), - // 7.* - String.format("%s\\.\\d+", versionParts[0]) - }; - - File[] dirs = dsDir.listFiles(); - for (String pattern : patterns) { - for (File dir : dirs) { - if (dir.isDirectory() && dir.getName().matches(pattern)) { - File dmg = new File(dir, "DeveloperDiskImage.dmg"); - File sig = new File(dir, dmg.getName() + ".signature"); - if (dmg.isFile() && sig.isFile()) { - return dmg; - } - } + + private void install(LockdowndClient lockdowndClient) throws Exception { + if (!installed) { + uploadInternal(lockdowndClient); + if (uploadProgressCallback == null) { + log("[ 50%%] Upload done. Installing app..."); } + installInternal(lockdowndClient); + installed = true; } - throw new FileNotFoundException("No DeveloperDiskImage.dmg found in " - + dsDir.getAbsolutePath() + " for iOS version " + productVersion - + " (" + buildVersion + ")"); } /** - * Splits productVersion and expand to 3 parts (e.g. 7.0 -> 7.0.0) + * mounts developer image, first checks if its required + * @return version of developer image that was mounted */ - private static String[] getProductVersionParts(String productVersion) { - String[] versionParts = Arrays.copyOf(productVersion.split("\\."), 3); - for (int i = 0; i < versionParts.length; i++) { - if (versionParts[i] == null) { - versionParts[i] = "0"; - } - } - return versionParts; - } - - private void mountDeveloperImage(LockdowndClient lockdowndClient) throws Exception { - // Find the DeveloperDiskImage.dmg path that best matches the current device. Here's what - // the paths look like: - // Platforms/iPhoneOS.platform/DeviceSupport/5.0/DeveloperDiskImage.dmg - // Platforms/iPhoneOS.platform/DeviceSupport/6.0/DeveloperDiskImage.dmg - // Platforms/iPhoneOS.platform/DeviceSupport/6.1/DeveloperDiskImage.dmg - // Platforms/iPhoneOS.platform/DeviceSupport/7.0/DeveloperDiskImage.dmg - // Platforms/iPhoneOS.platform/DeviceSupport/7.0 (11A465)/DeveloperDiskImage.dmg - // Platforms/iPhoneOS.platform/DeviceSupport/7.0.3 (11B508)/DeveloperDiskImage.dmg - - String productVersion = lockdowndClient.getValue(null, "ProductVersion").toString(); // E.g. 7.0.2 - String buildVersion = lockdowndClient.getValue(null, "BuildVersion").toString(); // E.g. 11B508 - File deviceSupport = new File(getXcodePath(), "Platforms/iPhoneOS.platform/DeviceSupport"); - log("Looking up developer disk image for iOS version %s (%s) in %s", productVersion, buildVersion, deviceSupport); - File devImage = findDeveloperImage(deviceSupport, productVersion, buildVersion); - File devImageSig = new File(devImage.getParentFile(), devImage.getName() + ".signature"); - byte[] devImageSigBytes = Files.readAllBytes(devImageSig.toPath()); - - LockdowndServiceDescriptor mimService = lockdowndClient.startService(MobileImageMounterClient.SERVICE_NAME); - try (MobileImageMounterClient mimClient = new MobileImageMounterClient(device, mimService)) { - - log("Copying developer disk image %s to device", devImage); - - int majorVersion = Integer.parseInt(getProductVersionParts(productVersion)[0]); - if (majorVersion >= 7) { - // Use new upload method - mimClient.uploadImage(devImage, null, devImageSigBytes); - } else { - LockdowndServiceDescriptor afcService = lockdowndClient.startService(AfcClient.SERVICE_NAME); - try (AfcClient afcClient = new AfcClient(device, afcService)) { - afcClient.makeDirectory("/PublicStaging"); - afcClient.fileCopy(devImage, "/PublicStaging/staging.dimage"); - } + private Version mountDeveloperImageIfRequired(LockdowndClient lockdowndClient, Version deviceVersion) throws Exception { + Retrying retrying = new Retrying<>(() -> { + LockdowndServiceDescriptor mimService = lockdowndClient.startService(MobileImageMounterClient.SERVICE_NAME); + return new MobileImageMounterClient(lockdowndClient.getDevice(), mimService); + }); + try (retrying) { + // check if already mounted + + log("Checking if developer disk image requires to be mounted."); + NSDictionary result = retrying.perform((mimClient) -> { + return mimClient.lookupImage(null); + }); + if (result.objectForKey("ImageSignature") != null) { + // already mounted + log("Developer disk image is already mounted."); + return null; } - - log("Mounting developer disk image"); - NSDictionary result = mimClient.mountImage("/PublicStaging/staging.dimage", devImageSigBytes, null); + + File deviceSupport = DeveloperImageResolver.getDeviceSupportPath(); + log("Looking up developer disk image for iOS version %s in %s", deviceVersion, deviceSupport); + DeveloperImageResolver.Response devImageResp = DeveloperImageResolver.findDeveloperImage(deviceSupport, deviceVersion); + byte[] devImageSigBytes = Files.readAllBytes(devImageResp.signature.toPath()); + + log("Copying developer disk image %s to device", devImageResp.dmg); + retrying.perform((mimClient) -> { + mimClient.uploadImage(devImageResp.dmg, null, devImageSigBytes); + }); + + log("Mounting developer disk image"); + result = retrying.perform((mimClient) -> { + return mimClient.mountImage("/PublicStaging/staging.dimage", devImageSigBytes, null); + }); NSString status = (NSString) result.objectForKey("Status"); if (status == null || !"Complete".equals(status.toString())) { - throw new IOException("Failed to mount " + devImage.getAbsolutePath() + " on the device."); + throw new IOException("Failed to mount " + devImageResp.dmg.getAbsolutePath() + " on the device."); } - } - } - private LockdowndServiceDescriptor startDebugServerService(LockdowndClient lockdowndClient, String serviceName) { - LockdowndServiceDescriptor serviceDescriptor; - try { - serviceDescriptor = lockdowndClient.startService(serviceName); - } catch (LibIMobileDeviceException e) { - if (e.getErrorCode() == LockdowndError.LOCKDOWN_E_INVALID_SERVICE.swigValue() && - DEBUGSERVER_SECURE_SERVICE_NAME.equals(serviceName)) { - // fallback with non SSL version - return startDebugServerService(lockdowndClient, DEBUGSERVER_SERVICE_NAME); - } else { - throw e; + // FIXME: seems to be a delay required between mounting image and ability to start service + Thread.sleep(1000); + + // verify that image was mounted + result = retrying.perform((mimClient) -> { + return mimClient.lookupImage(null); + }); + if (result.objectForKey("ImageSignature") == null) { + throw new LibIMobileDeviceException("Developer disk image mounting failed: status not mounted!"); } + return devImageResp.version; } - return serviceDescriptor; } private int launchInternal() throws Exception { - install(); - - int lockedRetriesLeft = launchOnLockedRetries; - while (true) { - String appPath = null; - - try (LockdowndClient lockdowndClient = new LockdowndClient(device, getClass().getSimpleName(), true)) { - appPath = getAppPath(lockdowndClient, appId); - String productVersion = lockdowndClient.getValue(null, "ProductVersion").toString(); // E.g. 7.0.2 - String buildVersion = lockdowndClient.getValue(null, "BuildVersion").toString(); // E.g. 11B508 - if(appLauncherCallback != null) { - appLauncherCallback.setAppLaunchInfo(new AppLauncherInfo(device, appPath, productVersion, buildVersion)); - } - LockdowndServiceDescriptor serviceDescriptor; - try { - serviceDescriptor = startDebugServerService(lockdowndClient, DEBUGSERVER_SECURE_SERVICE_NAME); - } catch (LibIMobileDeviceException e) { - if (e.getErrorCode() == LockdowndError.LOCKDOWN_E_INVALID_SERVICE.swigValue()) { - // This happens when the developer image hasn't been mounted. - // Mount and try again. - mountDeveloperImage(lockdowndClient); - serviceDescriptor = startDebugServerService(lockdowndClient, DEBUGSERVER_SECURE_SERVICE_NAME); - } else { - throw e; - } + String appPath = null; + + Retrying lockdownRetrying = new Retrying<>(() -> new LockdowndClient(findDevice(), getClass().getSimpleName(), true)); + try (lockdownRetrying) { + // get lockdown client, retry if password protected + LockdowndClient lockdowndClient = lockdownRetrying.perform((client) -> client); + + // install if required + install(lockdowndClient); + + appPath = getAppPath(lockdowndClient, appId); + String productVersion = lockdowndClient.getValue(null, "ProductVersion").toString(); // E.g. 7.0.2 + String buildVersion = lockdowndClient.getValue(null, "BuildVersion").toString(); // E.g. 11B508 + Version deviceVersion = Version.parse(productVersion); + + // check if development mode is enabled on ios16+ devices + if (deviceVersion.getMajor() >= 16) { + NSNumber developerModeStatus = (NSNumber) lockdowndClient.getValue("com.apple.security.mac.amfi", "DeveloperModeStatus"); + if (!developerModeStatus.boolValue()) { + String msg = "You have to enable Developer Mode on the given device!"; + log(msg); + throw new RuntimeException(msg); } + } - try (DebugServerClient client = new DebugServerClient(device, serviceDescriptor)) { - log("Debug server port: " + serviceDescriptor.getPort()); + // mount dev image if required + Version mountedDevImage = mountDeveloperImageIfRequired(lockdowndClient, deviceVersion); - if (localPort != -1) { - String exe = ((NSDictionary) PropertyListParser.parse(new File(localAppPath, "Info.plist"))).objectForKey("CFBundleExecutable").toString(); - log("launchios \"" + new File(localAppPath, exe).getAbsolutePath() + "\" \"" + appPath + "\" " + localPort); - StringBuilder argsString = new StringBuilder(); - for (String arg : args) { - if (argsString.length() > 0) { - argsString.append(' '); - } - argsString.append(arg); + // start debug server service + LockdowndServiceDescriptor debugServerServiceDescriptor; + { + log("Starting DebugServerService."); + + // check if device is locked here before starting debug server otherwise it will fail without + // ability to catch and retry + try (Retrying retrying = new Retrying<>(() -> null)) { + retrying.perform((ignored) -> { + NSNumber status = (NSNumber) lockdowndClient.getValue(null, "PasswordProtected"); + if (status != null && status.boolValue()) { + throw new LibIMobileDeviceException(LockdowndError.LOCKDOWN_E_PASSWORD_PROTECTED.swigValue(), + LockdowndError.LOCKDOWN_E_PASSWORD_PROTECTED.name()); } - log("process launch -- " + argsString); - } + }); + } - if (lockedRetriesLeft == launchOnLockedRetries) { - // First try - log("Remote app path: " + appPath); - log("Launching app..."); - } else { - log("Launching app (retry %d of %d)...", - (launchOnLockedRetries - lockedRetriesLeft), launchOnLockedRetries); - } + // starting from developer image version 13.6 there is secure version of debug server service + // if dev image wasn't mounted in this session its version is not known then using + // secure only for ios14+ + String serviceName; + if (deviceVersion.getMajor() >= 14 || (mountedDevImage != null && mountedDevImage.compareTo(new Version(13, 6)) >= 0)) { + serviceName = DEBUGSERVER_SECURE_SERVICE_NAME; + } else { + serviceName = DEBUGSERVER_SERVICE_NAME; + } + debugServerServiceDescriptor = lockdowndClient.startService(serviceName); + } - // just pipe stdout if no port forwarding should be done - // otherwise perform port forwarding and stdout piping - if(localPort == -1) { - return pipeStdOut(client, appPath); - } else { - return forward(client, appPath); + // app is ready to launch + if(appLauncherCallback != null) { + appLauncherCallback.setAppLaunchInfo(new AppLauncherInfo(lockdowndClient.getDevice(), appPath, productVersion, buildVersion)); + } + + // start debug service + try (DebugServerClient client = new DebugServerClient(lockdowndClient.getDevice(), debugServerServiceDescriptor)) { + log("Debug server port: " + debugServerServiceDescriptor.getPort()); + + if (localPort != -1) { + String exe = ((NSDictionary) PropertyListParser.parse(new File(localAppPath, "Info.plist"))).objectForKey("CFBundleExecutable").toString(); + log("launchios \"" + new File(localAppPath, exe).getAbsolutePath() + "\" \"" + appPath + "\" " + localPort); + StringBuilder argsString = new StringBuilder(); + for (String arg : args) { + if (argsString.length() > 0) { + argsString.append(' '); + } + argsString.append(arg); } + log("process launch -- " + argsString); } - } catch (RuntimeException e) { - if (!e.getMessage().contains("Locked") || lockedRetriesLeft == 0) { - throw e; + + log("Remote app path: " + appPath); + log("Launching app..."); + + // just pipe stdout if no port forwarding should be done + // otherwise perform port forwarding and stdout piping + if(localPort == -1) { + return pipeStdOut(client, appPath); + } else { + return forward(client, appPath); } - lockedRetriesLeft--; - log("Device locked. Retrying launch in %d seconds...", - secondsBetweenLaunchOnLockedRetries); - Thread.sleep(secondsBetweenLaunchOnLockedRetries * 1000); } } } @@ -992,63 +970,70 @@ private void debugForward(OutputStream fileOut, String prefix, List mess } } - private void installInternal() throws Exception { - try (LockdowndClient lockdowndClient = new LockdowndClient(device, getClass().getSimpleName(), true)) { - final LibIMobileDeviceException[] ex = new LibIMobileDeviceException[1]; - final CountDownLatch countDownLatch = new CountDownLatch(1); + private void installInternal(LockdowndClient lockdowndClient) throws Exception { + final LibIMobileDeviceException[] ex = new LibIMobileDeviceException[1]; + final CountDownLatch countDownLatch = new CountDownLatch(1); + Retrying retrying = new Retrying<>(() -> { LockdowndServiceDescriptor instproxyService = lockdowndClient.startService(InstallationProxyClient.SERVICE_NAME); - try (InstallationProxyClient instClient = new InstallationProxyClient(device, instproxyService)) { - instClient.upgrade("/PublicStaging/" + localAppPath.getName(), - new Options().packageType(localAppPath.isDirectory() ? PackageType.Developer : null), + return new InstallationProxyClient(lockdowndClient.getDevice(), instproxyService); + }); + try (retrying) { + retrying.perform((instClient) -> { + instClient.upgrade("/PublicStaging/" + localAppPath.getName(), + new Options().packageType(localAppPath.isDirectory() ? PackageType.Developer : null), new StatusCallback() { - - @Override - public void progress(String status, int percentComplete) { - if (installStatusCallback != null) { - installStatusCallback.progress(status, percentComplete); - } else { - log("[%3d%%] %s", 50 + percentComplete / 2, status); - } - } - @Override - public void success() { - try { - if (installStatusCallback != null) { - installStatusCallback.success(); - } else { - log("[100%%] Installation complete"); + @Override + public void progress(String status, int percentComplete) { + if (installStatusCallback != null) { + installStatusCallback.progress(status, percentComplete); + } else { + log("[%3d%%] %s", 50 + percentComplete / 2, status); + } } - } finally { - countDownLatch.countDown(); - } - } - @Override - public void error(String message) { - try { - ex[0] = new LibIMobileDeviceException(message); - if (installStatusCallback != null) { - installStatusCallback.error(message); - } else { - log("Error: %s", message); + + @Override + public void success() { + try { + if (installStatusCallback != null) { + installStatusCallback.success(); + } else { + log("[100%%] Installation complete"); + } + } finally { + countDownLatch.countDown(); + } } - } finally { - countDownLatch.countDown(); - } - } - }); + + @Override + public void error(String message) { + try { + ex[0] = new LibIMobileDeviceException(message); + if (installStatusCallback != null) { + installStatusCallback.error(message); + } else { + log("Error: %s", message); + } + } finally { + countDownLatch.countDown(); + } + } + }); countDownLatch.await(); - } - - if (ex[0] != null) { - throw ex[0]; - } + }); + } + + if (ex[0] != null) { + throw ex[0]; } } - private void uploadInternal() throws Exception { - try (LockdowndClient lockdowndClient = new LockdowndClient(device, getClass().getSimpleName(), true)) { + private void uploadInternal(LockdowndClient lockdowndClient) throws Exception { + Retrying retrying = new Retrying<>(() -> { LockdowndServiceDescriptor afcService = lockdowndClient.startService(AfcClient.SERVICE_NAME); - try (AfcClient afcClient = new AfcClient(device, afcService)) { + return new AfcClient(lockdowndClient.getDevice(), afcService); + }); + try (retrying) { + retrying.perform((afcClient) -> { afcClient.upload(localAppPath, "/PublicStaging", new UploadProgressCallback() { public void progress(File path, int percentComplete) { if (uploadProgressCallback != null) { @@ -1070,10 +1055,98 @@ public void error(String message) { } } }); + }); + } + } + + /** + * Helper class that performs retry if skipable errors happen such as Device is locked + * clientConstructor -- lambda that construct client that is passed into perform function + */ + + private class Retrying implements AutoCloseable { + + final private int retries; + final private int secondsBetweenRetries; + private int retriesLeft; + private Client client; + private final CheckedSupplier clientConstructor; + + public Retrying(int retries, int secondsBetweenRetries, CheckedSupplier clientConstructor) { + this.retries = retries; + this.secondsBetweenRetries = secondsBetweenRetries; + this.retriesLeft = retries; + this.clientConstructor = clientConstructor; + } + + public Retrying(CheckedSupplier clientConstructor) { + this(launchOnLockedRetries, secondsBetweenLaunchOnLockedRetries, clientConstructor); + } + + private void perform(CheckedConsumer action) throws Exception { + perform((client) -> { + action.accept(client); + return null; + }); + } + + private R perform(CheckedFunction action) throws Exception { + while (true) { + try { + if (client == null) + client = clientConstructor.get(); + R r = action.apply(client); + // reset retries + this.retriesLeft = retries; + return r; + } catch (LibIMobileDeviceException e) { + // if error had happened client to be re-created on next retry if it is not fatal + if (client != null) { + try { + client.close();; + } catch (Exception ignored){ + } + client = null; + } + + String message = null; + boolean fatal = true; + // if there is no retries left threat it as fatal + if (retriesLeft > 0) { + retriesLeft -= 1; + // check if it can be skipped + if (e.getErrorCode() == LockdowndError.LOCKDOWN_E_USER_DENIED_PAIRING.swigValue()) { + message = "Device is not paired with your computer, unlock it and choose to trust this computer when prompted."; + } else if (e.getErrorCode() == LockdowndError.LOCKDOWN_E_PAIRING_DIALOG_RESPONSE_PENDING.swigValue()) { + message = "Pairing in progress. Please choose to trust this computer."; + fatal = false; + } else if (e.getErrorCode() == LockdowndError.LOCKDOWN_E_PASSWORD_PROTECTED.swigValue() || + e.getErrorCode() == MobileImageMounterError.MOBILE_IMAGE_MOUNTER_E_DEVICE_LOCKED.swigValue()) { + message = "Device is locked. Please unlock to proceed."; + fatal = false; + } + } + + if (message != null) { + if (!fatal) log(message + " (retry %d of %d)...", (retries - retriesLeft), retries); + else log(message); + } + + if (fatal) + throw e; + + Thread.sleep(secondsBetweenRetries * 1000L); + } } } + + @Override + public void close() throws Exception { + if (client != null) + client.close(); + } } - + public int launch() throws IOException { try { return launchInternal(); @@ -1091,7 +1164,7 @@ public int launch() throws IOException { } } } - + private static void printUsageAndExit() { System.err.println(AppLauncher.class.getName() + " ..."); System.err.println(" -appid the id (CFBundleIdentifier) of the app to launch."); @@ -1150,26 +1223,11 @@ public static void main(String[] args) throws Exception { printUsageAndExit(); } - if (deviceId == null) { - String[] udids = IDevice.listUdids(); - if (udids.length == 0) { - System.err.println("No device connected"); - return; - } - if (udids.length > 1) { - System.err.println("More than 1 device connected (" - + Arrays.asList(udids) + "). Using " + udids[0]); - } - deviceId = udids[0]; - } - - IDevice device = new IDevice(deviceId); - AppLauncher launcher; if (localAppPath != null) { - launcher = new AppLauncher(device, localAppPath); + launcher = new AppLauncher(deviceId, localAppPath); } else { - launcher = new AppLauncher(device, appId); + launcher = new AppLauncher(deviceId, appId); } System.exit(launcher diff --git a/compiler/libimobiledevice/src/main/java/org/robovm/libimobiledevice/util/AppLauncherCallback.java b/compiler/libimobiledevice/src/main/java/org/robovm/libimobiledevice/util/AppLauncherCallback.java index 30d9b4afe..1bd17d344 100755 --- a/compiler/libimobiledevice/src/main/java/org/robovm/libimobiledevice/util/AppLauncherCallback.java +++ b/compiler/libimobiledevice/src/main/java/org/robovm/libimobiledevice/util/AppLauncherCallback.java @@ -24,11 +24,11 @@ * */ public interface AppLauncherCallback { - public void setAppLaunchInfo(AppLauncherInfo info); - - public byte[] filterOutput(byte[] data); + void setAppLaunchInfo(AppLauncherInfo info); + + byte[] filterOutput(byte[] data); - static class AppLauncherInfo { + class AppLauncherInfo { final IDevice device; final String remoteAppPath; final String productVersion; diff --git a/compiler/libimobiledevice/src/main/java/org/robovm/libimobiledevice/util/DeveloperImageResolver.java b/compiler/libimobiledevice/src/main/java/org/robovm/libimobiledevice/util/DeveloperImageResolver.java new file mode 100644 index 000000000..39a0af48b --- /dev/null +++ b/compiler/libimobiledevice/src/main/java/org/robovm/libimobiledevice/util/DeveloperImageResolver.java @@ -0,0 +1,122 @@ +/* + * 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.libimobiledevice.util; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class DeveloperImageResolver { + private static String xcodePath; + + static File getXcodePath() throws Exception { + if (xcodePath != null) { + return new File(xcodePath); + } + + File tmpFile = File.createTempFile("DeveloperImageResolver", ".tmp"); + try { + int ret = new ProcessBuilder("xcode-select", "-print-path") + .redirectErrorStream(true) + .redirectOutput(ProcessBuilder.Redirect.to(tmpFile)) + .start().waitFor(); + if (ret != 0) { + throw new IOException("xcode-select failed with error code: " + ret); + } + + return new File(new String(Files.readAllBytes(tmpFile.toPath()), StandardCharsets.UTF_8).trim()); + } finally { + tmpFile.delete(); + } + } + + static File getDeviceSupportPath() throws Exception { + return new File(getXcodePath(), "Platforms/iPhoneOS.platform/DeviceSupport"); + } + + /** + * Value object to provide information about resolved developer image + */ + static class Response { + final Version version; + final File dmg; + final File signature; + + Response(Version version, File dmg, File signature) { + this.version = version; + this.dmg = dmg; + this.signature = signature; + } + } + + static Response findDeveloperImage(File dsDir, Version deviceVersion) throws FileNotFoundException { + // Find the DeveloperDiskImage.dmg path that best matches the current device. Here's what + // the paths look like: + // Platforms/iPhoneOS.platform/DeviceSupport/5.0/DeveloperDiskImage.dmg + // Platforms/iPhoneOS.platform/DeviceSupport/6.0/DeveloperDiskImage.dmg + // Platforms/iPhoneOS.platform/DeviceSupport/6.1/DeveloperDiskImage.dmg + // Platforms/iPhoneOS.platform/DeviceSupport/7.0/DeveloperDiskImage.dmg + // Platforms/iPhoneOS.platform/DeviceSupport/7.0 (11A465)/DeveloperDiskImage.dmg + // Platforms/iPhoneOS.platform/DeviceSupport/7.0.3 (11B508)/DeveloperDiskImage.dmg + + // capturing patter: version + optional buildNumber e.g. "16.0.0 (16A123)" + Pattern pattern = Pattern.compile("(\\d+(?:\\.\\d+)*)(?:\\s*\\((.*)\\))?"); + + Version bestMatchVersion= null; + File bestMatchDmgFile= null; + File bestMatchSignatureFile= null; + File[] dirs = dsDir.listFiles(); + for (File dir : dirs) { + if (dir.isDirectory()) { + Matcher matcher = pattern.matcher(dir.getName()); + if (matcher.matches()) { + File dmg = new File(dir, "DeveloperDiskImage.dmg"); + File sig = new File(dir, dmg.getName() + ".signature"); + if (dmg.isFile() && sig.isFile()) { + Version dmgVersion = Version.parse(matcher.group(1)); + if (dmgVersion.getMajor() == deviceVersion.getMajor()) { + int diff = dmgVersion.compareTo(deviceVersion); + if (diff == 0) { + // exact match + bestMatchVersion = dmgVersion; + bestMatchDmgFile = dmg; + bestMatchSignatureFile = sig; + // found the one + break; + } else if (diff < 0 && (bestMatchVersion == null || bestMatchVersion.compareTo(dmgVersion) < 0)) { + // version is older than device one and version is never than best one + bestMatchVersion = dmgVersion; + bestMatchDmgFile = dmg; + bestMatchSignatureFile = sig; + } + } + } + } + } + } + if (bestMatchDmgFile == null) { + throw new FileNotFoundException("No DeveloperDiskImage.dmg found in " + + dsDir.getAbsolutePath() + " for iOS version " + deviceVersion); + } + + return new Response(bestMatchVersion, bestMatchDmgFile, bestMatchSignatureFile); + } +} diff --git a/compiler/libimobiledevice/src/main/java/org/robovm/libimobiledevice/util/Lambdas.java b/compiler/libimobiledevice/src/main/java/org/robovm/libimobiledevice/util/Lambdas.java new file mode 100644 index 000000000..ed3282bfc --- /dev/null +++ b/compiler/libimobiledevice/src/main/java/org/robovm/libimobiledevice/util/Lambdas.java @@ -0,0 +1,21 @@ +package org.robovm.libimobiledevice.util; + +/** + * Collection of lambda interface definitions that throws exception + */ +public final class Lambdas { + @FunctionalInterface + public interface CheckedConsumer { + void accept(T t) throws Exception; + } + + @FunctionalInterface + public interface CheckedSupplier { + T get() throws Exception; + } + + @FunctionalInterface + public interface CheckedFunction { + R apply(T t) throws Exception; + } +} diff --git a/compiler/libimobiledevice/src/main/java/org/robovm/libimobiledevice/util/Version.java b/compiler/libimobiledevice/src/main/java/org/robovm/libimobiledevice/util/Version.java new file mode 100644 index 000000000..e62020480 --- /dev/null +++ b/compiler/libimobiledevice/src/main/java/org/robovm/libimobiledevice/util/Version.java @@ -0,0 +1,63 @@ +/* + * 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.libimobiledevice.util; + +import java.util.Arrays; +import java.util.stream.Collectors; + +/** + * Another simple version class, local withing libmobiledevice + */ +class Version implements Comparable { + // parts of version -- parsed dot separated values "1.2.3" -> [1,2,3] + public final int[] parts; + + public Version(int... parts) { + this.parts = parts; + } + + @Override + public int compareTo(Version other) { + // walk by shortest common part + int commonPart = Math.min(parts.length, other.parts.length); + for (int i = 0; i < commonPart; i++) { + int diff = parts[i] - other.parts[i]; + if (diff != 0) + return diff; + } + + // same common part, longer wins + return this.parts.length - other.parts.length; + } + + public int getMajor() { + return parts[0]; + } + + public static Version parse(String v) { + String[] tokens = v.split("\\."); + int[] parts = new int[tokens.length]; + for (int i = 0; i < tokens.length; i++) + parts[i] = Integer.parseInt(tokens[i]); + return new Version(parts); + } + + @Override + public String toString() { + return Arrays.stream(parts).mapToObj(Integer::toString).collect(Collectors.joining(".")); + } +} diff --git a/compiler/libimobiledevice/src/test/java/org/robovm/libimobiledevice/util/AppLauncherTest.java b/compiler/libimobiledevice/src/test/java/org/robovm/libimobiledevice/util/DeveloperImageResolverTest.java similarity index 60% rename from compiler/libimobiledevice/src/test/java/org/robovm/libimobiledevice/util/AppLauncherTest.java rename to compiler/libimobiledevice/src/test/java/org/robovm/libimobiledevice/util/DeveloperImageResolverTest.java index 97a2b3e72..a7c832148 100755 --- a/compiler/libimobiledevice/src/test/java/org/robovm/libimobiledevice/util/AppLauncherTest.java +++ b/compiler/libimobiledevice/src/test/java/org/robovm/libimobiledevice/util/DeveloperImageResolverTest.java @@ -16,18 +16,18 @@ */ package org.robovm.libimobiledevice.util; -import static org.junit.Assert.*; - import java.io.File; import java.nio.file.Files; import java.nio.file.Path; import org.junit.Test; +import static org.junit.Assert.assertEquals; + /** - * Tests {@link AppLauncher}. + * Tests {@link DeveloperImageResolver}. */ -public class AppLauncherTest { +public class DeveloperImageResolverTest { private File createDeveloperImage(Path dsDir, String version) throws Exception { return createDeveloperImage(dsDir, version, true); @@ -56,19 +56,19 @@ public void testFindDeveloperImage() throws Exception { File f92 = createDeveloperImage(dsDir, "9.2"); File f102 = createDeveloperImage(dsDir, "10.2 (14D123)"); - assertEquals(f703_11B508, AppLauncher.findDeveloperImage(dsDir.toFile(), "7.0.3", "11B508")); - assertEquals(f61, AppLauncher.findDeveloperImage(dsDir.toFile(), "6.1.3", "10A123")); - assertEquals(f61, AppLauncher.findDeveloperImage(dsDir.toFile(), "6.1.1", "10A123")); - assertEquals(f612, AppLauncher.findDeveloperImage(dsDir.toFile(), "6.1.2", "10A123")); - assertEquals(f703_11B508, AppLauncher.findDeveloperImage(dsDir.toFile(), "7.0.3", "12C123")); - assertEquals(f70_11A465, AppLauncher.findDeveloperImage(dsDir.toFile(), "7.0.2", "12C123")); + assertEquals(f703_11B508, DeveloperImageResolver.findDeveloperImage(dsDir.toFile(), new Version(7, 0, 3)).dmg); + assertEquals(f612, DeveloperImageResolver.findDeveloperImage(dsDir.toFile(), new Version(6, 1, 2)).dmg); + assertEquals(f703_11B508, DeveloperImageResolver.findDeveloperImage(dsDir.toFile(), new Version(7, 0, 3)).dmg); // finding not exact match - assertEquals(f812_13B812, AppLauncher.findDeveloperImage(dsDir.toFile(), "8.1.3", "14C710")); - assertEquals(f812_13B812, AppLauncher.findDeveloperImage(dsDir.toFile(), "8.2", "14C710")); - assertEquals(f92, AppLauncher.findDeveloperImage(dsDir.toFile(), "9.3.3", "15C710")); - assertEquals(f92, AppLauncher.findDeveloperImage(dsDir.toFile(), "9.3", "15C710")); - assertEquals(f102, AppLauncher.findDeveloperImage(dsDir.toFile(), "10.3.5", "15C710")); - assertEquals(f102, AppLauncher.findDeveloperImage(dsDir.toFile(), "10.3", "15C710")); + assertEquals(f612, DeveloperImageResolver.findDeveloperImage(dsDir.toFile(), new Version(6, 1, 3)).dmg); + assertEquals(f61, DeveloperImageResolver.findDeveloperImage(dsDir.toFile(), new Version(6, 1, 1)).dmg); + assertEquals(f70_11A465, DeveloperImageResolver.findDeveloperImage(dsDir.toFile(), new Version(7, 0, 2)).dmg); + assertEquals(f812_13B812, DeveloperImageResolver.findDeveloperImage(dsDir.toFile(), new Version(8, 1, 3)).dmg); + assertEquals(f812_13B812, DeveloperImageResolver.findDeveloperImage(dsDir.toFile(), new Version(8, 2)).dmg); + assertEquals(f92, DeveloperImageResolver.findDeveloperImage(dsDir.toFile(), new Version(9, 3, 3)).dmg); + assertEquals(f92, DeveloperImageResolver.findDeveloperImage(dsDir.toFile(), new Version(9, 3)).dmg); + assertEquals(f102, DeveloperImageResolver.findDeveloperImage(dsDir.toFile(), new Version(10, 3, 5)).dmg); + assertEquals(f102, DeveloperImageResolver.findDeveloperImage(dsDir.toFile(), new Version(10, 3)).dmg); } } 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 b644faeba..77fcf627e 100644 --- a/plugins/debugger/src/main/java/org/robovm/debugger/Debugger.java +++ b/plugins/debugger/src/main/java/org/robovm/debugger/Debugger.java @@ -92,7 +92,7 @@ public void start() { // start JDWP server this.jdwpServer.start(); - // start hoocks channel + // start hooks channel this.hooksChannel.start(); } 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 23de3f867..55354227e 100644 --- a/plugins/debugger/src/main/java/org/robovm/debugger/DebuggerConfig.java +++ b/plugins/debugger/src/main/java/org/robovm/debugger/DebuggerConfig.java @@ -34,7 +34,8 @@ * Configuration file that is to start debugger */ public class DebuggerConfig { - public final static int TARGET_WAIT_TIMEOUT = 60000; + public final static int TARGET_WAIT_TIMEOUT = 60_000; + public final static int TARGET_DEPLOY_TIMEOUT = 120_000; /** * debugger local arch list, corresponds one from compiler 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 97c0c49f1..d91c1ce65 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 @@ -82,7 +82,6 @@ protected ProcessHandler executeRun() throws Throwable { } process = new ProcessProxy(process, pipedOut, stdoutStream, stderrStream, compiler); } - RoboVmPlugin.logInfo(getEnvironment().getProject(), "Launch done"); final OSProcessHandler processHandler = new ColoredProcessHandler(process, null); ProcessTerminatedListener.attach(processHandler); diff --git a/pom.xml b/pom.xml index 80143c33d..39e1ff847 100644 --- a/pom.xml +++ b/pom.xml @@ -342,7 +342,7 @@ maven-compiler-plugin 3.8.1 - 8 + 9 true true UTF-8 @@ -504,7 +504,7 @@ none - 8 + 9 false