├── .gitignore ├── javatests └── com │ └── google │ └── iosdevicecontrol │ ├── util │ ├── resource1 │ ├── resource2 │ └── ResourceTest.java │ ├── webinspector │ ├── MessageSelectorTest.java │ ├── MessageKeyTest.java │ ├── WebInspectorTest.java │ └── BinaryPlistSocketTest.java │ └── real │ └── RealAppProcessTest.java ├── OpenUrlApp ├── README ├── openURL-Prefix.pch ├── main.m ├── OpenURLAppDelegate.h └── openURL-Info.plist ├── third_party ├── idevice_app_runner │ ├── Makefile │ └── README.txt └── idevicewebinspectorproxy │ ├── README │ └── Makefile ├── java └── com │ └── google │ └── iosdevicecontrol │ ├── command │ ├── package-info.java │ ├── CommandExecutor.java │ ├── CommandStartException.java │ ├── CommandFailureException.java │ ├── CommandException.java │ ├── CommandResult.java │ ├── testing │ │ └── FakeExecutor.java │ ├── Examples.java │ ├── InputSource.java │ └── AsyncCopier.java │ ├── util │ ├── VoidCallable.java │ ├── CheckedCallable.java │ ├── JsonParser.java │ ├── ExceptionHandler.java │ ├── StringEnumMap.java │ ├── PlistParser.java │ ├── TunnelException.java │ ├── FluentLogger.java │ ├── EllipsisFormat.java │ └── Resource.java │ ├── IosDeviceResource.java │ ├── webinspector │ ├── InspectorSocket.java │ ├── InspectorDriver.java │ ├── ReportIdentifierMessage.java │ ├── InspectorPage.java │ ├── InspectorMessage.java │ ├── ForwardGetListingMessage.java │ ├── ApplicationUpdatedMessage.java │ ├── ApplicationConnectedMessage.java │ ├── ApplicationDisconnectedMessage.java │ ├── WebInspector.java │ ├── ReportConnectedDriverListMessage.java │ ├── ApplicationSentDataMessage.java │ ├── MessageSelector.java │ ├── ForwardSocketSetupMessage.java │ ├── ApplicationSentListingMessage.java │ ├── ForwardSocketDataMessage.java │ ├── ReportSetupMessage.java │ ├── ReportConnectedApplicationListMessage.java │ ├── InspectorApplication.java │ └── ApplicationMessage.java │ ├── testing │ ├── FakeIosDeviceHost.java │ ├── FakeIosDeviceSocket.java │ ├── FakeOpenUrlApp.java │ ├── FakeInspectorSocket.java │ └── FakeIosAppProcess.java │ ├── IosAppProcess.java │ ├── real │ ├── ConfigurationProfile.java │ ├── RealDevice.java │ ├── IdeviceCommands.java │ ├── RealAppProcess.java │ ├── CfgutilCommands.java │ └── DevDiskImages.java │ ├── simulator │ ├── SimulatorDevice.java │ ├── SimulatorAppProcess.java │ └── SimctlCommands.java │ ├── IosDeviceHost.java │ ├── IosVersion.java │ ├── examples │ ├── ExampleRealDeviceControl.java │ └── ExampleSimulatorDeviceControl.java │ ├── IosDeviceException.java │ ├── openurl │ └── AppResult.java │ ├── IosAppBundleId.java │ ├── IosAppInfo.java │ ├── IosDeviceSocket.java │ ├── IosModel.java │ └── IosDevice.java ├── CONTRIBUTING.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | -------------------------------------------------------------------------------- /javatests/com/google/iosdevicecontrol/util/resource1: -------------------------------------------------------------------------------- 1 | resource1 2 | -------------------------------------------------------------------------------- /javatests/com/google/iosdevicecontrol/util/resource2: -------------------------------------------------------------------------------- 1 | resource2 2 | -------------------------------------------------------------------------------- /OpenUrlApp/README: -------------------------------------------------------------------------------- 1 | This is the code for iOS apps that are used to help automate iOS tests. 2 | 3 | To build this app, create a new XCode project and import this folder. Then 4 | build the file as an ipa and move that ipa to /usr/local/share/OpenUrl.ipa in 5 | order to use the ipa in conjunction with the OpenUrlApp Java library. 6 | -------------------------------------------------------------------------------- /third_party/idevice_app_runner/Makefile: -------------------------------------------------------------------------------- 1 | PREFIX=/usr/local 2 | 3 | idevice-app-runner: idevice-app-runner.c 4 | gcc -g -pthread $^ -o $@ -I$(PREFIX)/include -L$(PREFIX)/lib -lplist -limobiledevice 5 | 6 | install: idevice-app-runner 7 | # Use mkdir -p first, because "install -D" not support on Mac 8 | mkdir -p $(PREFIX)/bin/ 9 | install $^ $(PREFIX)/bin/$^ 10 | -------------------------------------------------------------------------------- /third_party/idevicewebinspectorproxy/README: -------------------------------------------------------------------------------- 1 | Utility binary for proxying a WebInspector connection from a real iOS device to 2 | a local socket at a specific port. 3 | 4 | To install the binary, the libimobiledevice Github project needs to be cloned. 5 | Then run the following command, replacing libimd root with the location of the 6 | libimobiledevice root directory: 7 | 8 | sudo make install LIBIMD_ROOT=/path/to/libimobiledevice 9 | -------------------------------------------------------------------------------- /OpenUrlApp/openURL-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header 3 | // 4 | // The contents of this file are implicitly included at the beginning of every 5 | // source file. 6 | // 7 | 8 | #import 9 | 10 | #ifndef __IPHONE_3_0 11 | #warning "This project uses features only available in iOS SDK 3.0 and later." 12 | #endif 13 | 14 | #ifdef __OBJC__ 15 | #import 16 | #import 17 | #endif 18 | -------------------------------------------------------------------------------- /third_party/idevicewebinspectorproxy/Makefile: -------------------------------------------------------------------------------- 1 | PREFIX=/usr/local 2 | DEPS = $(LIBIMD_ROOT)/common/socket.h $(LIBIMD_ROOT)/common/thread.h $(LIBIMD_ROOT)/include/endianness.h 3 | OBJ = socket.o thread.o idevicewebinspectorproxy.o 4 | 5 | %.o: $(LIBIMD_ROOT)/common/%.c $(DEPS) 6 | gcc -c -o $@ $< 7 | 8 | idevicewebinspectorproxy: test-libimd-root $(OBJ) 9 | gcc -g $(filter-out $<,$^) -o $@ -lplist -limobiledevice 10 | rm *.o 11 | 12 | idevicewebinspectorproxy.o: idevicewebinspectorproxy.c 13 | gcc -c -o $@ -I$(LIBIMD_ROOT) -I$(LIBIMD_ROOT)/include $< 14 | 15 | test-libimd-root: 16 | test -n "$(LIBIMD_ROOT)" # $$LIBIMD_ROOT 17 | 18 | install: idevicewebinspectorproxy 19 | # Use mkdir -p first, because "install -D" not support on Mac 20 | mkdir -p $(PREFIX)/bin/ 21 | install $^ $(PREFIX)/bin/$^ 22 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/command/package-info.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | @CheckReturnValue 16 | @ParametersAreNonnullByDefault 17 | package com.google.iosdevicecontrol.command; 18 | 19 | import javax.annotation.CheckReturnValue; 20 | import javax.annotation.ParametersAreNonnullByDefault; 21 | -------------------------------------------------------------------------------- /OpenUrlApp/main.m: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #import 16 | 17 | #import "third_party/objective_c/iosdevicecontrol/openURL/OpenURLAppDelegate.h" 18 | 19 | int main(int argc, char *argv[]) 20 | { 21 | @autoreleasepool { 22 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([OpenURLAppDelegate class])); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /OpenUrlApp/OpenURLAppDelegate.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #import 18 | 19 | @interface OpenURLAppDelegate : UIResponder 20 | 21 | @property(strong, nonatomic) UIWindow *window; 22 | 23 | + (NSString *)getWiFiIpAddress; 24 | + (NSString *)getWiFiSsid; 25 | + (BOOL)openUrlWithString:(NSString *)urlString; 26 | 27 | @end 28 | 29 | void println(NSString *format, ...); 30 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution, 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/util/VoidCallable.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.util; 16 | 17 | import java.util.concurrent.Callable; 18 | 19 | /** 20 | * A task that may throw an exception. Like a {@link Callable} but with no return value. 21 | */ 22 | public interface VoidCallable { 23 | /** 24 | * Performs a computation, or throws an exception if unable to do so. 25 | */ 26 | void call() throws X; 27 | } 28 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/command/CommandExecutor.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.command; 16 | 17 | /** 18 | * Starts the execution of a {@link Command}. 19 | */ 20 | public interface CommandExecutor { 21 | /** 22 | * Starts the execution of the given command and returns the running process. 23 | * 24 | * @throws CommandStartException - if there was an error starting the command 25 | */ 26 | CommandProcess start(Command command) throws CommandStartException; 27 | } 28 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/util/CheckedCallable.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.util; 16 | 17 | import com.google.errorprone.annotations.CanIgnoreReturnValue; 18 | import java.util.concurrent.Callable; 19 | 20 | /** 21 | * A {@link Callable} which is guaranteed to only throw a checked exception of type {@code X} (or a 22 | * {@link RuntimeException}). 23 | */ 24 | public interface CheckedCallable { 25 | @CanIgnoreReturnValue 26 | V call() throws X; 27 | } 28 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/util/JsonParser.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.util; 16 | 17 | import java.io.StringReader; 18 | import javax.json.Json; 19 | import javax.json.JsonObject; 20 | import javax.json.JsonReader; 21 | 22 | /** Utility for parsing javax.json. */ 23 | public final class JsonParser { 24 | /** Parses a string to a {@link JsonObject}. */ 25 | public static JsonObject parseObject(String jsonString) { 26 | try (JsonReader reader = Json.createReader(new StringReader(jsonString))) { 27 | return reader.readObject(); 28 | } 29 | } 30 | 31 | private JsonParser() {} 32 | } 33 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/command/CommandStartException.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.command; 16 | 17 | /** 18 | * An exception thrown when a command fails to start. 19 | */ 20 | public class CommandStartException extends CommandException { 21 | /** 22 | * Constructs a new CommandStartException. 23 | */ 24 | public CommandStartException(Command command, String message) { 25 | super(command, message); 26 | } 27 | 28 | /** 29 | * Constructs a new CommandStartException. 30 | */ 31 | public CommandStartException(Command command, Exception cause) { 32 | super(command, cause); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/IosDeviceResource.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol; 16 | 17 | import static com.google.common.base.Preconditions.checkNotNull; 18 | 19 | /** A resource belonging to an iOS device. */ 20 | public abstract class IosDeviceResource implements AutoCloseable { 21 | private final IosDevice device; 22 | 23 | protected IosDeviceResource(IosDevice device) { 24 | this.device = checkNotNull(device); 25 | } 26 | 27 | /** Returns the device to which the resource belongs. */ 28 | public final IosDevice device() { 29 | return device; 30 | } 31 | 32 | @Override 33 | public abstract void close() throws IosDeviceException; 34 | } 35 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/util/ExceptionHandler.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.util; 16 | 17 | /** 18 | * Represents an operation that attempts to handle and recover from a thrown exception. 19 | */ 20 | public interface ExceptionHandler { 21 | /** 22 | * An exception handler that does nothing. 23 | */ 24 | public static final ExceptionHandler NOOP = 25 | new ExceptionHandler() { 26 | @Override 27 | public void handle(Exception exception) {} 28 | }; 29 | 30 | /** 31 | * Handles an exception, throwing an exception (possibly the same one) if unable to do so. 32 | */ 33 | void handle(E exception) throws Exception; 34 | } 35 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/command/CommandFailureException.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.command; 16 | 17 | import static com.google.common.base.Preconditions.checkNotNull; 18 | 19 | import com.google.common.base.Verify; 20 | 21 | /** 22 | * An exception thrown when a command result fails the command's success condition. 23 | */ 24 | public final class CommandFailureException extends CommandException { 25 | private final CommandResult result; 26 | 27 | CommandFailureException(Command command, CommandResult result) { 28 | super(command, checkNotNull(result, command).toString()); 29 | Verify.verify(!command().successCondition().apply(result), result.toString()); 30 | this.result = result; 31 | } 32 | 33 | public CommandResult result() { 34 | return result; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/webinspector/InspectorSocket.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.webinspector; 16 | 17 | import com.dd.plist.NSDictionary; 18 | import java.io.Closeable; 19 | import java.io.IOException; 20 | import java.util.Optional; 21 | 22 | /** A nexus for communication with a web inspector. */ 23 | public interface InspectorSocket extends Closeable { 24 | /** 25 | * Sends a plist to the web inspector. 26 | * 27 | * @throws IOException if an I/O error occurs. 28 | */ 29 | void sendMessage(NSDictionary message) throws IOException; 30 | 31 | /** 32 | * Blocks until a plist is received from the web inspector, or returns empty on EOF. 33 | * 34 | * @throws IOException if an I/O error occurs. 35 | */ 36 | Optional receiveMessage() throws IOException; 37 | } 38 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/testing/FakeIosDeviceHost.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.testing; 16 | 17 | import com.google.common.collect.ImmutableSet; 18 | import com.google.iosdevicecontrol.IosDevice; 19 | import com.google.iosdevicecontrol.IosDeviceHost; 20 | import java.io.IOException; 21 | import java.util.Set; 22 | 23 | /** Fake implementation of {@link IosDeviceHost}. */ 24 | public class FakeIosDeviceHost implements IosDeviceHost { 25 | private ImmutableSet connectedDevices = ImmutableSet.of(); 26 | 27 | @Override 28 | public ImmutableSet connectedDevices() throws IOException { 29 | return connectedDevices; 30 | } 31 | 32 | public void setConnectedDevices(Set connectedDevices) { 33 | this.connectedDevices = ImmutableSet.copyOf(connectedDevices); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/command/CommandException.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.command; 16 | 17 | import static com.google.common.base.Preconditions.checkNotNull; 18 | 19 | /** 20 | * An exception indicating a failure to execute a command. 21 | */ 22 | public abstract class CommandException extends Exception { 23 | private final Command command; 24 | 25 | CommandException(Command command, String message) { 26 | super(checkNotNull(command, message).toString() + ": " + message); 27 | this.command = command; 28 | } 29 | 30 | CommandException(Command command, Exception cause) { 31 | super(checkNotNull(command, cause).toString(), cause); 32 | this.command = command; 33 | } 34 | 35 | /** 36 | * The command that failed to execute. 37 | */ 38 | public final Command command() { 39 | return command; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/IosAppProcess.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol; 16 | 17 | import java.io.Reader; 18 | import java.time.Duration; 19 | import java.util.concurrent.TimeoutException; 20 | 21 | /** A running iOS application process. */ 22 | public interface IosAppProcess { 23 | /** 24 | * Kills the process and returns immediately. To kill and then wait for the process to terminate, 25 | * use kill().await(). 26 | */ 27 | IosAppProcess kill(); 28 | 29 | /** Waits for the app to terminate and returns its output. */ 30 | String await() throws IosDeviceException, InterruptedException; 31 | 32 | /** Waits for the app to terminate up to the specified timeout and returns its output. */ 33 | String await(Duration timeout) throws IosDeviceException, InterruptedException, TimeoutException; 34 | 35 | /** Returns a new reader to the UTF-8 encoded streamed output. */ 36 | Reader outputReader(); 37 | } 38 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/real/ConfigurationProfile.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.real; 16 | 17 | import com.google.auto.value.AutoValue; 18 | 19 | /** A configuration profile for a real iOS device. */ 20 | @AutoValue 21 | public abstract class ConfigurationProfile { 22 | public static Builder builder() { 23 | return new AutoValue_ConfigurationProfile.Builder(); 24 | } 25 | 26 | public abstract String displayName(); 27 | 28 | public abstract String identifier(); 29 | 30 | public abstract int version(); 31 | 32 | /** A builder for a configuration profile. */ 33 | @AutoValue.Builder 34 | public abstract static class Builder { 35 | public abstract Builder displayName(String displayName); 36 | 37 | public abstract Builder identifier(String identitifer); 38 | 39 | public abstract Builder version(int version); 40 | 41 | public abstract ConfigurationProfile build(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/webinspector/InspectorDriver.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.webinspector; 16 | 17 | import com.google.auto.value.AutoValue; 18 | 19 | /** 20 | * Information about a driver in an inspector message. 21 | * 22 | *

NOTE: As of iOS10, Safari issues reportConnectedDriverList messages, but so far they have only 23 | * contained empty dictionaries. Presumably they will eventually contain a list of "drivers" but 24 | * what these look like remains to be seen. Until then, the InspectorDriver is just a placeholder. 25 | */ 26 | @AutoValue 27 | public abstract class InspectorDriver extends MessageDict { 28 | /** Returns a new builder. */ 29 | public static Builder builder() { 30 | return new AutoValue_InspectorDriver.Builder(); 31 | } 32 | 33 | /** A builder for creating inspector drivers. */ 34 | @AutoValue.Builder 35 | public abstract static class Builder extends MessageDict.Builder { 36 | abstract Builder driverId(String driverId); 37 | 38 | @Override 39 | public abstract InspectorDriver build(); 40 | } 41 | 42 | abstract String driverId(); 43 | } 44 | -------------------------------------------------------------------------------- /javatests/com/google/iosdevicecontrol/webinspector/MessageSelectorTest.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.webinspector; 16 | 17 | import static com.google.common.truth.Truth.assertThat; 18 | import static com.google.common.truth.Truth.assertWithMessage; 19 | 20 | import org.junit.Test; 21 | import org.junit.runner.RunWith; 22 | import org.junit.runners.JUnit4; 23 | 24 | @RunWith(JUnit4.class) 25 | public class MessageSelectorTest { 26 | @Test 27 | public void testToString() { 28 | assertThat(MessageSelector.REPORT_SETUP.toString()).isEqualTo("_rpc_reportSetup:"); 29 | } 30 | 31 | @Test 32 | public void testForStringIsInverseOfToString() { 33 | for (MessageSelector selector : MessageSelector.values()) { 34 | assertThat(MessageSelector.forString(selector.toString())).isEqualTo(selector); 35 | } 36 | } 37 | 38 | @Test 39 | public void testForStringOfUnknownThrowsRuntimeException() { 40 | try { 41 | MessageSelector.forString("gobbledygook"); 42 | assertWithMessage("RuntimeException expected").fail(); 43 | } catch (RuntimeException expected) { 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/simulator/SimulatorDevice.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.simulator; 16 | 17 | import com.google.iosdevicecontrol.IosDevice; 18 | import com.google.iosdevicecontrol.IosDeviceException; 19 | 20 | /** Interface that extends {@link IosDevice} for Simulator specific commands and operations */ 21 | public interface SimulatorDevice extends IosDevice { 22 | /** 23 | * Shutdown this device and waits until it has completed the shutdown process. 24 | * 25 | * @throws IosDeviceException - if there was an error communicating with the device 26 | */ 27 | void shutdown() throws IosDeviceException; 28 | 29 | /** 30 | * Startup the device and wait until it has completed the boot process. 31 | * 32 | * @throws IosDeviceException - if there was an error communicating with the device 33 | */ 34 | void startup() throws IosDeviceException; 35 | 36 | /** 37 | * Shutdown the device, erase its content, and reset its settings to be factory new. 38 | * 39 | * @throws IosDeviceException - if there was an error communicating with the device 40 | */ 41 | void erase() throws IosDeviceException; 42 | } 43 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/IosDeviceHost.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol; 16 | 17 | import static com.google.common.base.Preconditions.checkNotNull; 18 | 19 | import com.google.common.collect.ImmutableSet; 20 | import java.io.IOException; 21 | import java.util.Optional; 22 | 23 | /** A host machine to which iOS devices are connected. */ 24 | public interface IosDeviceHost { 25 | /** 26 | * Returns the connected device with the given udid. 27 | * 28 | * @throws IOException - if unable to retrieve the connected device 29 | */ 30 | default IosDevice connectedDevice(String udid) throws IOException { 31 | checkNotNull(udid); 32 | Optional device = 33 | connectedDevices().stream().filter(d -> d.udid().equals(udid)).findAny(); 34 | if (!device.isPresent()) { 35 | throw new IOException("Device not connected: " + udid); 36 | } 37 | return device.get(); 38 | } 39 | 40 | /** 41 | * Returns all the devices currently connected to the host. 42 | * 43 | * @throws IOException - if unable to retrieve the list of connected devices 44 | */ 45 | ImmutableSet connectedDevices() throws IOException; 46 | } 47 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/util/StringEnumMap.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.util; 16 | 17 | import static com.google.common.base.Preconditions.checkArgument; 18 | import static com.google.common.collect.ImmutableMap.toImmutableMap; 19 | 20 | import com.google.common.collect.ImmutableMap; 21 | import java.util.stream.Stream; 22 | 23 | /** Immutable map from each enum value string to its value. */ 24 | public final class StringEnumMap> { 25 | private final ImmutableMap stringToValue; 26 | 27 | /** Constructs an immutable map to every value in the specified enum class. */ 28 | public StringEnumMap(Class enumClass) { 29 | E[] values = enumClass.getEnumConstants(); 30 | stringToValue = Stream.of(values).collect(toImmutableMap(v -> v.toString(), v -> v)); 31 | } 32 | 33 | /** 34 | * Returns the enum value with the given string representation. 35 | * 36 | * @throws IllegalArgumentException - if there is no such enum value. 37 | */ 38 | public E get(String string) { 39 | E value = stringToValue.get(string); 40 | checkArgument(value != null, "No value for string %s", string); 41 | return value; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/IosVersion.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol; 16 | 17 | import com.google.auto.value.AutoValue; 18 | 19 | /** 20 | * Version information of an iOS device. 21 | * 22 | * @see theiphonewiki.com 23 | */ 24 | @AutoValue 25 | public abstract class IosVersion { 26 | public static Builder builder() { 27 | return new AutoValue_IosVersion.Builder(); 28 | } 29 | 30 | /** The build version, e.g. "12H321". */ 31 | public abstract String buildVersion(); 32 | 33 | /** The product version, e.g. "8.4.1". */ 34 | public abstract String productVersion(); 35 | 36 | /** The major version number, e.g. 8. */ 37 | public final int majorVersion() { 38 | return Integer.parseInt(productVersion().substring(0, productVersion().indexOf('.'))); 39 | } 40 | 41 | public abstract Builder toBuilder(); 42 | 43 | /** 44 | * IosVersion builder. 45 | */ 46 | @AutoValue.Builder 47 | public abstract static class Builder { 48 | public abstract Builder buildVersion(String buildVersion); 49 | 50 | public abstract Builder productVersion(String productVersion); 51 | 52 | public abstract IosVersion build(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /OpenUrlApp/openURL-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | OpenUrl 9 | CFBundleExecutable 10 | OpenUrl 11 | CFBundleIdentifier 12 | com.google.openURL 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | OpenUrl 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1.0 25 | LSRequiresIPhoneOS 26 | 27 | CFBundleURLTypes 28 | 29 | 30 | CFBundleURLName 31 | com.google.openURL 32 | CFBundleURLSchemes 33 | 34 | openURL 35 | 36 | 37 | 38 | UIRequiredDeviceCapabilities 39 | 40 | armv7 41 | 42 | UISupportedInterfaceOrientations 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationLandscapeLeft 46 | UIInterfaceOrientationLandscapeRight 47 | 48 | UISupportedInterfaceOrientations~ipad 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationPortraitUpsideDown 52 | UIInterfaceOrientationLandscapeLeft 53 | UIInterfaceOrientationLandscapeRight 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /javatests/com/google/iosdevicecontrol/webinspector/MessageKeyTest.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.webinspector; 16 | 17 | import static com.google.common.truth.Truth.assertThat; 18 | import static com.google.common.truth.Truth.assertWithMessage; 19 | 20 | import org.junit.Test; 21 | import org.junit.runner.RunWith; 22 | import org.junit.runners.JUnit4; 23 | 24 | @RunWith(JUnit4.class) 25 | public class MessageKeyTest { 26 | @Test 27 | public void testToString() { 28 | assertThat(MessageKey.APPLICATION_IDENTIFIER.toString()) 29 | .isEqualTo("WIRApplicationIdentifierKey"); 30 | } 31 | 32 | @Test 33 | public void testUrlToString() { 34 | // The one exception to the otherwise nice toString conversion rule. 35 | assertThat(MessageKey.URL.toString()).isEqualTo("WIRURLKey"); 36 | } 37 | 38 | @Test 39 | public void testForStringIsInverseOfToString() { 40 | for (MessageKey key : MessageKey.values()) { 41 | assertThat(MessageKey.forString(key.toString())).isEqualTo(key); 42 | } 43 | } 44 | 45 | @Test 46 | public void testForStringOfUnknownThrowsRuntimeException() { 47 | try { 48 | MessageKey.forString("gobbledygook"); 49 | assertWithMessage("RuntimeException expected").fail(); 50 | } catch (RuntimeException expected) { 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/real/RealDevice.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.real; 16 | 17 | import com.google.common.collect.ImmutableList; 18 | import com.google.iosdevicecontrol.IosDevice; 19 | import com.google.iosdevicecontrol.IosDeviceException; 20 | import java.nio.file.Path; 21 | 22 | /** Interface that extends {@link IosDevice} for real device specific commands and operations */ 23 | public interface RealDevice extends IosDevice { 24 | /** Returns whether the device is currently restarting. */ 25 | boolean isRestarting(); 26 | 27 | /** Installs a configuration or provisioning profile at the specified path. */ 28 | void installProfile(Path profilePath) throws IosDeviceException; 29 | 30 | /** Removes a configuration profile with the specified identifier. */ 31 | void removeProfile(String identifier) throws IosDeviceException; 32 | 33 | /** Returns a list of the installed configuration profiles. */ 34 | ImmutableList listConfigurationProfiles() throws IosDeviceException; 35 | 36 | /** 37 | * Syncs the device's clock to the system time. 38 | * 39 | * @throws IosDeviceException - if there is an error communicating with the device 40 | */ 41 | void syncToSystemTime() throws IosDeviceException; 42 | 43 | /** 44 | * Returns the battery level of the device as an integer percentage in the range [0, 100]. 45 | * 46 | * @throws IosDeviceException - if there is an error communicating with the device 47 | */ 48 | int batteryLevel() throws IosDeviceException; 49 | } 50 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/webinspector/ReportIdentifierMessage.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.webinspector; 16 | 17 | import com.google.auto.value.AutoValue; 18 | 19 | /** 20 | * An reportIdentifier message. 21 | * 22 | *

Example: 23 | * 24 | *

{@code
25 |  * 
26 |  * 
28 |  * 
29 |  * 
30 |  *   __selector
31 |  *   _rpc_reportIdentifier:
32 |  *   __argument
33 |  *   
34 |  *     WIRConnectionIdentifierKey
35 |  *     17858421-36EF-4752-89F7-7A13ED5782C5
36 |  *   
37 |  * 
38 |  * 
39 |  * }
40 | */ 41 | @AutoValue 42 | public abstract class ReportIdentifierMessage extends InspectorMessage { 43 | /** Returns a new builder. */ 44 | public static Builder builder() { 45 | return new AutoValue_ReportIdentifierMessage.Builder(); 46 | } 47 | 48 | /** A builder for creating reportIdentifier messages. */ 49 | @AutoValue.Builder 50 | public abstract static class Builder extends InspectorMessage.Builder { 51 | @Override 52 | public abstract Builder connectionId(String connectionId); 53 | 54 | @Override 55 | public abstract ReportIdentifierMessage build(); 56 | } 57 | 58 | @Override 59 | public abstract String connectionId(); 60 | 61 | @Override 62 | public MessageSelector selector() { 63 | return MessageSelector.REPORT_IDENTIFIER; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/webinspector/InspectorPage.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.webinspector; 16 | 17 | import com.google.auto.value.AutoValue; 18 | import com.google.common.base.Optional; 19 | 20 | /** Information about a page in an inspector message. */ 21 | @AutoValue 22 | public abstract class InspectorPage extends MessageDict { 23 | /** Returns a new builder. */ 24 | public static Builder builder() { 25 | return new AutoValue_InspectorPage.Builder(); 26 | } 27 | 28 | /** A builder for creating inspector pages. */ 29 | @AutoValue.Builder 30 | public abstract static class Builder extends MessageDict.Builder { 31 | @Override 32 | public final Builder connectionId(String connectionId) { 33 | return optionalConnectionId(connectionId); 34 | } 35 | 36 | @Override 37 | public abstract Builder pageId(int pageId); 38 | 39 | @Override 40 | public abstract Builder title(String title); 41 | 42 | @Override 43 | public abstract Builder type(String type); 44 | 45 | @Override 46 | public abstract Builder url(String url); 47 | 48 | @Override 49 | public abstract InspectorPage build(); 50 | 51 | abstract Builder optionalConnectionId(String connectionId); 52 | } 53 | 54 | public abstract Optional optionalConnectionId(); 55 | 56 | @Override 57 | public abstract int pageId(); 58 | 59 | @Override 60 | public abstract String title(); 61 | 62 | @Override 63 | public abstract String type(); 64 | 65 | @Override 66 | public abstract String url(); 67 | 68 | @Override 69 | final String connectionId() { 70 | return fromOptional(optionalConnectionId()); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/examples/ExampleRealDeviceControl.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.examples; 16 | 17 | import com.google.iosdevicecontrol.real.RealDevice; 18 | import com.google.iosdevicecontrol.real.RealDeviceHost; 19 | import com.google.iosdevicecontrol.util.FluentLogger; 20 | import java.nio.file.Files; 21 | import java.nio.file.Path; 22 | 23 | /** 24 | * This class contains a real device use case for the IosDeviceControl library to document how the 25 | * library should be used and to verify that the open source build can compile and run successfully. 26 | */ 27 | public class ExampleRealDeviceControl { 28 | public static void main(String[] args) throws Exception { 29 | FluentLogger logger = FluentLogger.forEnclosingClass(); 30 | // Initialize the device host by providing disk images. 31 | RealDeviceHost.Configuration config = RealDeviceHost.withDeveloperDiskImagesFromXcode(); 32 | // Set a supervision identity for more control over the device. 33 | // config = config.withSupervisionIdentity(Paths.get("path/to/cert"), Paths.get("path/to/key")); 34 | RealDeviceHost realHost = config.initialize(); 35 | 36 | // Pick an arbitrary device 37 | RealDevice device = (RealDevice) realHost.connectedDevices().iterator().next(); 38 | // Or specify one by udid 39 | // RealDevice device = (RealDevice) realHost.connectedDevice(udid); 40 | 41 | // Pull some system logs then restart the device. 42 | Path logPath = Files.createTempDirectory("logs"); 43 | try (AutoCloseable systemLogger = device.startSystemLogger(logPath.resolve("sys.log"))) {} 44 | device.restart(); 45 | logger.atInfo().log("System logs written to: %s", logPath); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/webinspector/InspectorMessage.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.webinspector; 16 | 17 | import com.dd.plist.NSDictionary; 18 | import com.dd.plist.NSString; 19 | 20 | /** A message sent to or from the web inspector in the webkit remote debug protocol. */ 21 | public abstract class InspectorMessage extends MessageDict { 22 | private static final String SELECTOR_KEY = "__selector"; 23 | private static final String ARGUMENT_KEY = "__argument"; 24 | 25 | /** Converts the given plist to an inspector message. */ 26 | public static InspectorMessage fromPlist(NSDictionary plist) { 27 | try { 28 | NSString selectorString = (NSString) plist.get(SELECTOR_KEY); 29 | NSDictionary argumentDict = (NSDictionary) plist.get(ARGUMENT_KEY); 30 | MessageSelector selector = MessageSelector.forString(selectorString.getContent()); 31 | Builder messageBuilder = selector.newMessageBuilder(); 32 | messageBuilder.populateFromPlistDict(argumentDict); 33 | return messageBuilder.build(); 34 | } catch (RuntimeException e) { 35 | throw new IllegalArgumentException("Invalid plist: " + plist.toXMLPropertyList(), e); 36 | } 37 | } 38 | 39 | /** Converts this inspector message as a plist. */ 40 | public final NSDictionary toPlist() { 41 | NSDictionary plist = new NSDictionary(); 42 | plist.put(SELECTOR_KEY, selector().toString()); 43 | plist.put(ARGUMENT_KEY, toPlistDict()); 44 | return plist; 45 | } 46 | 47 | /** Returns the selector (the type of the message). */ 48 | public abstract MessageSelector selector(); 49 | 50 | /** Common supertype for all inspector message builders. */ 51 | public abstract static class Builder extends MessageDict.Builder { 52 | @Override 53 | public abstract InspectorMessage build(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/IosDeviceException.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol; 16 | 17 | import static com.google.common.base.Preconditions.checkNotNull; 18 | 19 | import com.google.common.base.Optional; 20 | import javax.annotation.Nullable; 21 | 22 | /** 23 | * Signals an error occurred when interacting with an iOS device. Each {@code IosDeviceException} 24 | * optionally provides a suggested {@link Remedy} that may resolve the error. 25 | */ 26 | public class IosDeviceException extends Exception { 27 | /** Remedies to some kinds of iOS device errors. */ 28 | public enum Remedy { 29 | DISMISS_DIALOG, 30 | REINSTALL_APP, 31 | RESTART_DEVICE, 32 | } 33 | 34 | private final IosDevice device; 35 | private final Optional remedy; 36 | 37 | public IosDeviceException(IosDevice device, String message) { 38 | this(device, device + ": " + message, null, Optional.absent()); 39 | } 40 | 41 | public IosDeviceException(IosDevice device, Throwable cause) { 42 | this(device, device.toString(), checkNotNull(cause), Optional.absent()); 43 | } 44 | 45 | public IosDeviceException(IosDevice device, Throwable cause, Remedy remedy) { 46 | this(device, device.toString(), checkNotNull(cause), Optional.of(remedy)); 47 | } 48 | 49 | private IosDeviceException( 50 | IosDevice device, String message, @Nullable Throwable cause, Optional remedy) { 51 | super(message, cause); 52 | this.device = checkNotNull(device); 53 | this.remedy = remedy; 54 | } 55 | 56 | /** Returns the device on which the error occurred. */ 57 | public final IosDevice device() { 58 | return device; 59 | } 60 | 61 | /** Suggested remedy that may resolve the cause of the error. */ 62 | public final Optional remedy() { 63 | return remedy; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/webinspector/ForwardGetListingMessage.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.webinspector; 16 | 17 | import com.google.auto.value.AutoValue; 18 | 19 | /** 20 | * A forwardGetListing message. 21 | * 22 | *

Example: 23 | * 24 | *

{@code
25 |  * 
26 |  * 
28 |  * 
29 |  * 
30 |  *   __selector
31 |  *   _rpc_forwardGetListing:
32 |  *   __argumentcommand
33 |  *   
34 |  *     WIRApplicationIdentifierKey
35 |  *     PID:176
36 |  *     WIRConnectionIdentifierKey
37 |  *     17858421-36EF-4752-89F7-7A13ED5782C5
38 |  *   
39 |  * 
40 |  * 
41 |  * }
42 | */ 43 | @AutoValue 44 | public abstract class ForwardGetListingMessage extends InspectorMessage { 45 | /** Returns a new builder. */ 46 | public static Builder builder() { 47 | return new AutoValue_ForwardGetListingMessage.Builder(); 48 | } 49 | 50 | /** A builder for creating forwardGetListing messages. */ 51 | @AutoValue.Builder 52 | public abstract static class Builder extends InspectorMessage.Builder { 53 | @Override 54 | public abstract Builder applicationId(String applicationId); 55 | 56 | @Override 57 | public abstract Builder connectionId(String connectionId); 58 | 59 | @Override 60 | public abstract ForwardGetListingMessage build(); 61 | } 62 | 63 | @Override 64 | public abstract String applicationId(); 65 | 66 | @Override 67 | public abstract String connectionId(); 68 | 69 | @Override 70 | public MessageSelector selector() { 71 | return MessageSelector.FORWARD_GET_LISTING; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/webinspector/ApplicationUpdatedMessage.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.webinspector; 16 | 17 | import com.google.auto.value.AutoValue; 18 | 19 | /** 20 | * An applicationUpdated message. 21 | * 22 | *

Example: 23 | * 24 | *

{@code
25 |  * 
26 |  * 
28 |  * 
29 |  * 
30 |  *   __selector
31 |  *   _rpc_applicationUpdated:
32 |  *   __argument
33 |  *   
34 |  *     WIRApplicationBundleIdentifierKey
35 |  *     com.apple.mobilesafari
36 |  *     WIRApplicationIdentifierKey
37 |  *     PID:176
38 |  *     WIRApplicationNameKey
39 |  *     Safari
40 |  *     WIRHostApplicationIdentifierKey
41 |  *     PID:457
42 |  *     WIRIsApplicationActiveKey
43 |  *     1
44 |  *     WIRIsApplicationProxyKey
45 |  *     
46 |  *   
47 |  * 
48 |  * 
49 |  * }
50 | */ 51 | @AutoValue 52 | public abstract class ApplicationUpdatedMessage extends ApplicationMessage { 53 | /** Returns an new builder. */ 54 | public static Builder builder() { 55 | return new AutoValue_ApplicationUpdatedMessage.Builder(); 56 | } 57 | 58 | /** A builder for creating applicationUpdated messages. */ 59 | @AutoValue.Builder 60 | public abstract static class Builder extends ApplicationMessage.Builder { 61 | @Override 62 | public abstract ApplicationUpdatedMessage build(); 63 | } 64 | 65 | @Override 66 | public MessageSelector selector() { 67 | return MessageSelector.APPLICATION_UPDATED; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/webinspector/ApplicationConnectedMessage.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.webinspector; 16 | 17 | import com.google.auto.value.AutoValue; 18 | 19 | /** 20 | * An applicationConnected message. 21 | * 22 | *

Example: 23 | * 24 | *

{@code
25 |  * 
26 |  * 
28 |  * 
29 |  * 
30 |  *   __selector
31 |  *   _rpc_applicationConnected:
32 |  *   __argument
33 |  *   
34 |  *     WIRApplicationBundleIdentifierKey
35 |  *     com.apple.mobilesafari
36 |  *     WIRApplicationIdentifierKey
37 |  *     PID:176
38 |  *     WIRApplicationNameKey
39 |  *     Safari
40 |  *     WIRHostApplicationIdentifierKey
41 |  *     PID:457
42 |  *     WIRIsApplicationActiveKey
43 |  *     1
44 |  *     WIRIsApplicationProxyKey
45 |  *     
46 |  *   
47 |  * 
48 |  * 
49 |  * }
50 | */ 51 | @AutoValue 52 | public abstract class ApplicationConnectedMessage extends ApplicationMessage { 53 | /** Returns a new builder. */ 54 | public static Builder builder() { 55 | return new AutoValue_ApplicationConnectedMessage.Builder(); 56 | } 57 | 58 | /** A builder for creating applicationConnected messages. */ 59 | @AutoValue.Builder 60 | public abstract static class Builder extends ApplicationMessage.Builder { 61 | @Override 62 | public abstract ApplicationConnectedMessage build(); 63 | } 64 | 65 | @Override 66 | public MessageSelector selector() { 67 | return MessageSelector.APPLICATION_CONNECTED; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/testing/FakeIosDeviceSocket.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.testing; 16 | 17 | import com.google.common.io.ByteStreams; 18 | import com.google.iosdevicecontrol.IosDevice; 19 | import com.google.iosdevicecontrol.IosDeviceException; 20 | import com.google.iosdevicecontrol.IosDeviceSocket; 21 | import java.io.ByteArrayInputStream; 22 | import java.io.ByteArrayOutputStream; 23 | import java.io.IOException; 24 | 25 | /** Fake implementaton of IosDeviceSocket. s */ 26 | public class FakeIosDeviceSocket extends IosDeviceSocket { 27 | private volatile boolean closed; 28 | private final ByteArrayInputStream input; 29 | private final ByteArrayOutputStream output; 30 | 31 | public FakeIosDeviceSocket(IosDevice device, byte[] inputBytes) { 32 | super(device); 33 | input = new ByteArrayInputStream(inputBytes); 34 | output = new ByteArrayOutputStream(); 35 | } 36 | 37 | @Override 38 | public int read(byte[] bytes) throws IosDeviceException { 39 | if (closed) { 40 | throw new IosDeviceException(device(), "socket closed"); 41 | } 42 | try { 43 | return ByteStreams.read(input, bytes, 0, bytes.length); 44 | } catch (IOException e) { 45 | throw new IosDeviceException(device(), e); 46 | } 47 | } 48 | 49 | @Override 50 | public void write(byte[] bytes) throws IosDeviceException { 51 | if (closed) { 52 | throw new IosDeviceException(device(), "socket closed"); 53 | } 54 | try { 55 | output.write(bytes); 56 | } catch (IOException e) { 57 | throw new IosDeviceException(device(), e); 58 | } 59 | } 60 | 61 | @Override 62 | public void close() throws IosDeviceException { 63 | closed = true; 64 | } 65 | 66 | /** Return an array of all bytes written to the socket. */ 67 | public byte[] bytesWritten() { 68 | return output.toByteArray(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/webinspector/ApplicationDisconnectedMessage.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.webinspector; 16 | 17 | import com.google.auto.value.AutoValue; 18 | 19 | /** 20 | * An applicationDisconnected message. 21 | * 22 | *

Example: 23 | * 24 | *

{@code
25 |  * 
26 |  * 
28 |  * 
29 |  * 
30 |  *   __selector
31 |  *   _rpc_applicationDisconnected:
32 |  *   __argument
33 |  *   
34 |  *     WIRApplicationBundleIdentifierKey
35 |  *     com.apple.mobilesafari
36 |  *     WIRApplicationIdentifierKey
37 |  *     PID:176
38 |  *     WIRApplicationNameKey
39 |  *     Safari
40 |  *     WIRHostApplicationIdentifierKey
41 |  *     PID:457
42 |  *     WIRIsApplicationActiveKey
43 |  *     1
44 |  *     WIRIsApplicationProxyKey
45 |  *     
46 |  *   
47 |  * 
48 |  * 
49 |  * }
50 | */ 51 | @AutoValue 52 | public abstract class ApplicationDisconnectedMessage extends ApplicationMessage { 53 | /** Returns a new builder. */ 54 | public static Builder builder() { 55 | return new AutoValue_ApplicationDisconnectedMessage.Builder(); 56 | } 57 | 58 | /** A builder for creating applicationDisconnected messages. */ 59 | @AutoValue.Builder 60 | public abstract static class Builder extends ApplicationMessage.Builder { 61 | @Override 62 | public abstract ApplicationDisconnectedMessage build(); 63 | } 64 | 65 | @Override 66 | public MessageSelector selector() { 67 | return MessageSelector.APPLICATION_DISCONNECTED; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/openurl/AppResult.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.openurl; 16 | 17 | import com.google.auto.value.AutoValue; 18 | import com.google.common.annotations.VisibleForTesting; 19 | import java.net.InetAddress; 20 | import java.util.Optional; 21 | 22 | /** A result of running the openURL app. */ 23 | public abstract class AppResult { 24 | private final String rawOutput; 25 | 26 | private AppResult(String rawOutput) { 27 | this.rawOutput = rawOutput; 28 | } 29 | 30 | public final String rawOutput() { 31 | return rawOutput; 32 | } 33 | 34 | /** Result of asking the app to open a url. */ 35 | public static final class OpenUrlResult extends AppResult { 36 | private final boolean success; 37 | 38 | @VisibleForTesting 39 | public OpenUrlResult(String rawOutput, boolean success) { 40 | super(rawOutput); 41 | this.success = success; 42 | } 43 | 44 | public boolean success() { 45 | return success; 46 | } 47 | } 48 | 49 | /** Result of asking the app to check the wifi connection. */ 50 | public static final class CheckWifiResult extends AppResult { 51 | private final Optional connection; 52 | 53 | @VisibleForTesting 54 | public CheckWifiResult(String rawOutput, Optional connection) { 55 | super(rawOutput); 56 | this.connection = connection; 57 | } 58 | 59 | public Optional connection() { 60 | return connection; 61 | } 62 | } 63 | 64 | /** Information about the WiFi connection of the device. */ 65 | @AutoValue 66 | public abstract static class WifiConnection { 67 | public static WifiConnection create(InetAddress ip, String ssid) { 68 | return new AutoValue_AppResult_WifiConnection(ip, ssid); 69 | } 70 | 71 | public abstract InetAddress ip(); 72 | 73 | public abstract String ssid(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/examples/ExampleSimulatorDeviceControl.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.examples; 16 | 17 | import com.google.iosdevicecontrol.IosAppBundleId; 18 | import com.google.iosdevicecontrol.IosAppProcess; 19 | import com.google.iosdevicecontrol.simulator.SimulatorDevice; 20 | import com.google.iosdevicecontrol.simulator.SimulatorDeviceHost; 21 | import com.google.iosdevicecontrol.util.FluentLogger; 22 | import java.nio.file.Files; 23 | import java.nio.file.Path; 24 | 25 | /** 26 | * This class contains a simulator use case for the IosDeviceControl library to document how the 27 | * library should be used and to verify that the open source build can compile and run successfully. 28 | */ 29 | public class ExampleSimulatorDeviceControl { 30 | public static void main(String[] args) throws Exception { 31 | FluentLogger logger = FluentLogger.forEnclosingClass(); 32 | SimulatorDeviceHost simHost = SimulatorDeviceHost.INSTANCE; 33 | 34 | // Optionally shutdown all devices before starting one. 35 | simHost.shutdownAllDevices(); 36 | 37 | // Pick an arbitrary device. 38 | SimulatorDevice sim = (SimulatorDevice) simHost.connectedDevices().iterator().next(); 39 | // Or specify one by udid 40 | // SimulatorDevice sim = (SimulatorDevice) simHost.connectedDevice(udid); 41 | 42 | // Start the device before interacting with it. 43 | sim.startup(); 44 | 45 | // The device can now be interacted with. Here is an example of starting Safari, taking a 46 | // screenshot, then closing Safari 47 | IosAppProcess safariProcess = sim.runApplication(new IosAppBundleId("com.apple.mobilesafari")); 48 | byte[] screenshotBytes = sim.takeScreenshot(); 49 | Path screenshotPath = Files.createTempFile("screenshot", ".png"); 50 | Files.write(screenshotPath, screenshotBytes); 51 | safariProcess.kill(); 52 | logger.atInfo().log("Screenshot written to: %s", screenshotPath); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/simulator/SimulatorAppProcess.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.simulator; 16 | 17 | import static com.google.common.base.Preconditions.checkNotNull; 18 | import static java.util.concurrent.TimeUnit.NANOSECONDS; 19 | 20 | import com.google.iosdevicecontrol.command.CommandFailureException; 21 | import com.google.iosdevicecontrol.command.CommandProcess; 22 | import com.google.iosdevicecontrol.IosAppProcess; 23 | import com.google.iosdevicecontrol.IosDevice; 24 | import com.google.iosdevicecontrol.IosDeviceException; 25 | import java.io.Reader; 26 | import java.time.Duration; 27 | import java.util.concurrent.TimeoutException; 28 | 29 | /** Implementation of {@link IosAppProcess} backed by an `xcrun simctl` command process. */ 30 | final class SimulatorAppProcess implements IosAppProcess { 31 | private final IosDevice device; 32 | private final CommandProcess process; 33 | 34 | SimulatorAppProcess(IosDevice device, CommandProcess process) { 35 | this.device = checkNotNull(device); 36 | this.process = checkNotNull(process); 37 | } 38 | 39 | @Override 40 | public SimulatorAppProcess kill() { 41 | process.kill(); 42 | return this; 43 | } 44 | 45 | @Override 46 | public String await() throws IosDeviceException, InterruptedException { 47 | try { 48 | return process.await().stderrStringUtf8(); 49 | } catch (CommandFailureException e) { 50 | throw new IosDeviceException(device, e); 51 | } 52 | } 53 | 54 | @Override 55 | public String await(Duration timeout) 56 | throws IosDeviceException, InterruptedException, TimeoutException { 57 | try { 58 | return process.await(timeout.getNano(), NANOSECONDS).stderrStringUtf8(); 59 | } catch (CommandFailureException e) { 60 | throw new IosDeviceException(device, e); 61 | } 62 | } 63 | 64 | @Override 65 | public Reader outputReader() { 66 | return process.stderrReaderUtf8(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/webinspector/WebInspector.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.webinspector; 16 | 17 | import static com.google.common.base.Preconditions.checkNotNull; 18 | 19 | import com.google.common.annotations.VisibleForTesting; 20 | import java.io.Closeable; 21 | import java.io.IOException; 22 | import java.util.Optional; 23 | 24 | /** A web inspector. */ 25 | public final class WebInspector implements Closeable { 26 | /** Connects to a web inspector running on a real device. */ 27 | public static WebInspector connectToRealDevice(String udid) throws IOException { 28 | return connect(BinaryPlistSocket.openToRealDevice(udid)); 29 | } 30 | 31 | /** Connects to a web inspector running on a simulator. */ 32 | public static WebInspector connectToSimulator() throws IOException { 33 | return connect(BinaryPlistSocket.openToSimulator()); 34 | } 35 | 36 | /** 37 | * Connect to the application with the specified application bundle identifier, using the 38 | * specified socket factory and notifying the specified listener of devtools messages. 39 | */ 40 | private static WebInspector connect(InspectorSocket socket) throws IOException { 41 | return new WebInspector(socket); 42 | } 43 | 44 | private final InspectorSocket socket; 45 | 46 | @VisibleForTesting 47 | public WebInspector(InspectorSocket socket) { 48 | this.socket = checkNotNull(socket); 49 | } 50 | 51 | /** Sends a message to the web inspector. */ 52 | public void sendMessage(InspectorMessage message) throws IOException { 53 | socket.sendMessage(message.toPlist()); 54 | } 55 | 56 | /** Receives a message from the inspector socket or empty if the device socket is closed. */ 57 | public Optional receiveMessage() throws IOException { 58 | return socket.receiveMessage().map(InspectorMessage::fromPlist); 59 | } 60 | 61 | @Override 62 | public void close() throws IOException { 63 | socket.close(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/IosAppBundleId.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol; 16 | 17 | import static com.google.common.base.Preconditions.checkArgument; 18 | 19 | import java.io.IOException; 20 | import java.nio.file.Path; 21 | import java.util.Objects; 22 | import java.util.regex.Pattern; 23 | 24 | /** 25 | * An iOS application bundle identifier; normally a reverse DNS string, e.g. com.apple.mobilesafari. 26 | */ 27 | public final class IosAppBundleId { 28 | // Implementation note: I didn't use @AutoValue, because I didn't want to add an additional 29 | // property method to this class beyond toString. See: https://github.com/google/auto/issues/357 30 | 31 | private static final Pattern VALID_UTI_PATTERN = Pattern.compile("^[\\w-\\.]+$"); 32 | 33 | /** 34 | * Returns the bundle identifier read from a ".ipa" application archive file. This is equivalent 35 | * to {@code IosAppInfo.readFromIpa(ipaPath).bundleId()}. 36 | */ 37 | public static IosAppBundleId readFromIpa(Path ipaPath) throws IOException { 38 | return IosAppInfo.readFromPath(ipaPath).bundleId(); 39 | } 40 | 41 | private final String string; 42 | 43 | /** 44 | * Constructs a bundle ID from the given string. This constructor checks that the specified string 45 | * is a valid Apple Uniform Type Identifier (UTI), meaning it contains only alphanumeric 46 | * (A-Z,a-z,0-9), hyphen (-), and period (.) characters. 47 | * 48 | * @throws IllegalArgumentException if the string is not a valid Apple UTI. 49 | */ 50 | public IosAppBundleId(String string) { 51 | checkArgument(VALID_UTI_PATTERN.matcher(string).matches()); 52 | this.string = string; 53 | } 54 | 55 | @Override 56 | public String toString() { 57 | return string; 58 | } 59 | 60 | @Override 61 | public int hashCode() { 62 | return Objects.hashCode(string); 63 | } 64 | 65 | @Override 66 | public boolean equals(Object o) { 67 | return o instanceof IosAppBundleId && string.equals(((IosAppBundleId) o).string); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/webinspector/ReportConnectedDriverListMessage.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.webinspector; 16 | 17 | import com.google.auto.value.AutoValue; 18 | import com.google.common.collect.ImmutableList; 19 | import java.util.List; 20 | 21 | /** 22 | * A reportConnectedDriverList message. 23 | * 24 | *

NOTE: As of iOS10, Safari issues reportConnectedDriverList messages, but so far they have only 25 | * contained empty dictionaries. Presumably they will eventually contain a list of "drivers" but 26 | * what these look like remains to be seen. Until then, the InspectorDriver is just a placeholder. 27 | * 28 | *

Example: 29 | * 30 | *

{@code
31 |  * 
32 |  * 
34 |  * 
35 |  * 
36 |  *   __selector
37 |  *   _rpc_reportConnectedDriverList:
38 |  *   __argument
39 |  *   
40 |  *     WIRDriverDictionaryKey
41 |  *     
42 |  *   
43 |  * 
44 |  * 
45 |  * }
46 | */ 47 | @AutoValue 48 | public abstract class ReportConnectedDriverListMessage extends InspectorMessage { 49 | /** Returns a new builder. */ 50 | public static Builder builder() { 51 | return new AutoValue_ReportConnectedDriverListMessage.Builder(); 52 | } 53 | 54 | /** A builder for creating reportConnectedDriverList messages. */ 55 | @AutoValue.Builder 56 | public abstract static class Builder extends InspectorMessage.Builder { 57 | @Override 58 | public abstract Builder driverDictionary(List driverDictionary); 59 | 60 | @Override 61 | public abstract ReportConnectedDriverListMessage build(); 62 | } 63 | 64 | @Override 65 | public abstract ImmutableList driverDictionary(); 66 | 67 | @Override 68 | public MessageSelector selector() { 69 | return MessageSelector.REPORT_CONNECTED_DRIVER_LIST; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/webinspector/ApplicationSentDataMessage.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.webinspector; 16 | 17 | import com.google.auto.value.AutoValue; 18 | import javax.json.JsonObject; 19 | 20 | /** 21 | * An applicationSentData message. 22 | * 23 | *

Example: 24 | * 25 | *

{@code
26 |  * 
27 |  * 
29 |  * 
30 |  * 
31 |  *   __selector
32 |  *   _rpc_applicationSentData:
33 |  *   __argument
34 |  *   
35 |  *     WIRApplicationIdentifierKey
36 |  *     com.apple.mobilesafari
37 |  *     WIRDestinationKey
38 |  *     C1EAD225-D6BC-44B9-9089-2D7CC2D2204C
39 |  *     WIRMessageDataKey
40 |  *     }{"id": 1, "result": true}{@code
41 |  *   
42 |  * 
43 |  * 
44 |  * }
45 | */ 46 | @AutoValue 47 | public abstract class ApplicationSentDataMessage extends InspectorMessage { 48 | /** Returns a new builder. */ 49 | public static Builder builder() { 50 | return new AutoValue_ApplicationSentDataMessage.Builder(); 51 | } 52 | 53 | /** A builder for creating applicationSentData messages. */ 54 | @AutoValue.Builder 55 | public abstract static class Builder extends InspectorMessage.Builder { 56 | @Override 57 | public abstract Builder applicationId(String applicationId); 58 | 59 | @Override 60 | public abstract Builder destination(String destination); 61 | 62 | @Override 63 | public abstract Builder messageData(JsonObject messageData); 64 | 65 | @Override 66 | public abstract ApplicationSentDataMessage build(); 67 | } 68 | 69 | @Override 70 | public abstract String applicationId(); 71 | 72 | @Override 73 | public abstract String destination(); 74 | 75 | @Override 76 | public abstract JsonObject messageData(); 77 | 78 | @Override 79 | public MessageSelector selector() { 80 | return MessageSelector.APPLICATION_SENT_DATA; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/command/CommandResult.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.command; 16 | 17 | import static java.nio.charset.StandardCharsets.UTF_8; 18 | 19 | import com.google.common.base.MoreObjects; 20 | import java.nio.charset.Charset; 21 | 22 | /** The result of executing a {@link Command}. */ 23 | public final class CommandResult { 24 | private final int exitCode; 25 | private final CapturingOutputStream stdout; 26 | private final CapturingOutputStream stderr; 27 | 28 | CommandResult(int exitCode, CapturingOutputStream stdout, CapturingOutputStream stderr) { 29 | this.exitCode = exitCode; 30 | this.stdout = stdout; 31 | this.stderr = stderr; 32 | } 33 | 34 | /** Returns the exit code of the command execution. */ 35 | public int exitCode() { 36 | return exitCode; 37 | } 38 | 39 | /** Returns the standard output of the command execution as a new array of bytes. */ 40 | public byte[] stdoutBytes() { 41 | return stdout.toByteArray(); 42 | } 43 | 44 | /** Returns the standard error of the command execution as a new array of bytes. */ 45 | public byte[] stderrBytes() { 46 | return stderr.toByteArray(); 47 | } 48 | 49 | /** Returns the standard output of the command execution as a string. */ 50 | public String stdoutString(Charset cs) { 51 | return stdout.toString(cs); 52 | } 53 | 54 | /** Returns the standard error of the command execution as a string. */ 55 | public String stderrString(Charset cs) { 56 | return stderr.toString(cs); 57 | } 58 | 59 | /** Returns the standard output of the command execution as a UTF-8 string. */ 60 | public String stdoutStringUtf8() { 61 | return stdoutString(UTF_8); 62 | } 63 | 64 | /** Returns the standard error of the command execution as a UTF-8 string. */ 65 | public String stderrStringUtf8() { 66 | return stderrString(UTF_8); 67 | } 68 | 69 | @Override 70 | public String toString() { 71 | return MoreObjects.toStringHelper(this) 72 | .add("exit code", exitCode) 73 | .add("stdout", stdoutStringUtf8()) 74 | .add("stderr", stderrStringUtf8()) 75 | .toString(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /javatests/com/google/iosdevicecontrol/webinspector/WebInspectorTest.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.webinspector; 16 | 17 | import static com.google.common.truth.Truth.assertThat; 18 | import static com.google.common.truth.Truth8.assertThat; 19 | 20 | import com.google.iosdevicecontrol.testing.FakeInspectorSocket; 21 | import java.io.IOException; 22 | import org.junit.Before; 23 | import org.junit.Test; 24 | import org.junit.runner.RunWith; 25 | import org.junit.runners.JUnit4; 26 | 27 | /** Created by gdennis on 6/6/17. */ 28 | @RunWith(JUnit4.class) 29 | public class WebInspectorTest { 30 | private static final InspectorMessage MESSAGE1 = 31 | ReportIdentifierMessage.builder().connectionId("id1").build(); 32 | private static final InspectorMessage MESSAGE2 = 33 | ApplicationConnectedMessage.builder() 34 | .applicationBundleId("com.apple.mobile.safari") 35 | .applicationId("123") 36 | .applicationName("Safari") 37 | .isApplicationActive(true) 38 | .isApplicationProxy(false) 39 | .build(); 40 | 41 | private FakeInspectorSocket fakeInspectorSocket; 42 | private WebInspector inspector; 43 | 44 | @Before 45 | public void setup() { 46 | fakeInspectorSocket = new FakeInspectorSocket(); 47 | inspector = new WebInspector(fakeInspectorSocket); 48 | } 49 | 50 | @Test 51 | public void testSendMessage() throws IOException { 52 | inspector.sendMessage(MESSAGE1); 53 | inspector.sendMessage(MESSAGE2); 54 | assertThat(fakeInspectorSocket.dequeueMessagesSent()).containsExactly(MESSAGE1, MESSAGE2); 55 | } 56 | 57 | @Test 58 | public void testReceiveMessage() throws IOException { 59 | fakeInspectorSocket.enqueueMessageToReceive(MESSAGE1); 60 | fakeInspectorSocket.enqueueMessageToReceive(MESSAGE2); 61 | assertThat(inspector.receiveMessage()).hasValue(MESSAGE1); 62 | assertThat(inspector.receiveMessage()).hasValue(MESSAGE2); 63 | } 64 | 65 | @Test 66 | public void testClose() throws IOException { 67 | assertThat(fakeInspectorSocket.isClosed()).isFalse(); 68 | inspector.close(); 69 | assertThat(fakeInspectorSocket.isClosed()).isTrue(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/testing/FakeOpenUrlApp.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.testing; 16 | 17 | import com.google.iosdevicecontrol.IosAppBundleId; 18 | import com.google.iosdevicecontrol.IosAppInfo; 19 | import com.google.iosdevicecontrol.IosDevice; 20 | import com.google.iosdevicecontrol.IosDeviceException; 21 | import com.google.iosdevicecontrol.openurl.AppResult.CheckWifiResult; 22 | import com.google.iosdevicecontrol.openurl.AppResult.OpenUrlResult; 23 | import com.google.iosdevicecontrol.openurl.AppResult.WifiConnection; 24 | import com.google.iosdevicecontrol.openurl.OpenUrlApp; 25 | import java.net.URI; 26 | import java.nio.file.Path; 27 | import java.util.Optional; 28 | 29 | /** A fake OpenUrlApp for testing. */ 30 | public class FakeOpenUrlApp extends OpenUrlApp { 31 | private static final IosAppInfo FAKE_APP_INFO = 32 | IosAppInfo.builder().bundleId(new IosAppBundleId("fake.google.OpenUrl")).build(); 33 | 34 | private Optional wifiConnection = Optional.empty(); 35 | 36 | public FakeOpenUrlApp() {} 37 | 38 | public FakeOpenUrlApp(WifiConnection wifiConnection) { 39 | setWifiConnection(wifiConnection); 40 | } 41 | 42 | @Override 43 | public IosAppInfo appInfo() { 44 | return FAKE_APP_INFO; 45 | } 46 | 47 | @Override 48 | public Path ipaPath() { 49 | // TODO(user): Create a FakeIosApp class and return an instance of it here. 50 | // Once that is done, consider moving this class to the java ios/testing directory. 51 | throw new UnsupportedOperationException(); 52 | } 53 | 54 | @Override 55 | public OpenUrlResult openUrl(IosDevice device, URI url) { 56 | return new OpenUrlResult("fake output", true); 57 | } 58 | 59 | @Override 60 | public void openBlankPage(IosDevice device) {} 61 | 62 | @Override 63 | public CheckWifiResult checkWifi(IosDevice device) throws IosDeviceException { 64 | return new CheckWifiResult("fake output", wifiConnection); 65 | } 66 | 67 | public FakeOpenUrlApp setWifiConnection(WifiConnection wifiConnection) { 68 | this.wifiConnection = Optional.of(wifiConnection); 69 | return this; 70 | } 71 | 72 | public FakeOpenUrlApp clearWifiConnection() { 73 | wifiConnection = Optional.empty(); 74 | return this; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/webinspector/MessageSelector.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.webinspector; 16 | 17 | import static com.google.common.base.CaseFormat.LOWER_CAMEL; 18 | import static com.google.common.base.CaseFormat.UPPER_UNDERSCORE; 19 | 20 | import com.google.iosdevicecontrol.util.StringEnumMap; 21 | import java.util.function.Supplier; 22 | 23 | /** The type of a web inspector protocol message. */ 24 | public enum MessageSelector { 25 | APPLICATION_CONNECTED(ApplicationConnectedMessage::builder), 26 | APPLICATION_DISCONNECTED(ApplicationDisconnectedMessage::builder), 27 | APPLICATION_SENT_DATA(ApplicationSentDataMessage::builder), 28 | APPLICATION_SENT_LISTING(ApplicationSentListingMessage::builder), 29 | APPLICATION_UPDATED(ApplicationUpdatedMessage::builder), 30 | FORWARD_GET_LISTING(ForwardGetListingMessage::builder), 31 | FORWARD_SOCKET_DATA(ForwardSocketDataMessage::builder), 32 | FORWARD_SOCKET_SETUP(ForwardSocketSetupMessage::builder), 33 | REPORT_CONNECTED_APPLICATION_LIST(ReportConnectedApplicationListMessage::builder), 34 | REPORT_CONNECTED_DRIVER_LIST(ReportConnectedDriverListMessage::builder), 35 | REPORT_IDENTIFIER(ReportIdentifierMessage::builder), 36 | REPORT_SETUP(ReportSetupMessage::builder); 37 | 38 | private static final StringEnumMap STRING_TO_SELECTOR = 39 | new StringEnumMap<>(MessageSelector.class); 40 | 41 | /** 42 | * Returns the MessageSelector for the specified string, e.g. returns 43 | * REPORT_SETUP for the string "_rpc_reportSetup". 44 | */ 45 | public static MessageSelector forString(String s) { 46 | return STRING_TO_SELECTOR.get(s); 47 | } 48 | 49 | private final String string; 50 | 51 | @SuppressWarnings("ImmutableEnumChecker") 52 | private final Supplier newMessageBuilder; 53 | 54 | private MessageSelector(Supplier newMessageBuilder) { 55 | // E.g. "REPORT_SETUP" becomes "_rpc_reportSetup:" 56 | string = "_rpc_" + UPPER_UNDERSCORE.to(LOWER_CAMEL, name()) + ":"; 57 | this.newMessageBuilder = newMessageBuilder; 58 | } 59 | 60 | InspectorMessage.Builder newMessageBuilder() { 61 | return newMessageBuilder.get(); 62 | } 63 | 64 | @Override 65 | public String toString() { 66 | return string; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /javatests/com/google/iosdevicecontrol/util/ResourceTest.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.util; 16 | 17 | import static com.google.common.truth.Truth.assertThat; 18 | import static com.google.common.truth.Truth.assert_; 19 | 20 | import com.google.iosdevicecontrol.util.Resource.ResourceToPathCopier; 21 | import java.io.IOException; 22 | import java.nio.file.Files; 23 | import java.nio.file.Path; 24 | import org.junit.Test; 25 | import org.junit.runner.RunWith; 26 | import org.junit.runners.JUnit4; 27 | 28 | /** Unit tests for {@link com.google.iosdevicecontrol.util.Resource}. */ 29 | @RunWith(JUnit4.class) 30 | public class ResourceTest { 31 | 32 | @Test 33 | public void testCanExtractResourceToPath() throws IOException { 34 | Resource resource = Resource.named("com/google/iosdevicecontrol/util/resource1"); 35 | Path path = resource.toPath(); 36 | assertThat(Files.exists(path)).isTrue(); 37 | } 38 | 39 | @Test 40 | public void testExtractFailsWhenFileCopyingFails() { 41 | Resource resource = 42 | Resource.named( 43 | "com/google/iosdevicecontrol/util/resource1", 44 | new ResourceToPathCopier() { 45 | @Override 46 | public Path copy(String name) throws IOException { 47 | throw new IOException(); 48 | } 49 | }); 50 | try { 51 | resource.toPath(); 52 | assert_().fail(); 53 | } catch (IOException expected) { 54 | } 55 | } 56 | 57 | @Test 58 | public void testCannotConstructNonExistentResource() { 59 | try { 60 | Resource.named("i/do/not/exist"); 61 | assert_().fail(); 62 | } catch (IllegalArgumentException expected) { 63 | } 64 | } 65 | 66 | @Test 67 | public void testResourcesWithDifferentNamesAreNotEqual() { 68 | Resource resource1 = Resource.named("com/google/iosdevicecontrol/util/resource1"); 69 | Resource resource2 = Resource.named("com/google/iosdevicecontrol/util/resource2"); 70 | assertThat(resource1).isNotEqualTo(resource2); 71 | } 72 | 73 | @Test 74 | public void testResourcesWithSameNameAreTheSameInstance() { 75 | Resource resource1a = Resource.named("com/google/iosdevicecontrol/util/resource1"); 76 | Resource resource1b = Resource.named("com/google/iosdevicecontrol/util/resource1"); 77 | assertThat(resource1a).isSameAs(resource1b); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/IosAppInfo.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol; 16 | 17 | import com.dd.plist.NSDictionary; 18 | import com.dd.plist.NSObject; 19 | import com.google.auto.value.AutoValue; 20 | import com.google.common.collect.MoreCollectors; 21 | import com.google.common.io.MoreFiles; 22 | import com.google.iosdevicecontrol.util.PlistParser; 23 | import java.io.IOException; 24 | import java.nio.file.FileSystem; 25 | import java.nio.file.FileSystems; 26 | import java.nio.file.Files; 27 | import java.nio.file.Path; 28 | 29 | /** Information about an iOS application. */ 30 | @AutoValue 31 | public abstract class IosAppInfo { 32 | /** Returns the application info read from either an app folder or ipa archive */ 33 | public static IosAppInfo readFromPath(Path ipaOrAppPath) throws IOException { 34 | NSObject plistDict; 35 | if (Files.isDirectory(ipaOrAppPath)) { 36 | plistDict = PlistParser.fromPath(ipaOrAppPath.resolve("Info.plist")); 37 | } else { 38 | try (FileSystem ipaFs = FileSystems.newFileSystem(ipaOrAppPath, null)) { 39 | Path appPath = 40 | MoreFiles.listFiles(ipaFs.getPath("Payload")) 41 | .stream() 42 | // Can't use Files.isDirectory, because no entry is a "directory" in a zip. 43 | .filter(e -> e.toString().endsWith(".app/")) 44 | .collect(MoreCollectors.onlyElement()); 45 | plistDict = PlistParser.fromPath(appPath.resolve("Info.plist")); 46 | } 47 | } 48 | return readFromPlistDictionary((NSDictionary) plistDict); 49 | } 50 | 51 | /** Returns the application info read from a plist dictionary. */ 52 | public static IosAppInfo readFromPlistDictionary(NSDictionary dict) { 53 | return builder() 54 | .bundleId(new IosAppBundleId(dict.get("CFBundleIdentifier").toString())) 55 | .build(); 56 | } 57 | 58 | /** Returns a new builder. */ 59 | public static Builder builder() { 60 | return new AutoValue_IosAppInfo.Builder(); 61 | } 62 | 63 | /** A builder for {@code IosAppInfo}. */ 64 | @AutoValue.Builder 65 | public abstract static class Builder { 66 | public abstract Builder bundleId(IosAppBundleId bundleId); 67 | 68 | public abstract IosAppInfo build(); 69 | } 70 | 71 | /** The bundle identifier of the application. */ 72 | public abstract IosAppBundleId bundleId(); 73 | } 74 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/webinspector/ForwardSocketSetupMessage.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.webinspector; 16 | 17 | import com.google.auto.value.AutoValue; 18 | 19 | /** 20 | * A forwardSocketSetup message. 21 | * 22 | *

Example: 23 | * 24 | *

{@code
25 |  * 
26 |  * 
28 |  * 
29 |  * 
30 |  *   __selector
31 |  *   _rpc_forwardSocketSetup:
32 |  *   __argument
33 |  *   
34 |  *     WIRApplicationIdentifierKey
35 |  *     PID:176
36 |  *     WIRConnectionIdentifierKey
37 |  *     17858421-36EF-4752-89F7-7A13ED5782C5
38 |  *     WIRPageIdentifierKey
39 |  *     1
40 |  *     WIRSenderKey
41 |  *     945f1146-2aa3-4875-a4c2-21cace3c4ade
42 |  *   
43 |  * 
44 |  * 
45 |  * }
46 | */ 47 | @AutoValue 48 | public abstract class ForwardSocketSetupMessage extends InspectorMessage { 49 | /** Returns a new builder. */ 50 | public static Builder builder() { 51 | return new AutoValue_ForwardSocketSetupMessage.Builder(); 52 | } 53 | 54 | /** A builder for creating forwardSocketSetup messages. */ 55 | @AutoValue.Builder 56 | public abstract static class Builder extends InspectorMessage.Builder { 57 | @Override 58 | public abstract Builder applicationId(String applicationId); 59 | 60 | @Override 61 | public abstract Builder automaticallyPause(boolean automaticallyPause); 62 | 63 | @Override 64 | public abstract Builder connectionId(String connectionId); 65 | 66 | @Override 67 | public abstract Builder pageId(int pageId); 68 | 69 | @Override 70 | public abstract Builder sender(String sender); 71 | 72 | @Override 73 | public abstract ForwardSocketSetupMessage build(); 74 | } 75 | 76 | @Override 77 | public abstract String applicationId(); 78 | 79 | @Override 80 | public abstract boolean automaticallyPause(); 81 | 82 | @Override 83 | public abstract String connectionId(); 84 | 85 | @Override 86 | public abstract int pageId(); 87 | 88 | @Override 89 | public abstract String sender(); 90 | 91 | @Override 92 | public MessageSelector selector() { 93 | return MessageSelector.FORWARD_SOCKET_SETUP; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/util/PlistParser.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.util; 16 | 17 | import static java.nio.charset.StandardCharsets.UTF_8; 18 | 19 | import com.dd.plist.BinaryPropertyListParser; 20 | import com.dd.plist.NSObject; 21 | import com.dd.plist.PropertyListFormatException; 22 | import com.dd.plist.PropertyListParser; 23 | import com.dd.plist.XMLPropertyListParser; 24 | import java.io.IOException; 25 | import java.io.UnsupportedEncodingException; 26 | import java.nio.file.Files; 27 | import java.nio.file.Path; 28 | import java.text.ParseException; 29 | import javax.xml.parsers.ParserConfigurationException; 30 | import org.xml.sax.SAXException; 31 | 32 | /** Utilities for parsing plists. */ 33 | public class PlistParser { 34 | /** 35 | * Parses the content of a plist file as UTF-8 encoded XML. 36 | * 37 | * @throws PlistParseException - if there is an error parsing the content. 38 | */ 39 | public static NSObject fromPath(Path plist) { 40 | try { 41 | return PropertyListParser.parse(Files.readAllBytes(plist)); 42 | } catch (ParserConfigurationException 43 | | ParseException 44 | | PropertyListFormatException 45 | | IOException 46 | | SAXException e) { 47 | throw new PlistParseException(e); 48 | } 49 | } 50 | 51 | /** 52 | * Parses an XML string to a plist object. 53 | * 54 | * @throws PlistParseException - if there is an error parsing the string. 55 | */ 56 | public static NSObject fromXml(String xml) { 57 | try { 58 | return XMLPropertyListParser.parse(xml.getBytes(UTF_8)); 59 | } catch (ParserConfigurationException 60 | | ParseException 61 | | PropertyListFormatException 62 | | IOException 63 | | SAXException e) { 64 | throw new PlistParseException(e); 65 | } 66 | } 67 | 68 | /** 69 | * Parses a byte array to a plist object. 70 | * 71 | * @throws PlistParseException - if there is an error parsing the bytes. 72 | */ 73 | public static NSObject fromBinary(byte[] bytes) { 74 | try { 75 | return BinaryPropertyListParser.parse(bytes); 76 | } catch (UnsupportedEncodingException | PropertyListFormatException e) { 77 | throw new PlistParseException(e); 78 | } 79 | } 80 | 81 | /** Thrown if a parsing error occurs. */ 82 | public static class PlistParseException extends RuntimeException { 83 | private PlistParseException(Exception cause) { 84 | super(cause); 85 | } 86 | } 87 | 88 | private PlistParser() {} 89 | } 90 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/webinspector/ApplicationSentListingMessage.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.webinspector; 16 | 17 | import com.google.auto.value.AutoValue; 18 | import com.google.common.collect.ImmutableList; 19 | import java.util.List; 20 | 21 | /** 22 | * An applicationSentListing message. 23 | * 24 | *

Example: 25 | * 26 | *

{@code
27 |  * 
28 |  * 
30 |  * 
31 |  * 
32 |  *   __selector
33 |  *   _rpc_applicationSentListing:
34 |  *   __argument
35 |  *   
36 |  *     WIRApplicationIdentifierKey
37 |  *     com.apple.mobilesafari
38 |  *     WIRListingKey
39 |  *     
40 |  *       1
41 |  *       
42 |  *         WIRPageIdentifierKey
43 |  *         1
44 |  *         WIRTitleKey
45 |  *         Google
46 |  *         WIRURLKey
47 |  *         http://www.google.com
48 |  *       
49 |  *       2
50 |  *       
51 |  *         WIRPageIdentifierKey
52 |  *         2
53 |  *         WIRTitleKey
54 |  *         Yahoo
55 |  *         WIRURLKey
56 |  *         http://www.yahoo.com
57 |  *       
58 |  *     
59 |  *   
60 |  * 
61 |  * 
62 |  * }
63 | */ 64 | @AutoValue 65 | public abstract class ApplicationSentListingMessage extends InspectorMessage { 66 | /** Returns a new builder. */ 67 | public static Builder builder() { 68 | return new AutoValue_ApplicationSentListingMessage.Builder(); 69 | } 70 | 71 | /** A builder for creating applicationSentListing messages. */ 72 | @AutoValue.Builder 73 | public abstract static class Builder extends InspectorMessage.Builder { 74 | @Override 75 | public abstract Builder applicationId(String applicationId); 76 | 77 | @Override 78 | public abstract Builder listing(List listing); 79 | 80 | @Override 81 | public abstract ApplicationSentListingMessage build(); 82 | } 83 | 84 | @Override 85 | public abstract String applicationId(); 86 | 87 | @Override 88 | public abstract ImmutableList listing(); 89 | 90 | @Override 91 | public MessageSelector selector() { 92 | return MessageSelector.APPLICATION_SENT_LISTING; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/webinspector/ForwardSocketDataMessage.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.webinspector; 16 | 17 | import com.google.auto.value.AutoValue; 18 | import javax.json.JsonObject; 19 | 20 | /** 21 | * A forwardSocketData message. 22 | * 23 | *

Example: 24 | * 25 | *

{@code
26 |  * 
27 |  * 
29 |  * 
30 |  * 
31 |  *   __selector
32 |  *   _rpc_forwardSocketData:
33 |  *   __argumentcommand
34 |  *   
35 |  *     WIRApplicationIdentifierKey
36 |  *     PID:176
37 |  *     WIRConnectionIdentifierKey
38 |  *     17858421-36EF-4752-89F7-7A13ED5782C5
39 |  *     WIRPageIdentifierKey
40 |  *     1
41 |  *     WIRSenderKey
42 |  *     945f1146-2aa3-4875-a4c2-21cace3c4ade
43 |  *     WIRSocketDataKey
44 |  *     }{"id": 1, "method": "Inspector.enable"}{@code
45 |  *   
46 |  * 
47 |  * 
48 |  * }
49 | */ 50 | @AutoValue 51 | public abstract class ForwardSocketDataMessage extends InspectorMessage { 52 | /** Returns a new builder. */ 53 | public static Builder builder() { 54 | return new AutoValue_ForwardSocketDataMessage.Builder(); 55 | } 56 | 57 | /** A builder for creating forwardSocketData messages. */ 58 | @AutoValue.Builder 59 | public abstract static class Builder extends InspectorMessage.Builder { 60 | @Override 61 | public abstract Builder applicationId(String applicationId); 62 | 63 | @Override 64 | public abstract Builder connectionId(String connectionId); 65 | 66 | @Override 67 | public abstract Builder pageId(int pageId); 68 | 69 | @Override 70 | public abstract Builder sender(String sender); 71 | 72 | @Override 73 | public abstract Builder socketData(JsonObject socketData); 74 | 75 | @Override 76 | public abstract ForwardSocketDataMessage build(); 77 | } 78 | 79 | @Override 80 | public abstract String applicationId(); 81 | 82 | @Override 83 | public abstract String connectionId(); 84 | 85 | @Override 86 | public abstract int pageId(); 87 | 88 | @Override 89 | public abstract String sender(); 90 | 91 | @Override 92 | public abstract JsonObject socketData(); 93 | 94 | @Override 95 | public MessageSelector selector() { 96 | return MessageSelector.FORWARD_SOCKET_DATA; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/testing/FakeInspectorSocket.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.testing; 16 | 17 | import static com.google.common.base.Preconditions.checkState; 18 | 19 | import com.dd.plist.NSDictionary; 20 | import com.google.common.collect.ImmutableList; 21 | import com.google.iosdevicecontrol.webinspector.InspectorMessage; 22 | import com.google.iosdevicecontrol.webinspector.InspectorSocket; 23 | import java.io.IOException; 24 | import java.util.Iterator; 25 | import java.util.Optional; 26 | import java.util.Queue; 27 | import java.util.concurrent.BlockingQueue; 28 | import java.util.concurrent.ConcurrentLinkedQueue; 29 | import java.util.concurrent.LinkedBlockingQueue; 30 | 31 | /** 32 | * Fake implementation of {@link InspectorSocket} that maintains a queue of all messages sent to the 33 | * inspector and a queue of all messages to be received by the inspector. 34 | */ 35 | public final class FakeInspectorSocket implements InspectorSocket { 36 | private volatile boolean closed; 37 | private final Queue messagesSent = new ConcurrentLinkedQueue<>(); 38 | private final BlockingQueue messagesToReceive = new LinkedBlockingQueue<>(); 39 | 40 | @Override 41 | public void sendMessage(NSDictionary message) throws IOException { 42 | if (closed) { 43 | throw new IOException("socket closed"); 44 | } 45 | messagesSent.offer(InspectorMessage.fromPlist(message)); 46 | } 47 | 48 | @Override 49 | public Optional receiveMessage() throws IOException { 50 | if (closed) { 51 | return Optional.empty(); 52 | } 53 | try { 54 | return Optional.of(messagesToReceive.take().toPlist()); 55 | } catch (InterruptedException e) { 56 | Thread.currentThread().interrupt(); 57 | throw new IOException(e); 58 | } 59 | } 60 | 61 | /** Dequeues and returns all the messages sent to the inspector. */ 62 | public ImmutableList dequeueMessagesSent() { 63 | ImmutableList.Builder messages = ImmutableList.builder(); 64 | for (Iterator i = messagesSent.iterator(); i.hasNext(); ) { 65 | messages.add(i.next()); 66 | i.remove(); 67 | } 68 | return messages.build(); 69 | } 70 | 71 | /** Enqueues a message to be returned by {@link #receiveMessage}. */ 72 | public void enqueueMessageToReceive(InspectorMessage message) { 73 | checkState(!closed); 74 | checkState(messagesToReceive.add(message)); 75 | } 76 | 77 | @Override 78 | public void close() throws IOException { 79 | closed = true; 80 | } 81 | 82 | public boolean isClosed() { 83 | return closed; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/util/TunnelException.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.util; 16 | 17 | import static com.google.common.base.Preconditions.checkArgument; 18 | import static com.google.common.base.Preconditions.checkNotNull; 19 | 20 | import java.util.concurrent.Callable; 21 | 22 | /** 23 | * An unchecked exception used to temporarily wrap a checked exception, so that it can be 24 | * thrown from a method or constructor that doesn't declare that exception type, and then extracted 25 | * in a surrounding context and handled or propagated as usual. 26 | */ 27 | public final class TunnelException extends RuntimeException { 28 | /** 29 | * Evaluate the result of the specified lambda, wrapping any checked exception thrown in a {@code 30 | * TunnelException}. Unchecked exceptions are rethrown unchanged. 31 | */ 32 | public static T tunnel(Callable callback) { 33 | checkNotNull(callback); 34 | try { 35 | return callback.call(); 36 | } catch (RuntimeException e) { 37 | throw e; 38 | } catch (Exception e) { 39 | throw new TunnelException(e); 40 | } 41 | } 42 | 43 | private TunnelException(Exception e) { 44 | super("TunnelExceptions should always be unwrapped, so this message should never be seen.", e); 45 | } 46 | 47 | @Override 48 | public synchronized Exception getCause() { 49 | return (Exception) super.getCause(); 50 | } 51 | 52 | private static ClassCastException exception( 53 | Throwable cause, String message, Object... formatArgs) { 54 | ClassCastException result = new ClassCastException(String.format(message, formatArgs)); 55 | result.initCause(cause); 56 | return result; 57 | } 58 | 59 | @SafeVarargs 60 | private static void checkNoRuntimeExceptions( 61 | String methodName, Class... clazzes) { 62 | for (Class clazz : clazzes) { 63 | checkArgument( 64 | !RuntimeException.class.isAssignableFrom(clazz), 65 | "The cause of a TunnelException can never be a RuntimeException, " 66 | + "but %s argument was %s", 67 | methodName, 68 | clazz); 69 | } 70 | } 71 | 72 | /** Returns the underlying checked exception of the specified type. */ 73 | public X getCauseAs(Class exceptionClazz) { 74 | checkNotNull(exceptionClazz); 75 | checkNoRuntimeExceptions("getCause", exceptionClazz); 76 | if (exceptionClazz.isInstance(getCause())) { 77 | return exceptionClazz.cast(getCause()); 78 | } 79 | throw exception(getCause(), "getCause(%s) doesn't match underlying exception", exceptionClazz); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/util/FluentLogger.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.util; 16 | 17 | import java.util.Optional; 18 | import java.util.logging.ConsoleHandler; 19 | import java.util.logging.Level; 20 | import java.util.logging.Logger; 21 | 22 | /** 23 | * Class for a fluent logger. Used as a factory of instances to build log statements via method 24 | * chaining. 25 | */ 26 | public final class FluentLogger { 27 | /** Returns a generic fluent logger for a class. */ 28 | public static FluentLogger forEnclosingClass() { 29 | StackTraceElement caller = new Throwable().getStackTrace()[1]; 30 | Logger logger = Logger.getLogger(caller.getClassName()); 31 | logger.setUseParentHandlers(false); 32 | logger.addHandler(new ConsoleHandler()); 33 | return new FluentLogger(logger, Level.OFF, Optional.empty()); 34 | } 35 | 36 | private final Logger logger; 37 | private final Level level; 38 | private final Optional cause; 39 | 40 | private FluentLogger(Logger logger, Level level, Optional cause) { 41 | this.level = level; 42 | this.logger = logger; 43 | this.cause = cause; 44 | } 45 | 46 | /** Returns a fluent logger with the specified cause. */ 47 | public FluentLogger withCause(Throwable cause) { 48 | return new FluentLogger(logger, level, Optional.of(cause)); 49 | } 50 | 51 | /** Convenience method for at({@link Level#INFO}). */ 52 | public FluentLogger atInfo() { 53 | return at(Level.INFO); 54 | } 55 | 56 | /** Convenience method for at({@link Level#SEVERE}). */ 57 | public FluentLogger atSevere() { 58 | return at(Level.SEVERE); 59 | } 60 | 61 | /** Convenience method for at({@link Level#WARNING}). */ 62 | public FluentLogger atWarning() { 63 | return at(Level.WARNING); 64 | } 65 | 66 | /** Convenience method for at({@link Level#FINE}). */ 67 | public FluentLogger atFine() { 68 | return at(Level.FINE); 69 | } 70 | 71 | /** Returns a fluent logger with the specified logging level. */ 72 | public FluentLogger at(Level level) { 73 | return new FluentLogger(logger, level, cause); 74 | } 75 | 76 | /** Print a log of the {@link FluentLogger#cause}. */ 77 | public void log() { 78 | log(""); 79 | } 80 | 81 | /** Print a formatted log message. */ 82 | public void log(String message, Object... params) { 83 | StackTraceElement caller = new Throwable().getStackTrace()[1]; 84 | String formatMsg = String.format(message, params); 85 | if (cause.isPresent()) { 86 | logger.logp(level, logger.getName(), caller.getMethodName(), formatMsg, cause.get()); 87 | } else { 88 | logger.logp(level, logger.getName(), caller.getMethodName(), formatMsg); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/IosDeviceSocket.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol; 16 | 17 | import com.google.common.io.ByteStreams; 18 | import java.io.Closeable; 19 | import java.io.IOException; 20 | import java.io.InputStream; 21 | import java.io.OutputStream; 22 | import java.net.Socket; 23 | 24 | /** A socket to communicate to an iOS device. */ 25 | public abstract class IosDeviceSocket extends IosDeviceResource { 26 | /** Wraps an iOS device and a Java socket as an IosDeviceSocket. */ 27 | public static IosDeviceSocket wrap(IosDevice device, Socket socket) throws IosDeviceException { 28 | return new SocketWrapper(device, socket); 29 | } 30 | 31 | protected IosDeviceSocket(IosDevice device) { 32 | super(device); 33 | } 34 | 35 | /** 36 | * Read bytes from the socket until the array is full or EOF is reached, returning the number of 37 | * bytes read. The returned value is less than the length of the array only when EOF is reached. 38 | */ 39 | public abstract int read(byte[] bytes) throws IosDeviceException; 40 | 41 | /** Writes bytes to the socket. */ 42 | public abstract void write(byte[] bytes) throws IosDeviceException; 43 | 44 | /** Wraps a device and a Java socket as an IosDeviceSocket. */ 45 | private static class SocketWrapper extends IosDeviceSocket { 46 | private final InputStream socketIn; 47 | private final OutputStream socketOut; 48 | private final Closeable socketClose; 49 | 50 | private SocketWrapper(IosDevice device, Socket socket) throws IosDeviceException { 51 | super(device); 52 | try { 53 | socketIn = socket.getInputStream(); 54 | socketOut = socket.getOutputStream(); 55 | } catch (IOException | RuntimeException e) { 56 | try { 57 | socket.close(); 58 | } catch (IOException ce) { 59 | e.addSuppressed(ce); 60 | } 61 | throw new IosDeviceException(device, e); 62 | } 63 | socketClose = socket::close; 64 | } 65 | 66 | @Override 67 | public int read(byte[] bytes) throws IosDeviceException { 68 | try { 69 | return ByteStreams.read(socketIn, bytes, 0, bytes.length); 70 | } catch (IOException e) { 71 | throw new IosDeviceException(device(), e); 72 | } 73 | } 74 | 75 | @Override 76 | public void write(byte[] bytes) throws IosDeviceException { 77 | try { 78 | socketOut.write(bytes); 79 | } catch (IOException e) { 80 | throw new IosDeviceException(device(), e); 81 | } 82 | } 83 | 84 | @Override 85 | public void close() throws IosDeviceException { 86 | try { 87 | socketClose.close(); 88 | } catch (IOException e) { 89 | throw new IosDeviceException(device(), e); 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iOS Device Control Library 2 | 3 | iOS Device Control is a Java library for controlling the iOS Simulator and real 4 | (physical) iOS devices tethered to a device running macOS. The library offers 5 | the ability to get device properties, install and start applications, take 6 | screenshots, capture logs, and more! 7 | 8 | Examples of how to use this library can be found 9 | [here](src/com/google/iosdevicecontrol/examples). To build the examples, run: 10 | 11 | ```console 12 | mvn assembly:assembly 13 | ``` 14 | which will create two runnable jars in the target directory. 15 | 16 | ## Installation 17 | 18 | iOS Device Control only works on macOS with tethered real devices or with Xcode 19 | 8+ with the simctl tool installed for the iOS Simulator. 20 | 21 | ### Simulator Automation 22 | 23 | Install [Xcode 8 or above](https://developer.apple.com/xcode/) 24 | and verify the following command works: 25 | 26 | ```console 27 | xcrun simctl --version 28 | ``` 29 | 30 | ### Real Device Automation 31 | 32 | The following dependencies can be installed easily with 33 | [homebrew](http://brew.sh/): 34 | 35 | ```console 36 | brew install autoconf automake libtool libxml2 libzip pkg-config openssl 37 | ``` 38 | 39 | Install libplist by building from source: 40 | ```console 41 | git clone https://github.com/libimobiledevice/libplist.git 42 | cd libplist 43 | ./autogen.sh 44 | make 45 | sudo make install 46 | ``` 47 | 48 | Install libusbmuxd by building from source: 49 | ```console 50 | git clone https://github.com/libimobiledevice/libusbmuxd.git 51 | cd libusbmuxd 52 | ./autogen.sh 53 | make 54 | sudo make install 55 | ``` 56 | 57 | Install libimobiledevice by building from source: 58 | ```console 59 | git clone https://github.com/libimobiledevice/libimobiledevice.git 60 | cd libimobiledevice 61 | ./autogen.sh 62 | make 63 | sudo make install 64 | ``` 65 | 66 | Install ideviceinstaller by building from source: 67 | ```console 68 | git clone https://github.com/libimobiledevice/ideviceinstaller.git 69 | cd ideviceinstaller 70 | ./autogen.sh 71 | make 72 | sudo make install 73 | ``` 74 | 75 | Install idevice_app_runner and idevicewebinspectorproxy by building from 76 | source. This can be done by following the instructions outlined in the 77 | READMEs of the respective projects in the [third_party directory](third_party). 78 | 79 | #### Optional Utilities 80 | 81 | Most of the iOS Device Control library can be used with just the above tools. 82 | For additional control of real devices, the following tools can optionally be 83 | installed: 84 | 85 | Install [Apple Configurator 2](https://support.apple.com/apple-configurator) 86 | and install the automation tools by selecting the "Install Automation Tools..." 87 | option under the Apple Configurator 2 menu. 88 | 89 | Install the provided OpenUrl app by following the instructions 90 | [here](OpenUrlApp/README) to automate Safari on real devices. 91 | 92 | ## Troubleshooting 93 | 94 | For real devices, make sure that both the device is trusted and the lockdown 95 | folder has the correct permissions. 96 | ```console 97 | sudo chmod -R 777 /var/db/lockdown 98 | ``` 99 | 100 | ## License 101 | 102 | iOS Device Control is licensed under the open-source 103 | [Apache 2.0 license](LICENSE). 104 | 105 | ## Contributing 106 | 107 | Please [see the guidelines for contributing](CONTRIBUTING.md) before creating 108 | pull requests. 109 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/webinspector/ReportSetupMessage.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.webinspector; 16 | 17 | import com.google.auto.value.AutoValue; 18 | import com.google.common.base.Optional; 19 | 20 | /** 21 | * A reportSetup message. 22 | * 23 | *

Example: 24 | * 25 | *

{@code
 26 |  * 
 27 |  * 
 29 |  * 
 30 |  * 
 31 |  *   __selector
 32 |  *   _rpc_reportSetup:
 33 |  *   __argument
 34 |  *   
 35 |  *     WIRSimulatorBuildKey
 36 |  *     11D167
 37 |  *     WIRSimulatorNameKey
 38 |  *     iPhone Simulator
 39 |  *     WIRSimulatorProductVersionKey
 40 |  *     7.1
 41 |  *   
 42 |  * 
 43 |  * 
 44 |  * }
45 | */ 46 | @AutoValue 47 | public abstract class ReportSetupMessage extends InspectorMessage { 48 | /** Returns a new builder. */ 49 | public static Builder builder() { 50 | return new AutoValue_ReportSetupMessage.Builder(); 51 | } 52 | 53 | /** A builder for creating reportSetup messages. */ 54 | @AutoValue.Builder 55 | public abstract static class Builder extends InspectorMessage.Builder { 56 | @Override 57 | public final Builder simulatorBuild(String simulatorBuild) { 58 | return optionalSimulatorBuild(simulatorBuild); 59 | } 60 | 61 | @Override 62 | public final Builder simulatorName(String simulatorName) { 63 | return optionalSimulatorName(simulatorName); 64 | } 65 | 66 | @Override 67 | public final Builder simulatorProductVersion(String simulatorProductVersion) { 68 | return optionalSimulatorProductVersion(simulatorProductVersion); 69 | } 70 | 71 | @Override 72 | public abstract ReportSetupMessage build(); 73 | 74 | abstract Builder optionalSimulatorBuild(String simulatorBuild); 75 | 76 | abstract Builder optionalSimulatorName(String simulatorName); 77 | 78 | abstract Builder optionalSimulatorProductVersion(String simulatorProductVersion); 79 | } 80 | 81 | public abstract Optional optionalSimulatorBuild(); 82 | 83 | public abstract Optional optionalSimulatorName(); 84 | 85 | public abstract Optional optionalSimulatorProductVersion(); 86 | 87 | @Override 88 | final String simulatorBuild() { 89 | return fromOptional(optionalSimulatorBuild()); 90 | } 91 | 92 | @Override 93 | final String simulatorName() { 94 | return fromOptional(optionalSimulatorName()); 95 | } 96 | 97 | @Override 98 | final String simulatorProductVersion() { 99 | return fromOptional(optionalSimulatorProductVersion()); 100 | } 101 | 102 | @Override 103 | public MessageSelector selector() { 104 | return MessageSelector.REPORT_SETUP; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/command/testing/FakeExecutor.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.command.testing; 16 | 17 | import com.google.common.collect.ImmutableList; 18 | import com.google.iosdevicecontrol.command.Command; 19 | import com.google.iosdevicecontrol.command.CommandExecutor; 20 | import com.google.iosdevicecontrol.command.CommandStartException; 21 | import java.util.Iterator; 22 | import java.util.NoSuchElementException; 23 | import java.util.Queue; 24 | import java.util.concurrent.ConcurrentLinkedQueue; 25 | 26 | /** 27 | * An executor that creates a new running {@link FakeProcess} on each call to {@link #start}, and 28 | * saves the processes it started in a FIFO queue for later inspection by a test. This class is 29 | * thread-safe, so it may be used as a fake in concurrent applications. 30 | */ 31 | public class FakeExecutor implements CommandExecutor { 32 | /** Returns a FakeExector whoses processes immediately exit with the specified exit code. */ 33 | public static FakeExecutor immediatelyExits(final int exitCode) { 34 | return new FakeExecutor() { 35 | @Override 36 | public FakeProcess start(Command command) throws CommandStartException { 37 | FakeProcess process = super.start(command); 38 | process.setTerminated(exitCode); 39 | return process; 40 | } 41 | }; 42 | } 43 | 44 | /** 45 | * Returns a FakeExector whoses processes immediately exit with the specified exit code, stdout, 46 | * and stderr. 47 | */ 48 | public static FakeExecutor immediatelyExits( 49 | final int exitCode, final String stdout, final String stderr) { 50 | return new FakeExecutor() { 51 | @Override 52 | public FakeProcess start(Command command) throws CommandStartException { 53 | FakeProcess process = super.start(command); 54 | process.writeStdoutUtf8(stdout); 55 | process.writeStderrUtf8(stderr); 56 | process.setTerminated(exitCode); 57 | return process; 58 | } 59 | }; 60 | } 61 | 62 | private final Queue started = new ConcurrentLinkedQueue<>(); 63 | 64 | @Override 65 | public FakeProcess start(Command command) throws CommandStartException { 66 | FakeProcess process = FakeProcess.start(command); 67 | started.add(process); 68 | return process; 69 | } 70 | 71 | /** 72 | * Retrieves and removes the earliest started process in the queue (the head of the queue). 73 | * 74 | * @throws NoSuchElementException - if no process if left in the queue. 75 | */ 76 | public FakeProcess dequeueProcess() { 77 | return started.remove(); 78 | } 79 | 80 | /** Retrieves and removes all processes in the queue, in the order they were started. */ 81 | public ImmutableList dequeueAllProcesses() { 82 | ImmutableList.Builder processes = ImmutableList.builder(); 83 | for (Iterator i = started.iterator(); i.hasNext(); ) { 84 | processes.add(i.next()); 85 | i.remove(); 86 | } 87 | return processes.build(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/command/Examples.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.command; 16 | 17 | import static com.google.iosdevicecontrol.command.Command.command; 18 | 19 | import java.io.BufferedReader; 20 | import java.nio.file.Path; 21 | 22 | /** 23 | * This class contains several examples used by README.md, to ensure that they compile and stay 24 | * up-to-date. It should not be referenced by any other code. This class is part of :command so that 25 | * build issues with the examples will be caught immediately. 26 | * 27 | * @deprecated No one should be using this class. 28 | */ 29 | @Deprecated 30 | class Examples { 31 | void helloWorld() throws Exception { 32 | Command.command("echo", "Hello", "World").execute(); 33 | } 34 | 35 | void stdoutStderr(Path executable) throws Exception { 36 | CommandResult result = command(executable).execute(); 37 | System.out.println(result.stdoutStringUtf8()); 38 | System.out.println(result.stderrStringUtf8()); 39 | } 40 | 41 | void template() throws Exception { 42 | Command template = command("echo", "Foo"); 43 | template.withArgumentsAppended("Bar").execute(); // echos "Foo Bar" 44 | template.withArgumentsAppended("Baz").execute(); // echos "Foo Baz" 45 | template.withArguments("Fizz", "Buzz").execute(); // echos "Fizz Buzz" 46 | } 47 | 48 | void running(Path executable) throws Exception { 49 | CommandProcess process = command(executable).start(); 50 | while (process.isAlive()) { 51 | System.out.println("Still running..."); 52 | Thread.sleep(100); 53 | } 54 | CommandResult result = process.await(); // process is done now 55 | System.out.println(result.exitCode()); 56 | } 57 | 58 | void failure(Path executableThatMightFail) throws Exception { 59 | try { 60 | CommandResult result = command(executableThatMightFail).execute(); 61 | System.out.println(result.stdoutStringUtf8()); 62 | } catch (CommandFailureException e) { 63 | System.out.println(e.result().stderrStringUtf8()); 64 | } 65 | } 66 | 67 | void customSuccess(Path executableThatShouldFail) throws Exception { 68 | CommandResult result = command(executableThatShouldFail) 69 | .withSuccessExitCodes(1, 2, 10) // Note that 0 is not considered a "successful" exit now 70 | .execute(); 71 | System.out.println(result.exitCode()); 72 | } 73 | 74 | void readWhileRunning(Path longRunningCommand) throws Exception { 75 | CommandProcess process = command(longRunningCommand).start(); 76 | try (BufferedReader stdout = new BufferedReader(process.stdoutReaderUtf8())) { 77 | String line; 78 | while ((line = stdout.readLine()) != null) { 79 | System.out.println(line); 80 | } 81 | } 82 | // this should return immediately since the process has closed stdout 83 | CommandResult result = process.await(); 84 | // The full output is still available in the result 85 | System.out.println("stdout: " + result.stdoutStringUtf8()); 86 | System.out.println("stderr: " + result.stderrStringUtf8()); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/webinspector/ReportConnectedApplicationListMessage.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.webinspector; 16 | 17 | import com.google.auto.value.AutoValue; 18 | import com.google.common.collect.ImmutableList; 19 | import java.util.List; 20 | 21 | /** 22 | * A reportConnectedApplicationList message. 23 | * 24 | *

Example: 25 | * 26 | *

{@code
27 |  * 
28 |  * 
30 |  * 
31 |  * 
32 |  *   __selector
33 |  *   _rpc_reportConnectedApplicationList:
34 |  *   __argument
35 |  *   
36 |  *     WIRApplicationDictionaryKey
37 |  *     
38 |  *       PID:176
39 |  *       
40 |  *         WIRApplicationBundleIdentifierKey
41 |  *         com.apple.mobilesafari
42 |  *         WIRApplicationIdentifierKey
43 |  *         PID:176
44 |  *         WIRApplicationNameKey
45 |  *         Safari
46 |  *         WIRHostApplicationIdentifierKey
47 |  *         PID:457
48 |  *         WIRIsApplicationActiveKey
49 |  *         1
50 |  *         WIRIsApplicationProxyKey
51 |  *         
52 |  *       
53 |  *       PID:263
54 |  *       
55 |  *         WIRApplicationBundleIdentifierKey
56 |  *         com.apple.WebKit.WebContent
57 |  *         WIRApplicationIdentifierKey
58 |  *         PID:263
59 |  *         WIRApplicationNameKey
60 |  *         WebContent
61 |  *         WIRHostApplicationIdentifierKey
62 |  *         PID:739
63 |  *         WIRIsApplicationActiveKey
64 |  *         0
65 |  *         WIRIsApplicationProxyKey
66 |  *         
67 |  *       
68 |  *     
69 |  *   
70 |  * 
71 |  * 
72 |  * }
73 | */ 74 | @AutoValue 75 | public abstract class ReportConnectedApplicationListMessage extends InspectorMessage { 76 | /** Returns a new builder. */ 77 | public static Builder builder() { 78 | return new AutoValue_ReportConnectedApplicationListMessage.Builder(); 79 | } 80 | 81 | /** A builder for creating reportConnectedApplicationList messages. */ 82 | @AutoValue.Builder 83 | public abstract static class Builder extends InspectorMessage.Builder { 84 | @Override 85 | public abstract Builder applicationDictionary(List applicationDictionary); 86 | 87 | @Override 88 | public abstract ReportConnectedApplicationListMessage build(); 89 | } 90 | 91 | @Override 92 | public abstract ImmutableList applicationDictionary(); 93 | 94 | @Override 95 | public MessageSelector selector() { 96 | return MessageSelector.REPORT_CONNECTED_APPLICATION_LIST; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/webinspector/InspectorApplication.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.webinspector; 16 | 17 | import com.google.auto.value.AutoValue; 18 | import com.google.common.base.Optional; 19 | 20 | /** Information about an application in an inspector message. */ 21 | @AutoValue 22 | public abstract class InspectorApplication extends MessageDict { 23 | /** Returns a new builder. */ 24 | public static Builder builder() { 25 | return new AutoValue_InspectorApplication.Builder(); 26 | } 27 | 28 | /** A builder for creating inspector applications. */ 29 | @AutoValue.Builder 30 | public abstract static class Builder extends MessageDict.Builder { 31 | @Override 32 | public abstract Builder applicationBundleId(String applicationBundleId); 33 | 34 | @Override 35 | public abstract Builder applicationId(String applicationId); 36 | 37 | @Override 38 | public abstract Builder applicationName(String applicationName); 39 | 40 | @Override 41 | public final Builder hostApplicationId(String hostApplicationId) { 42 | return optionalHostApplicationId(hostApplicationId); 43 | } 44 | 45 | @Override 46 | public abstract Builder isApplicationActive(boolean isApplicationActive); 47 | 48 | @Override 49 | public abstract Builder isApplicationProxy(boolean isApplicationProxy); 50 | 51 | @Override 52 | public final Builder isApplicationReady(boolean isApplicationReady) { 53 | return optionalIsApplicationReady(isApplicationReady); 54 | } 55 | 56 | @Override 57 | public final Builder remoteAutomationEnabled(boolean remoteAutomationEnabled) { 58 | return optionalRemoteAutomationEnabled(remoteAutomationEnabled); 59 | } 60 | 61 | @Override 62 | public abstract InspectorApplication build(); 63 | 64 | abstract Builder optionalHostApplicationId(String hostApplicationId); 65 | 66 | abstract Builder optionalIsApplicationReady(boolean isApplicationReady); 67 | 68 | abstract Builder optionalRemoteAutomationEnabled(boolean remoteAutomationEnabled); 69 | } 70 | 71 | @Override 72 | public abstract String applicationBundleId(); 73 | 74 | @Override 75 | public abstract String applicationId(); 76 | 77 | @Override 78 | public abstract String applicationName(); 79 | 80 | public abstract Optional optionalHostApplicationId(); 81 | 82 | @Override 83 | public abstract boolean isApplicationActive(); 84 | 85 | @Override 86 | public abstract boolean isApplicationProxy(); 87 | 88 | public abstract Optional optionalIsApplicationReady(); 89 | 90 | public abstract Optional optionalRemoteAutomationEnabled(); 91 | 92 | @Override 93 | final String hostApplicationId() { 94 | return fromOptional(optionalHostApplicationId()); 95 | } 96 | 97 | @Override 98 | final boolean isApplicationReady() { 99 | return fromOptional(optionalIsApplicationReady()); 100 | } 101 | 102 | @Override 103 | final boolean remoteAutomationEnabled() { 104 | return fromOptional(optionalRemoteAutomationEnabled()); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /third_party/idevice_app_runner/README.txt: -------------------------------------------------------------------------------- 1 | *** Run app on iDevice using com.apple.debugserver *** 2 | 3 | This simple utility will start iOS app (using com.apple.debugserver) 4 | and tail its output. 5 | 6 | Interrupting idevice-app-runner process (ctrl-c) stops app on iDevice 7 | too. 8 | 9 | Requirements: 10 | 11 | * libimobiledevice - http://www.libimobiledevice.org/ 12 | * iDevice(s) (maybe in developer mode) 13 | 14 | Build: 15 | 16 | $ make 17 | 18 | Usage: 19 | 20 | $ idevice-app-runner -r /private/var/mobile/Applications/........-....-....-....-............/... 21 | 22 | I cooked up something mostly by tracing APIs and syscalls used in 23 | Xcode and fruitscrap. 24 | 25 | To get app path, for example: 26 | 27 | $ APPNAME=something APPPATH=`ideviceinstaller -l -o xml | egrep -A1 'Path|CFBundleName' | tr -d $'\n' | sed 's/--/\n/g' | egrep -A1 'CFBundleName.*'$APPNAME | tail -1 | tr '<>' ' ' | awk '{print $5}'` 28 | 29 | or 30 | 31 | $ ideviceinstaller -l -o xml | grep -A1 'Path' | grep '' | tr '<>' ' ' | awk '{ print $2 }' | grep $APPNAME 32 | 33 | Tested: 34 | 35 | Linux 3.0, x86_64, libimobiledevice-git, iOS 4.1 - works 36 | Linux 3.0, x86_64, libimobiledevice-git, iOS 5.0.1 - works 37 | Linux 3.0, i686, libimobiledevice-git, iOS 5.0.1 - works 38 | 39 | Notes: 40 | 41 | Sometimes debugger process or application being run is left 42 | hanging, it's not killed properly. When that happens it's impossible 43 | to run application again. Workaround for this issue is to restore 44 | device with --reboot option, 45 | 46 | $ idevicebackup2 restore --reboot bakdir... 47 | 48 | Wait for device to come back online by monitoring with ideviceinfo. It 49 | will take a few minutes. When reboot is completed, developer image 50 | should be mounted, 51 | 52 | $ idevicemountimage .../{DeveloperDiskImage.dmg,DeveloperDiskImage.dmg.signature} 53 | 54 | so idevice-app-runner can be used again. 55 | 56 | References: 57 | 58 | https://github.com/ghughes/fruitstrap - very helpful 59 | 60 | Contact: 61 | 62 | predrg@gmail.com 63 | 64 | DTRUSS COMMANDS: 65 | 66 | With tweak in fruitscrap.c: 67 | 68 | #define GDB_SHELL "sudo dtruss /Developer/Platforms/iPhoneOS.platform/Developer/usr/libexec/gdb/gdb-arm-apple-darwin --arch armv7 -q -x " PREP_CMDS_PATH " 2> dtruss2.log" 69 | 70 | For example: 71 | 72 | $ egrep 'write\(0x5,|read\(0x5,' dtruss2.log 73 | 74 | GDB COMMANDS: 75 | 76 | $ gdb .../Xcode nnnnn 77 | 78 | set print elements 10000 79 | 80 | b mobdevlog 81 | command 82 | silent 83 | printf "mobdevlog: %s\n", $rsi 84 | cont 85 | end 86 | 87 | b USBMuxDebugLog 88 | command 89 | silent 90 | printf "USBMuxDebugLog: %s %s\n", $rsi, $rdi 91 | cont 92 | end 93 | 94 | b send 95 | command 96 | printf "fd=%ld, size=%ld\n", $rdi, $rdx 97 | x/s $rsi 98 | bt 3 99 | cont 100 | end 101 | 102 | b SSL_write 103 | command 104 | p/d $rdx 105 | x/s $rsi 106 | cont 107 | end 108 | 109 | #b write 110 | b *0x0000000100fd3268 111 | command 112 | printf "fd=%ld, size=%ld\n", $rdi, $rdx 113 | x/s $rsi 114 | bt 3 115 | cont 116 | end 117 | 118 | #b recvfrom 119 | b *0x10100494c 120 | command 121 | printf "fd=%ld, size=%ld\n", $rdi, $rdx 122 | set variable $buf = $rsi 123 | cont 124 | end 125 | 126 | #b 127 | b *0x10100494c+17 128 | command 129 | x/s $buf 130 | cont 131 | end 132 | 133 | #b read 134 | b *0x100fcb45c 135 | command 136 | printf "fd=%ld, size=%ld\n", $rdi, $rdx 137 | set variable $buf2 = $rsi 138 | cont 139 | end 140 | 141 | #b read+17 142 | b *0x100fcb45c+17 143 | command 144 | x/s $buf2 145 | cont 146 | end 147 | 148 | 149 | #b BIO_write 150 | b *0x106aefaf8 151 | command 152 | printf "fd=%ld, size=%ld\n", $rdi, $rdx 153 | x/s $rsi 154 | x/100c $rsi 155 | bt 3 156 | cont 157 | end 158 | 159 | b DTDKStartSecureDebugServerService 160 | 161 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/testing/FakeIosAppProcess.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.testing; 16 | 17 | import static com.google.common.base.Preconditions.checkNotNull; 18 | import static java.util.concurrent.TimeUnit.NANOSECONDS; 19 | 20 | import com.google.common.util.concurrent.Futures; 21 | import com.google.common.util.concurrent.SettableFuture; 22 | import com.google.iosdevicecontrol.IosAppProcess; 23 | import com.google.iosdevicecontrol.IosDevice; 24 | import com.google.iosdevicecontrol.IosDeviceException; 25 | import com.google.iosdevicecontrol.util.CheckedCallable; 26 | import com.google.iosdevicecontrol.util.CheckedCallables; 27 | import java.io.IOException; 28 | import java.io.Reader; 29 | import java.io.StringReader; 30 | import java.time.Duration; 31 | import java.util.concurrent.ExecutionException; 32 | import java.util.concurrent.TimeoutException; 33 | 34 | /** Fake implementation of {@link IosAppProcess}. */ 35 | public class FakeIosAppProcess implements IosAppProcess { 36 | private final IosDevice device; 37 | private final SettableFuture future; 38 | 39 | public FakeIosAppProcess(IosDevice device) { 40 | this.device = checkNotNull(device); 41 | future = SettableFuture.create(); 42 | } 43 | 44 | @Override 45 | public FakeIosAppProcess kill() { 46 | future.cancel(false); 47 | return this; 48 | } 49 | 50 | @Override 51 | public String await() throws IosDeviceException { 52 | try { 53 | return future.get(); 54 | } catch (ExecutionException e) { 55 | throw mapExecutionException(e); 56 | } catch (InterruptedException e) { 57 | throw mapInterruptedException(e); 58 | } 59 | } 60 | 61 | @Override 62 | public String await(Duration timeout) throws IosDeviceException, TimeoutException { 63 | try { 64 | return future.get(timeout.toNanos(), NANOSECONDS); 65 | } catch (ExecutionException e) { 66 | throw mapExecutionException(e); 67 | } catch (InterruptedException e) { 68 | throw mapInterruptedException(e); 69 | } 70 | } 71 | 72 | @Override 73 | public Reader outputReader() { 74 | CheckedCallable delegate = 75 | CheckedCallables.memoize( 76 | () -> new StringReader(Futures.getChecked(future, IOException.class))); 77 | return new Reader() { 78 | @Override 79 | public int read(char[] cbuf, int off, int len) throws IOException { 80 | return delegate.call().read(cbuf, off, len); 81 | } 82 | 83 | @Override 84 | public void close() throws IOException { 85 | delegate.call().close(); 86 | } 87 | }; 88 | } 89 | 90 | public void setOutput(String s) { 91 | future.set(s); 92 | } 93 | 94 | public void setException(Throwable throwable) { 95 | future.setException(throwable); 96 | } 97 | 98 | private IosDeviceException mapExecutionException(Exception e) throws IosDeviceException { 99 | throw new IosDeviceException(device, e.getCause()); 100 | } 101 | 102 | private IosDeviceException mapInterruptedException(InterruptedException e) 103 | throws IosDeviceException { 104 | Thread.currentThread().interrupt(); 105 | throw new IosDeviceException(device, e); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /javatests/com/google/iosdevicecontrol/real/RealAppProcessTest.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.real; 16 | 17 | import static com.google.iosdevicecontrol.command.Command.command; 18 | import static com.google.common.truth.Truth.assertThat; 19 | import static com.google.common.truth.Truth.assert_; 20 | 21 | import com.google.iosdevicecontrol.command.Command; 22 | import com.google.iosdevicecontrol.command.testing.FakeProcess; 23 | import com.google.iosdevicecontrol.IosDeviceException; 24 | import com.google.iosdevicecontrol.IosDeviceException.Remedy; 25 | import com.google.iosdevicecontrol.testing.FakeIosDevice.FakeRealDevice; 26 | import org.junit.Before; 27 | import org.junit.Test; 28 | import org.junit.runner.RunWith; 29 | import org.junit.runners.JUnit4; 30 | 31 | /** Tests for {@link com.google.iosdevicecontrol.real.RealAppProcess}. */ 32 | @RunWith(JUnit4.class) 33 | 34 | public class RealAppProcessTest { 35 | private static final String APPRUNNER_STDERR_PREFIX = 36 | "sent[19] ($QStartNoAckMode#b0)\nsent[18] ($qLaunchSuccess#00)\nrecv[0] ()\n"; 37 | private static final RealDevice IOS_DEVICE = new FakeRealDevice(); 38 | private static final Command APPRUNNER_COMMAND = command("idevice-app-runner"); 39 | 40 | private FakeProcess commandProcess; 41 | private RealAppProcess appProcess; 42 | 43 | @Before 44 | public void setup() throws Exception { 45 | commandProcess = FakeProcess.start(APPRUNNER_COMMAND); 46 | appProcess = new RealAppProcess(IOS_DEVICE, commandProcess); 47 | commandProcess.writeStderrUtf8(APPRUNNER_STDERR_PREFIX); 48 | } 49 | 50 | @Test 51 | public void testMap4294967295Error() throws Exception { 52 | testErrorHasRemedy( 53 | Remedy.REINSTALL_APP, "Error: recv ($E4294967295#00) instead of expected ($OK#00)\n"); 54 | } 55 | 56 | @Test 57 | public void testMapFailedToGetTaskForProcessError() throws Exception { 58 | testErrorHasRemedy( 59 | Remedy.REINSTALL_APP, 60 | "Error: recv ($Efailed to get the task for process -1#00) instead of expected ($OK#00)\n"); 61 | } 62 | 63 | @Test 64 | public void testMapNotFoundError() throws Exception { 65 | testErrorHasRemedy( 66 | Remedy.REINSTALL_APP, "Error: recv ($ENotFound#00) instead of expected ($OK#00)\n"); 67 | } 68 | 69 | @Test 70 | public void testMapNoSuchFileOrDirectoryError() throws Exception { 71 | testErrorHasRemedy( 72 | Remedy.REINSTALL_APP, 73 | "Error: recv ($ENo such file or directory (/private/var/mobile/Containers/Bundle/" 74 | + "Application/46E85E58-864B-4EFA/OpenUrl.app)#00) instead of expected ($OK#00)\n"); 75 | } 76 | 77 | @Test 78 | public void testMapUnknownAppIdError() throws Exception { 79 | testErrorHasRemedy( 80 | Remedy.REINSTALL_APP, "Unknown APPID (com.google.openURL) is not in:"); 81 | } 82 | 83 | @Test 84 | public void testMapTimedOutTryingToLaunchApp() throws Exception { 85 | testErrorHasRemedy( 86 | Remedy.RESTART_DEVICE, 87 | "Error: recv ($Etimed out trying to launch app#00) instead of expected ($OK#00)\n"); 88 | } 89 | 90 | private void testErrorHasRemedy(Remedy remedy, String stderr) throws Exception { 91 | commandProcess.writeStderrUtf8(stderr); 92 | commandProcess.setTerminated(1); 93 | try { 94 | appProcess.await(); 95 | assert_().fail(); 96 | } catch (IosDeviceException e) { 97 | assertThat(e.remedy()).hasValue(remedy); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/IosModel.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol; 16 | 17 | import com.google.auto.value.AutoValue; 18 | import com.google.common.base.Splitter; 19 | import com.google.common.base.Suppliers; 20 | import com.google.iosdevicecontrol.util.StringEnumMap; 21 | import java.util.function.Supplier; 22 | 23 | /** Model information of an iOS device. */ 24 | @AutoValue 25 | public abstract class IosModel { 26 | private static final Splitter PRODUCT_CLASS_SPLITTER = Splitter.on(' ').limit(2); 27 | 28 | /** The class of device. */ 29 | public enum DeviceClass { 30 | IPAD, 31 | IPHONE, 32 | IPOD; 33 | 34 | private static final StringEnumMap STRING_TO_DEVICE_CLASS = 35 | new StringEnumMap<>(DeviceClass.class); 36 | 37 | /** 38 | * Returns the device class for the specified string, e.g. returns IPHONE for the 39 | * string "iPhone". 40 | */ 41 | public static DeviceClass forString(String s) { 42 | return STRING_TO_DEVICE_CLASS.get(s); 43 | } 44 | 45 | private final String string; 46 | 47 | private DeviceClass() { 48 | // e.g. "IPHONE" becomes "iPhone"; 49 | string = "iP" + name().substring(2).toLowerCase(); 50 | } 51 | 52 | @Override 53 | public String toString() { 54 | return string; 55 | } 56 | } 57 | 58 | /** CPU architecture of an iOS device. */ 59 | public enum Architecture { 60 | ARM64, 61 | ARMV7, 62 | ARMV7F, 63 | ARMV7K, 64 | ARMV7S, 65 | I386, 66 | X86_64; 67 | 68 | private static final StringEnumMap STRING_TO_ARCHITECTURE = 69 | new StringEnumMap<>(Architecture.class); 70 | 71 | /** 72 | * Returns the architecture for the specified string, e.g. returns ARMV7 for the 73 | * string "armv7". 74 | */ 75 | public static Architecture forString(String s) { 76 | return STRING_TO_ARCHITECTURE.get(s); 77 | } 78 | 79 | private final String string; 80 | 81 | private Architecture() { 82 | string = name().toLowerCase(); 83 | } 84 | 85 | @Override 86 | public String toString() { 87 | return string; 88 | } 89 | } 90 | 91 | public static Builder builder() { 92 | return new AutoValue_IosModel.Builder(); 93 | } 94 | 95 | private final Supplier deviceClass = 96 | Suppliers.memoize( 97 | () -> 98 | DeviceClass.forString(PRODUCT_CLASS_SPLITTER.split(productName()).iterator().next())); 99 | 100 | /** Model identifier, e.g. "iPhone5,1". */ 101 | public abstract String identifier(); 102 | 103 | /** Product string of this model, e.g. "iPhone 5". */ 104 | public abstract String productName(); 105 | 106 | /** Architecture of the device, e.g. "arm64". */ 107 | public abstract Architecture architecture(); 108 | 109 | /** Device class of this model, e.g. "iPad". */ 110 | public final DeviceClass deviceClass() { 111 | return deviceClass.get(); 112 | } 113 | 114 | /** IosModel builder. */ 115 | @AutoValue.Builder 116 | public abstract static class Builder { 117 | public abstract Builder identifier(String identifier); 118 | 119 | public abstract Builder productName(String productName); 120 | 121 | public abstract Builder architecture(Architecture architecture); 122 | 123 | /** Equivalent to architecture(Architecture.fromString(archString)). */ 124 | public final Builder architecture(String archString) { 125 | return architecture(Architecture.forString(archString)); 126 | } 127 | 128 | public abstract IosModel build(); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/util/EllipsisFormat.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.util; 16 | 17 | /** 18 | * Simple text formatting class which makes it easy to truncate 19 | * strings using an ellipsis ("..."). Supports a number of truncation 20 | * styles depending on what is desired. 21 | * 22 | *

Note that this class is not fully Unicode aware and counts characters 23 | * rather than code points. If Unicode content is formatted using this class 24 | * then the number of code-points in the output may currently be smaller than 25 | * the {@code length} parameter. However this class is safe to use for Unicode 26 | * content and will never split a string in the middle of a surrogate pair. 27 | */ 28 | public final class EllipsisFormat { 29 | private static final String ELLIPSIS = "..."; 30 | private static final int ELLIPSIS_LEN = ELLIPSIS.length(); 31 | private final int maxLength; 32 | private final Style style; 33 | 34 | public enum Style { LEFT, RIGHT, CENTER } 35 | 36 | /** 37 | * Constructs a new format instance where the string will be 38 | * truncated if it is greater than the specified length. The default 39 | * style will be used. 40 | */ 41 | public EllipsisFormat(int length) { 42 | this(length, Style.RIGHT); 43 | } 44 | 45 | /** 46 | * Constructs a new format instance where the string will be 47 | * truncated if it is greater than the specified length. The 48 | * specified style will be used. 49 | */ 50 | public EllipsisFormat(int length, Style style) { 51 | if (length < 0) { 52 | throw new IllegalArgumentException(length + " < 0"); 53 | } 54 | if (style == null) { 55 | throw new NullPointerException(); 56 | } 57 | this.maxLength = length; 58 | this.style = style; 59 | } 60 | 61 | /** 62 | * Returns the truncated string, or the original string if the 63 | * string is not too long. 64 | */ 65 | public String format(String s) { 66 | if (s.length() <= maxLength) { 67 | return s; 68 | } 69 | if (maxLength <= ELLIPSIS_LEN) { 70 | return ELLIPSIS; 71 | } 72 | int length = maxLength - ELLIPSIS_LEN; 73 | switch (style) { 74 | case RIGHT: 75 | return leftPart(s, length) + ELLIPSIS; 76 | case LEFT: 77 | return ELLIPSIS + rightPart(s, length); 78 | case CENTER: 79 | // Note: There's no risk of including the same code-point twice 80 | // here because length must be strictly less than s.length(). 81 | int leftLength = length / 2; 82 | int rightLength = length - leftLength; 83 | return leftPart(s, leftLength) + ELLIPSIS + rightPart(s, rightLength); 84 | default: 85 | throw new AssertionError(); 86 | } 87 | } 88 | 89 | /** 90 | * Returns a left-substring of the given string without risking splitting 91 | * across a surrogate pair. If a surrogate pair would be split at the given 92 | * length then the output is modified to include it. 93 | */ 94 | private static String leftPart(String s, int len) { 95 | if (len > 0 && Character.isHighSurrogate(s.charAt(len - 1))) { 96 | len++; 97 | } 98 | return s.substring(0, len); 99 | } 100 | 101 | /** 102 | * Returns a right-substring of the given string without risking splitting 103 | * across a surrogate pair. If a surrogate pair would be split at the given 104 | * length then the output is modified to include it. 105 | */ 106 | private static String rightPart(String s, int len) { 107 | int start = s.length() - len; 108 | if (start < s.length() && Character.isLowSurrogate(s.charAt(start))) { 109 | start--; 110 | } 111 | return s.substring(start); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/simulator/SimctlCommands.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.simulator; 16 | 17 | import static com.google.common.base.Preconditions.checkNotNull; 18 | import static com.google.iosdevicecontrol.command.Command.command; 19 | 20 | import com.google.common.collect.ImmutableList; 21 | import com.google.common.collect.ImmutableMap; 22 | import com.google.iosdevicecontrol.command.Command; 23 | import com.google.iosdevicecontrol.command.CommandProcess; 24 | import com.google.iosdevicecontrol.command.CommandStartException; 25 | import java.nio.file.Path; 26 | import java.nio.file.Paths; 27 | 28 | /** Abstracts the commands necessary for simulated devices that use xcrun simctl. */ 29 | final class SimctlCommands { 30 | private static final Command SIMCTL = command("xcrun", "simctl"); 31 | private static final Command SIMULATOR = 32 | command( 33 | Paths.get( 34 | "/Applications/Xcode.app/Contents/Developer" 35 | + "/Applications/Simulator.app/Contents/MacOS/Simulator")); 36 | 37 | static CommandProcess list() { 38 | return exec("list", "--json", "devices"); 39 | } 40 | 41 | static CommandProcess shutdownAll() { 42 | return exec("shutdown", "all"); 43 | } 44 | 45 | private final String udid; 46 | 47 | SimctlCommands(String udid) { 48 | this.udid = checkNotNull(udid); 49 | } 50 | 51 | CommandProcess getenv(String envVar) { 52 | return exec("getenv", udid, envVar); 53 | } 54 | 55 | CommandProcess install(String ipaPath) { 56 | return exec("install", udid, ipaPath); 57 | } 58 | 59 | CommandProcess uninstall(String bundleId) { 60 | return exec("uninstall", udid, bundleId); 61 | } 62 | 63 | CommandProcess launch(String bundleId, String... args) { 64 | return exec( 65 | simctl("launch", "--console", udid, bundleId) 66 | .withArgumentsAppended(ImmutableList.copyOf(args))); 67 | } 68 | 69 | CommandProcess screenshot(Path screenshotPath) { 70 | return exec("io", udid, "screenshot", "--type=png", screenshotPath.toString()); 71 | } 72 | 73 | CommandProcess enumerate() { 74 | return exec("io", udid, "enumerate"); 75 | } 76 | 77 | CommandProcess shutdown() { 78 | return exec("shutdown", udid); 79 | } 80 | 81 | CommandProcess syslog(Path syslogPath) { 82 | return exec( 83 | simctl("spawn", udid, "log", "stream", "--level=debug", "--system") 84 | .withStdoutTo(syslogPath)); 85 | } 86 | 87 | /** Boot the device and open it in the simulator for debugging. */ 88 | CommandProcess open() { 89 | return exec(SIMULATOR.withArguments("-CurrentDeviceUDID", udid)); 90 | } 91 | 92 | /** Boot the device. */ 93 | CommandProcess boot() { 94 | return exec("boot", udid); 95 | } 96 | 97 | CommandProcess erase() { 98 | return exec("erase", udid); 99 | } 100 | 101 | private static Command simctl(String... arguments) { 102 | return SIMCTL.withArgumentsAppended(ImmutableList.copyOf(arguments)); 103 | } 104 | 105 | private static CommandProcess exec(String... arguments) { 106 | return exec(simctl(arguments)); 107 | } 108 | 109 | private static CommandProcess exec(Command command) { 110 | try { 111 | return command.withEnvironment(ImmutableMap.of()).start(); 112 | } catch (CommandStartException e) { 113 | throw new IllegalStateException("Has Xcode 8 been installed?", e); 114 | } 115 | } 116 | 117 | static final class ShellCommands { 118 | static CommandProcess lsof(String... args) { 119 | return exec(command("lsof", args)); 120 | } 121 | 122 | static CommandProcess ps(String... args) { 123 | return exec(command("ps", args)); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /javatests/com/google/iosdevicecontrol/webinspector/BinaryPlistSocketTest.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.webinspector; 16 | 17 | import static com.google.common.truth.Truth.assertThat; 18 | 19 | import com.dd.plist.BinaryPropertyListWriter; 20 | import com.dd.plist.NSDictionary; 21 | import com.dd.plist.NSObject; 22 | import com.google.common.primitives.Ints; 23 | import com.google.iosdevicecontrol.util.ForwardingSocket; 24 | import com.google.iosdevicecontrol.util.PlistParser; 25 | import java.io.ByteArrayInputStream; 26 | import java.io.ByteArrayOutputStream; 27 | import java.io.IOException; 28 | import java.io.InputStream; 29 | import java.io.OutputStream; 30 | import java.net.Socket; 31 | import java.util.Arrays; 32 | import org.junit.Test; 33 | import org.junit.runner.RunWith; 34 | import org.junit.runners.JUnit4; 35 | 36 | @RunWith(JUnit4.class) 37 | public class BinaryPlistSocketTest { 38 | @Test 39 | public void testSendMessage() throws IOException { 40 | NSDictionary inputMessage = new NSDictionary(); 41 | inputMessage.put("hello", "world"); 42 | 43 | FakeSocket fakeSocket = new FakeSocket(new byte[] {}); 44 | try (InspectorSocket socket = new BinaryPlistSocket(fakeSocket)) { 45 | socket.sendMessage(inputMessage); 46 | } 47 | 48 | byte[] outputBytes = fakeSocket.outputBytes(); 49 | byte[] messageBytes = Arrays.copyOfRange(outputBytes, 4, outputBytes.length); 50 | NSObject outputMessage = PlistParser.fromBinary(messageBytes); 51 | assertThat(outputMessage).isEqualTo(inputMessage); 52 | } 53 | 54 | @Test 55 | public void testReceiveMessage() throws IOException { 56 | NSDictionary inputMessage = new NSDictionary(); 57 | inputMessage.put("goodbye", "world"); 58 | byte[] messageBytes = BinaryPropertyListWriter.writeToArray(inputMessage); 59 | byte[] lengthBytes = Ints.toByteArray(messageBytes.length); 60 | byte[] inputBytes = new byte[messageBytes.length + 4]; 61 | System.arraycopy(lengthBytes, 0, inputBytes, 0, 4); 62 | System.arraycopy(messageBytes, 0, inputBytes, 4, messageBytes.length); 63 | 64 | FakeSocket fakeSocket = new FakeSocket(inputBytes); 65 | try (InspectorSocket socket = new BinaryPlistSocket(fakeSocket)) { 66 | assertThat(socket.receiveMessage().get()).isEqualTo(inputMessage); 67 | } 68 | } 69 | 70 | @Test 71 | public void testReceiveEOF() throws IOException { 72 | NSDictionary inputMessage = new NSDictionary(); 73 | inputMessage.put("goodbye", "world"); 74 | byte[] messageBytes = BinaryPropertyListWriter.writeToArray(inputMessage); 75 | byte[] lengthBytes = Ints.toByteArray(messageBytes.length + 1); 76 | byte[] inputBytes = new byte[messageBytes.length + 4]; 77 | System.arraycopy(lengthBytes, 0, inputBytes, 0, 4); 78 | System.arraycopy(messageBytes, 0, inputBytes, 4, messageBytes.length); 79 | 80 | FakeSocket fakeSocket = new FakeSocket(inputBytes); 81 | try (InspectorSocket socket = new BinaryPlistSocket(fakeSocket)) { 82 | assertThat(socket.receiveMessage().isPresent()).isFalse(); 83 | } 84 | } 85 | 86 | private static final class FakeSocket extends ForwardingSocket { 87 | private final InputStream socketIn; 88 | private final ByteArrayOutputStream socketOut = new ByteArrayOutputStream(); 89 | 90 | private FakeSocket(byte[] inputBytes) { 91 | super(new Socket()); 92 | socketIn = new ByteArrayInputStream(inputBytes); 93 | } 94 | 95 | private byte[] outputBytes() { 96 | return socketOut.toByteArray(); 97 | } 98 | 99 | @Override 100 | public InputStream getInputStream() { 101 | return socketIn; 102 | } 103 | 104 | @Override 105 | public OutputStream getOutputStream() { 106 | return socketOut; 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/real/IdeviceCommands.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.real; 16 | 17 | import static com.google.iosdevicecontrol.command.Command.command; 18 | 19 | import com.google.common.collect.ImmutableList; 20 | import com.google.common.collect.ImmutableMap; 21 | import com.google.iosdevicecontrol.command.Command; 22 | import com.google.iosdevicecontrol.command.CommandExecutor; 23 | import com.google.iosdevicecontrol.command.CommandProcess; 24 | import com.google.iosdevicecontrol.command.CommandStartException; 25 | import java.nio.file.Path; 26 | import java.nio.file.Paths; 27 | import java.util.Arrays; 28 | import java.util.List; 29 | import java.util.function.UnaryOperator; 30 | 31 | /** The binaries provided by libimobiledevice and friends. */ 32 | final class IdeviceCommands { 33 | static CommandProcess id(CommandExecutor executor, String... args) { 34 | return exec(executor, "idevice_id", ImmutableList.copyOf(args), UnaryOperator.identity()); 35 | } 36 | 37 | static CommandProcess exec(Command command) { 38 | try { 39 | return command.start(); 40 | } catch (CommandStartException e) { 41 | throw new IllegalStateException("Have the idevice commands been installed?", e); 42 | } 43 | } 44 | 45 | private final CommandExecutor executor; 46 | private final String udid; 47 | 48 | IdeviceCommands(CommandExecutor executor, String udid) { 49 | this.executor = executor; 50 | this.udid = udid; 51 | } 52 | 53 | CommandProcess apprunner(String... args) { 54 | return exec("idevice-app-runner", args); 55 | } 56 | 57 | CommandProcess crashreport(String... args) { 58 | return exec("idevicecrashreport", args); 59 | } 60 | 61 | CommandProcess date(String... args) { 62 | return exec("idevicedate", args); 63 | } 64 | 65 | CommandProcess diagnostics(String... args) { 66 | return exec("idevicediagnostics", args); 67 | } 68 | 69 | CommandProcess imagemounter(String... args) { 70 | return exec("ideviceimagemounter", args); 71 | } 72 | 73 | CommandProcess info(String... args) { 74 | return exec("ideviceinfo", args); 75 | } 76 | 77 | CommandProcess installer(String... args) { 78 | return exec("ideviceinstaller", args); 79 | } 80 | 81 | CommandProcess screenshot(String... args) { 82 | return exec("idevicescreenshot", args); 83 | } 84 | 85 | CommandProcess syslog(Path logPath, String... args) { 86 | return exec("idevicesyslog", args, c -> c.withStdoutTo(logPath)); 87 | } 88 | 89 | CommandProcess webinspectorproxy(String... args) { 90 | return exec("idevicewebinspectorproxy", args); 91 | } 92 | 93 | private CommandProcess exec(String filename, String... args) { 94 | return exec(filename, args, UnaryOperator.identity()); 95 | } 96 | 97 | private CommandProcess exec(String filename, String[] args, UnaryOperator transform) { 98 | ImmutableList commandArgs = 99 | ImmutableList.builder().add("-u").add(udid).addAll(Arrays.asList(args)).build(); 100 | return exec(executor, filename, commandArgs, transform); 101 | } 102 | 103 | /** 104 | * Starts an idevice command and translates any raised IOException to an IllegalStateException. If 105 | * start raises IOException, the binary file probably does not exist or is not executable; i.e. 106 | * the idevice commands were not installed properly prior to using this library. 107 | */ 108 | private static CommandProcess exec( 109 | CommandExecutor executor, 110 | String filename, 111 | List commandArgs, 112 | UnaryOperator transform) { 113 | // TODO(user): Install idevice commands to temp on the fly. 114 | Path path = Paths.get("/usr/local/bin", filename); 115 | Command command = 116 | command(path) 117 | .withExecutor(executor) 118 | .withArguments(commandArgs) 119 | .withEnvironment(ImmutableMap.of()); 120 | command = transform.apply(command); 121 | return exec(command); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/real/RealAppProcess.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.real; 16 | 17 | import static com.google.common.base.Preconditions.checkNotNull; 18 | import static com.google.iosdevicecontrol.IosDeviceException.Remedy.REINSTALL_APP; 19 | import static com.google.iosdevicecontrol.IosDeviceException.Remedy.RESTART_DEVICE; 20 | import static java.util.concurrent.TimeUnit.NANOSECONDS; 21 | 22 | import com.google.common.collect.ImmutableMap; 23 | import com.google.iosdevicecontrol.command.CommandFailureException; 24 | import com.google.iosdevicecontrol.command.CommandProcess; 25 | import com.google.iosdevicecontrol.IosAppProcess; 26 | import com.google.iosdevicecontrol.IosDevice; 27 | import com.google.iosdevicecontrol.IosDeviceException; 28 | import com.google.iosdevicecontrol.IosDeviceException.Remedy; 29 | import java.io.Reader; 30 | import java.time.Duration; 31 | import java.util.Map.Entry; 32 | import java.util.concurrent.TimeoutException; 33 | import java.util.regex.Pattern; 34 | 35 | /** Implementation of {@link IosAppProcess} backed by an idevice-app-runner command process. */ 36 | final class RealAppProcess implements IosAppProcess { 37 | private static final ImmutableMap LAST_LINE_PATTERN_TO_REMEDY; 38 | 39 | static { 40 | ImmutableMap.Builder pattern2Remedy = ImmutableMap.builder(); 41 | // http://stackoverflow.com/questions/26287365 42 | pattern2Remedy.put(Pattern.compile("\\$E4294967295#"), REINSTALL_APP); 43 | // https://developer.apple.com/library/ios/qa/qa1682/_index.html 44 | pattern2Remedy.put(Pattern.compile("\\$Efailed to get the task for process.*#"), REINSTALL_APP); 45 | // http://stackoverflow.com/questions/10167442 46 | pattern2Remedy.put(Pattern.compile("\\$ENo such file or directory.*#"), REINSTALL_APP); 47 | // http://stackoverflow.com/questions/10833151 48 | pattern2Remedy.put(Pattern.compile("\\$ENotFound#"), REINSTALL_APP); 49 | // http://stackoverflow.com/questions/26032085 50 | pattern2Remedy.put(Pattern.compile("\\$Etimed out trying to launch app#"), RESTART_DEVICE); 51 | pattern2Remedy.put(Pattern.compile("Unknown APPID"), REINSTALL_APP); 52 | LAST_LINE_PATTERN_TO_REMEDY = pattern2Remedy.build(); 53 | } 54 | 55 | private final IosDevice device; 56 | private final CommandProcess process; 57 | 58 | RealAppProcess(IosDevice device, CommandProcess process) { 59 | this.device = checkNotNull(device); 60 | this.process = checkNotNull(process); 61 | } 62 | 63 | @Override 64 | public RealAppProcess kill() { 65 | process.kill(); 66 | return this; 67 | } 68 | 69 | @Override 70 | public String await() throws IosDeviceException, InterruptedException { 71 | try { 72 | return process.await().stdoutStringUtf8(); 73 | } catch (CommandFailureException e) { 74 | throw mapCommandFailureException(e); 75 | } 76 | } 77 | 78 | @Override 79 | public String await(Duration timeout) 80 | throws IosDeviceException, TimeoutException, InterruptedException { 81 | try { 82 | return process.await(timeout.toNanos(), NANOSECONDS).stdoutStringUtf8(); 83 | } catch (CommandFailureException e) { 84 | throw mapCommandFailureException(e); 85 | } 86 | } 87 | 88 | @Override 89 | public Reader outputReader() { 90 | return process.stdoutReaderUtf8(); 91 | } 92 | 93 | private IosDeviceException mapCommandFailureException(CommandFailureException e) { 94 | String stderr = e.result().stderrStringUtf8().trim(); 95 | int lastLineIndex = stderr.lastIndexOf('\n'); 96 | String lastLine = lastLineIndex < 0 ? stderr : stderr.substring(lastLineIndex); 97 | // See if there's a suggested remedy for this error. 98 | for (Entry entry : LAST_LINE_PATTERN_TO_REMEDY.entrySet()) { 99 | if (entry.getKey().matcher(lastLine).find()) { 100 | return new IosDeviceException(device, e, entry.getValue()); 101 | } 102 | } 103 | 104 | return new IosDeviceException(device, e); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/util/Resource.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.util; 16 | 17 | import static com.google.common.base.Preconditions.checkArgument; 18 | 19 | import com.google.auto.value.AutoValue; 20 | import com.google.common.annotations.VisibleForTesting; 21 | import com.google.common.collect.Interner; 22 | import com.google.common.collect.Interners; 23 | import java.io.File; 24 | import java.io.IOException; 25 | import java.io.InputStream; 26 | import java.nio.file.Files; 27 | import java.nio.file.Path; 28 | 29 | /** 30 | * A wrapper to a Java resource, for easy and memoized conversion to a path on disk. 31 | */ 32 | @AutoValue 33 | public abstract class Resource { 34 | private static final ClassLoader CLASS_LOADER = Resource.class.getClassLoader(); 35 | private static final Interner INTERNER = Interners.newStrongInterner(); 36 | 37 | /** The default strategy for copying a resource to a file system path. */ 38 | private static final ResourceToPathCopier DEFAULT_COPIER = 39 | new ResourceToPathCopier() { 40 | @Override 41 | public Path copy(String resourceName) throws IOException { 42 | InputStream stream = CLASS_LOADER.getResourceAsStream(resourceName); 43 | // On some OS's the resource separator '/' differs from the file separator. 44 | Path path = RESOURCE_ROOT.get().resolve(resourceName.replace('/', File.separatorChar)); 45 | Files.createDirectories(path.getParent()); 46 | Files.copy(stream, path); 47 | return path; 48 | } 49 | }; 50 | 51 | /** 52 | * Returns the Java resource with the specified name. 53 | * 54 | * @throws IllegalArgumentException - if a resource of that name does not exist 55 | */ 56 | public static Resource named(String name) { 57 | return Resource.named(name, DEFAULT_COPIER); 58 | } 59 | 60 | /** 61 | * Returns the name of the resource. 62 | */ 63 | public abstract String name(); 64 | 65 | private final LazyPath path = 66 | new LazyPath() { 67 | @Override 68 | Path create() throws IOException { 69 | return copier().copy(name()); 70 | } 71 | }; 72 | 73 | /** 74 | * Extracts the resource to a path on the default filesystem. 75 | * 76 | * @throws IOException - if an I/O error occurs 77 | */ 78 | public Path toPath() throws IOException { 79 | return path.get(); 80 | } 81 | 82 | /** 83 | * Temporary directory under which we will extract all resources. 84 | */ 85 | private static final LazyPath RESOURCE_ROOT = 86 | new LazyPath() { 87 | @Override 88 | Path create() throws IOException { 89 | return Files.createTempDirectory("resources"); 90 | } 91 | }; 92 | 93 | /** 94 | * A path on the default file system that is lazily created. 95 | */ 96 | private abstract static class LazyPath { 97 | private final CheckedCallable callable = 98 | CheckedCallables.memoize( 99 | new CheckedCallable() { 100 | @Override 101 | public Path call() throws IOException { 102 | return create(); 103 | } 104 | }); 105 | 106 | private final Path get() throws IOException { 107 | return callable.call(); 108 | } 109 | 110 | abstract Path create() throws IOException; 111 | } 112 | 113 | // 114 | // The following members exist only to mock out the resource copying for testing. 115 | // 116 | 117 | @VisibleForTesting 118 | static Resource named(String name, ResourceToPathCopier copier) { 119 | checkArgument(CLASS_LOADER.getResource(name) != null, "Resource does not exist: %s", name); 120 | return INTERNER.intern(new AutoValue_Resource(name, copier)); 121 | } 122 | 123 | @VisibleForTesting 124 | abstract ResourceToPathCopier copier(); 125 | 126 | @VisibleForTesting 127 | interface ResourceToPathCopier { 128 | Path copy(String resourceName) throws IOException; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/webinspector/ApplicationMessage.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.webinspector; 16 | 17 | import com.google.common.base.Optional; 18 | import com.google.common.base.Supplier; 19 | import com.google.common.base.Suppliers; 20 | 21 | /** Skeletal support for a message containing an application. */ 22 | abstract class ApplicationMessage extends InspectorMessage { 23 | /** Abstract builder for application messages. */ 24 | abstract static class Builder> extends InspectorMessage.Builder { 25 | @Override 26 | public abstract B applicationBundleId(String applicationBundleId); 27 | 28 | @Override 29 | public abstract B applicationId(String applicationId); 30 | 31 | @Override 32 | public abstract B applicationName(String applicationName); 33 | 34 | @Override 35 | public final B hostApplicationId(String hostApplicationId) { 36 | return optionalHostApplicationId(hostApplicationId); 37 | } 38 | 39 | @Override 40 | public abstract B isApplicationActive(boolean isApplicationActive); 41 | 42 | @Override 43 | public abstract B isApplicationProxy(boolean isApplicationProxy); 44 | 45 | @Override 46 | public final B isApplicationReady(boolean isApplicationReady) { 47 | return optionalIsApplicationReady(isApplicationReady); 48 | } 49 | 50 | @Override 51 | public final B remoteAutomationEnabled(boolean remoteAutomationEnabled) { 52 | return optionalRemoteAutomationEnabled(remoteAutomationEnabled); 53 | } 54 | 55 | abstract B optionalHostApplicationId(String hostApplicationId); 56 | 57 | abstract B optionalIsApplicationReady(boolean isApplicationReady); 58 | 59 | abstract B optionalRemoteAutomationEnabled(boolean remoteAutomationEnabled); 60 | } 61 | 62 | private final Supplier application = 63 | Suppliers.memoize( 64 | () -> { 65 | InspectorApplication.Builder builder = 66 | InspectorApplication.builder() 67 | .applicationBundleId(applicationBundleId()) 68 | .applicationId(applicationId()) 69 | .applicationName(applicationName()) 70 | .isApplicationActive(isApplicationActive()) 71 | .isApplicationProxy(isApplicationProxy()); 72 | if (optionalHostApplicationId().isPresent()) { 73 | builder.hostApplicationId(optionalHostApplicationId().get()); 74 | } 75 | if (optionalIsApplicationReady().isPresent()) { 76 | builder.isApplicationReady(optionalIsApplicationReady().get()); 77 | } 78 | if (optionalRemoteAutomationEnabled().isPresent()) { 79 | builder.remoteAutomationEnabled(optionalRemoteAutomationEnabled().get()); 80 | } 81 | return builder.build(); 82 | }); 83 | 84 | /** Returns the properties of this message as an application dictionary. */ 85 | public InspectorApplication asApplication() { 86 | return application.get(); 87 | } 88 | 89 | @Override 90 | public abstract String applicationBundleId(); 91 | 92 | @Override 93 | public abstract String applicationId(); 94 | 95 | @Override 96 | public abstract String applicationName(); 97 | 98 | public abstract Optional optionalHostApplicationId(); 99 | 100 | @Override 101 | public abstract boolean isApplicationActive(); 102 | 103 | @Override 104 | public abstract boolean isApplicationProxy(); 105 | 106 | public abstract Optional optionalIsApplicationReady(); 107 | 108 | public abstract Optional optionalRemoteAutomationEnabled(); 109 | 110 | @Override 111 | final String hostApplicationId() { 112 | return fromOptional(optionalHostApplicationId()); 113 | } 114 | 115 | @Override 116 | final boolean isApplicationReady() { 117 | return fromOptional(optionalIsApplicationReady()); 118 | } 119 | 120 | @Override 121 | final boolean remoteAutomationEnabled() { 122 | return fromOptional(optionalRemoteAutomationEnabled()); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/real/CfgutilCommands.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.real; 16 | 17 | import static com.google.common.base.Preconditions.checkNotNull; 18 | import static com.google.common.base.Preconditions.checkState; 19 | import static com.google.iosdevicecontrol.command.Command.command; 20 | 21 | import com.google.auto.value.AutoValue; 22 | import com.google.common.collect.ImmutableList; 23 | import com.google.common.collect.ImmutableMap; 24 | import com.google.iosdevicecontrol.command.Command; 25 | import com.google.iosdevicecontrol.command.CommandExecutor; 26 | import com.google.iosdevicecontrol.command.CommandProcess; 27 | import com.google.iosdevicecontrol.command.CommandStartException; 28 | import java.nio.file.Path; 29 | import java.nio.file.Paths; 30 | import java.util.List; 31 | import java.util.Optional; 32 | 33 | /** 34 | * Utility to execute the cfgutil subcommands against a device. The format of all command output is 35 | * plist XML. 36 | */ 37 | final class CfgutilCommands { 38 | private static final Path CFGUTIL = Paths.get("/usr/local/bin/cfgutil"); 39 | 40 | /** Paths to the certificate and private key of a supervision identity. */ 41 | @AutoValue 42 | abstract static class SupervisionIdentity { 43 | static SupervisionIdentity create(Path certPath, Path keyPath) { 44 | return new AutoValue_CfgutilCommands_SupervisionIdentity(certPath, keyPath); 45 | } 46 | 47 | abstract Path certificatePath(); 48 | 49 | abstract Path privateKeyPath(); 50 | } 51 | 52 | static CommandProcess list(CommandExecutor executor) { 53 | return exec(executor, ImmutableList.of("list")); 54 | } 55 | 56 | private final CommandExecutor executor; 57 | private final String ecid; 58 | private final Optional supervisionId; 59 | 60 | CfgutilCommands( 61 | CommandExecutor executor, String ecid, Optional supervisionId) { 62 | this.executor = checkNotNull(executor); 63 | this.ecid = checkNotNull(ecid); 64 | this.supervisionId = checkNotNull(supervisionId); 65 | } 66 | 67 | boolean isSupervised() { 68 | return supervisionId.isPresent(); 69 | } 70 | 71 | CommandProcess get(String property) { 72 | return exec("get", property); 73 | } 74 | 75 | CommandProcess installProfile(String profilePath) { 76 | return execSupervised("install-profile", profilePath); 77 | } 78 | 79 | CommandProcess pair() { 80 | return execSupervised("pair"); 81 | } 82 | 83 | CommandProcess removeProfile(String pathOrIdentifier) { 84 | return execSupervised("remove-profile", pathOrIdentifier); 85 | } 86 | 87 | private CommandProcess exec(String subcommand, String... args) { 88 | ImmutableList arguments = 89 | ImmutableList.builder().add("-e", ecid).add(subcommand).add(args).build(); 90 | return exec(executor, arguments); 91 | } 92 | 93 | private CommandProcess execSupervised(String subcommand, String... args) { 94 | checkState( 95 | isSupervised(), 96 | "must set a supervision identity in the device host to use `cfgutil %s`", 97 | subcommand); 98 | ImmutableList arguments = 99 | ImmutableList.builder() 100 | .add("-e", ecid) 101 | .add("-C", supervisionId.get().certificatePath().toString()) 102 | .add("-K", supervisionId.get().privateKeyPath().toString()) 103 | .add(subcommand) 104 | .add(args) 105 | .build(); 106 | return exec(executor, arguments); 107 | } 108 | 109 | /** 110 | * Starts an cfgutil command and translates any raised CommandStartException to an 111 | * IllegalStateException. If start raises IllegalStateException, the Apple Configurator 2 112 | * automation tools are probably not installed. 113 | */ 114 | private static CommandProcess exec(CommandExecutor executor, List arguments) { 115 | Command command = 116 | command(CFGUTIL) 117 | .withExecutor(executor) 118 | .withArguments("--format", "plist") 119 | .withArgumentsAppended(arguments) 120 | .withEnvironment(ImmutableMap.of()); 121 | try { 122 | return command.start(); 123 | } catch (CommandStartException e) { 124 | throw new IllegalStateException( 125 | "Have the Apple Configurator automation tools been installed?", e); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/IosDevice.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol; 16 | 17 | import com.google.common.collect.ImmutableSet; 18 | import java.nio.file.Path; 19 | 20 | /** 21 | * An iOS device. This device is not necessarily connected to any device host. All implementations 22 | * must ensure that two devices are equal if and only if their udids are equal. 23 | */ 24 | public interface IosDevice { 25 | /** Returns the udid of the device. */ 26 | String udid(); 27 | 28 | /** Returns whether the device is responsive to communication from the host. */ 29 | boolean isResponsive(); 30 | 31 | /** 32 | * Returns the model of the device. 33 | * 34 | * @throws IosDeviceException - if there is an error communicating with the device 35 | */ 36 | IosModel model() throws IosDeviceException; 37 | 38 | /** 39 | * Returns the version of the device. 40 | * 41 | * @throws IosDeviceException - if there is an error communicating with the device 42 | */ 43 | IosVersion version() throws IosDeviceException; 44 | 45 | /** 46 | * Lists the application information of all installed applications on the device. 47 | * 48 | * @throws IosDeviceException - if there is an error communicating with the device 49 | */ 50 | ImmutableSet listApplications() throws IosDeviceException; 51 | 52 | /** 53 | * Returns whether an application with the specified bundle ID is installed. 54 | * 55 | * @throws IosDeviceException - if there is an error communicating with the device 56 | */ 57 | boolean isApplicationInstalled(IosAppBundleId bundleId) throws IosDeviceException; 58 | 59 | /** 60 | * Installs the specified application from a .app folder or .ipa archive on the device. If an 61 | * application with the same bundle ID is already installed, it will be overwritten. 62 | * 63 | * @throws IosDeviceException - if there is an error communicating with the device 64 | */ 65 | void installApplication(Path ipaOrAppPath) throws IosDeviceException; 66 | 67 | /** 68 | * Uninstall the application with the specified bundle ID on the device. If an application with 69 | * this bundle ID is not installed, this is a noop. 70 | * 71 | * @throws IosDeviceException - if there is an error communicating with the device 72 | */ 73 | void uninstallApplication(IosAppBundleId bundleId) throws IosDeviceException; 74 | 75 | /** 76 | * Runs the specified iOS application with the specified args and returns immediately with a 77 | * {@link IosAppProcess}, which is a future to the text output of the running application. 78 | * 79 | * @throws IosDeviceException - if there is an error communicating with the device 80 | */ 81 | IosAppProcess runApplication(IosAppBundleId bundleId, String... args) throws IosDeviceException; 82 | 83 | /** 84 | * Starts capturing the system log for the device and writes it to the provided path. Returns an 85 | * {@link IosDeviceResource}, which can be used in a try-with-resources block. 86 | * 87 | * @throws IosDeviceException - if there was an error communicating with the device 88 | * @throws IllegalStateException - if system log capturing is already started 89 | */ 90 | IosDeviceResource startSystemLogger(Path logPath) throws IosDeviceException; 91 | 92 | /** 93 | * Copies the crash logs to the specified directory and removes them from the device. 94 | * 95 | * @throws IosDeviceException - if there was an error communicating with the device 96 | */ 97 | void pullCrashLogs(Path directory) throws IosDeviceException; 98 | 99 | /** 100 | * Clears the crash logs on the device. 101 | * 102 | * @throws IosDeviceException - if there was an error communicating with the device 103 | */ 104 | void clearCrashLogs() throws IosDeviceException; 105 | 106 | /** 107 | * Opens a socket to communicate with the web inspector. 108 | * 109 | * @throws IosDeviceException - if there was an error communicating with the device 110 | */ 111 | IosDeviceSocket openWebInspectorSocket() throws IosDeviceException; 112 | 113 | /** 114 | * Restarts the device and waits for it to be responsive. 115 | * 116 | * @throws IosDeviceException - if there is an error communicating with the device 117 | */ 118 | void restart() throws IosDeviceException; 119 | 120 | /** 121 | * Takes a screenshot in PNG format and returns the bytes. 122 | * 123 | * @throws IosDeviceException - if there is an error communicating with the device 124 | */ 125 | byte[] takeScreenshot() throws IosDeviceException; 126 | } 127 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/command/InputSource.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.command; 16 | 17 | import static com.google.common.base.Preconditions.checkState; 18 | 19 | import com.google.auto.value.AutoValue; 20 | import com.google.common.base.Optional; 21 | import com.google.common.io.ByteSource; 22 | import java.io.FileDescriptor; 23 | import java.io.FileInputStream; 24 | import java.io.FilterInputStream; 25 | import java.io.IOException; 26 | import java.io.InputStream; 27 | import java.nio.file.Files; 28 | import java.nio.file.Path; 29 | 30 | /** 31 | * A source from which a command reads its input. 32 | */ 33 | @AutoValue 34 | public abstract class InputSource { 35 | /** 36 | * The kind of source from which command input will be read. 37 | */ 38 | public enum Kind { 39 | /** Input from a file. */ 40 | FILE, 41 | /** Input from the parent JVM process; usually equivalent to {@link System#in}. */ 42 | // TODO(user) settle on a name, perhaps via Java API Review. 43 | JVM, 44 | /** 45 | * Input from the command process, so it may be written to {@link CommandProcess#stdinStream}. 46 | */ 47 | // TODO(user) actually provide CommandProcess#stdinStream. 48 | PROCESS, 49 | /** Input from an input stream. */ 50 | STREAM 51 | } 52 | 53 | /** 54 | * Input from the command process, so it may be written to {@link CommandProcess#stdinStream}. 55 | */ 56 | public static InputSource fromProcess() { 57 | return FROM_PROCESS; 58 | } 59 | 60 | /** 61 | * Input from the parent JVM process; usually equivalent to {@link System#in}. 62 | */ 63 | public static InputSource fromJvm() { 64 | return FROM_JVM; 65 | } 66 | 67 | /** 68 | * Input from the specified file. 69 | */ 70 | public static InputSource fromFile(Path file) { 71 | return create(Kind.FILE, FileByteSource.create(file)); 72 | } 73 | 74 | /** 75 | * Input from an input stream supplied by the specified byte source. 76 | */ 77 | public static InputSource fromStream(ByteSource byteSource) { 78 | return create(Kind.STREAM, byteSource); 79 | } 80 | 81 | private static InputSource create(Kind kind, Optional byteSource) { 82 | return new AutoValue_InputSource(kind, byteSource); 83 | } 84 | 85 | private static InputSource create(Kind kind, ByteSource byteSource) { 86 | return create(kind, Optional.of(byteSource)); 87 | } 88 | 89 | /** 90 | * The {@link Kind} of input source. 91 | */ 92 | public abstract Kind kind(); 93 | 94 | /** 95 | * The file from which input is read, if {@link #kind} is {@code FILE}. 96 | * 97 | * @throws IllegalStateException - if the input source is not a file 98 | */ 99 | public final Path file() { 100 | checkState(kind().equals(Kind.FILE), "Source is %s, not a file.", kind()); 101 | return ((FileByteSource) byteSource().get()).file(); 102 | } 103 | 104 | /** 105 | * The byte source which input is read, if {@link #kind} is {@code STREAM}. 106 | * 107 | * @throws IllegalStateException - if the input source is not a stream 108 | */ 109 | public final ByteSource streamSupplier() { 110 | checkState(kind().equals(Kind.STREAM), "Source is %s, not a stream.", kind()); 111 | return byteSource().get(); 112 | } 113 | 114 | /** 115 | * A ByteSource wrapping this input source; present when {@link #kind} is not {@code PROCESS}. 116 | */ 117 | abstract Optional byteSource(); 118 | 119 | private static final InputSource FROM_JVM = create(Kind.JVM, new ByteSource() { 120 | @Override 121 | public InputStream openStream() throws IOException { 122 | return new FilterInputStream(new FileInputStream(FileDescriptor.in)) { 123 | @Override 124 | public void close() { 125 | // Do not close the (global) file descriptor. 126 | } 127 | }; 128 | } 129 | 130 | @Override 131 | public String toString() { 132 | return "ByteSource(stdin)"; 133 | } 134 | }); 135 | 136 | private static final InputSource FROM_PROCESS = 137 | create(Kind.PROCESS, Optional.absent()); 138 | 139 | @AutoValue 140 | abstract static class FileByteSource extends ByteSource { 141 | private static FileByteSource create(Path file) { 142 | return new AutoValue_InputSource_FileByteSource(file); 143 | } 144 | 145 | abstract Path file(); 146 | 147 | @Override 148 | public InputStream openStream() throws IOException { 149 | return Files.newInputStream(file()); 150 | } 151 | 152 | @Override 153 | public String toString() { 154 | return "ByteSource(" + file() + ")"; 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/command/AsyncCopier.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.command; 16 | 17 | import com.google.common.annotations.VisibleForTesting; 18 | import com.google.common.base.Supplier; 19 | import com.google.iosdevicecontrol.util.FluentLogger; 20 | import com.google.common.io.ByteStreams; 21 | import com.google.common.util.concurrent.ThreadFactoryBuilder; 22 | import com.google.common.util.concurrent.Uninterruptibles; 23 | import java.io.IOException; 24 | import java.io.InputStream; 25 | import java.io.OutputStream; 26 | import java.util.concurrent.CountDownLatch; 27 | import java.util.concurrent.ExecutorService; 28 | import java.util.concurrent.Executors; 29 | import java.util.concurrent.Future; 30 | import java.util.logging.Level; 31 | 32 | /** Copies an InputStream to an OutputStream asynchronously. */ 33 | final class AsyncCopier { 34 | /** Injectable strategy for performing a synchronous copy. */ 35 | @VisibleForTesting 36 | interface CopyStrategy { 37 | void copy(InputStream from, OutputStream to) throws IOException; 38 | } 39 | 40 | private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 41 | 42 | @VisibleForTesting 43 | static final CopyStrategy REAL_COPY_STRATEGY = 44 | new CopyStrategy() { 45 | @Override 46 | public void copy(InputStream from, OutputStream to) throws IOException { 47 | ByteStreams.copy(from, to); 48 | } 49 | }; 50 | 51 | @VisibleForTesting 52 | static final ExecutorService realExecutorService = 53 | Executors.newCachedThreadPool( 54 | new ThreadFactoryBuilder().setNameFormat("async-copy-%d").setDaemon(true).build()); 55 | 56 | /** 57 | * Starts an asynchronous copy from the input stream to the output stream. The returned copier 58 | * assumes responsibility for closing the streams when the copy is complete. 59 | */ 60 | static AsyncCopier start(InputStream from, OutputStream to, Supplier ioExceptionLogLevel) { 61 | return new AsyncCopier(from, to, ioExceptionLogLevel, REAL_COPY_STRATEGY, realExecutorService); 62 | } 63 | 64 | private final InputStream source; 65 | private final OutputStream sink; 66 | private final Supplier ioExceptionLogLevel; 67 | private final CopyStrategy copyStrategy; 68 | private final Future copyFuture; 69 | 70 | private final CountDownLatch copyStarted = new CountDownLatch(1); 71 | private final CountDownLatch copyTerminated = new CountDownLatch(1); 72 | 73 | @VisibleForTesting 74 | AsyncCopier( 75 | InputStream source, 76 | OutputStream sink, 77 | Supplier ioExceptionLogLevel, 78 | CopyStrategy copyStrategy, 79 | ExecutorService executorService) { 80 | this.source = source; 81 | this.sink = sink; 82 | this.ioExceptionLogLevel = ioExceptionLogLevel; 83 | this.copyStrategy = copyStrategy; 84 | 85 | // Submit the copy task and wait uninterruptibly, but very briefly, for it to actually start. 86 | copyFuture = executorService.submit(new Runnable() { 87 | @Override public void run() { 88 | copy(); 89 | } 90 | }); 91 | Uninterruptibles.awaitUninterruptibly(copyStarted); 92 | } 93 | 94 | private void copy() { 95 | copyStarted.countDown(); 96 | try { 97 | try { 98 | copyStrategy.copy(source, sink); 99 | } catch (IOException e) { 100 | logger.at(ioExceptionLogLevel.get()).withCause(e).log(); 101 | } finally { 102 | closeStreams(); 103 | } 104 | } catch (RuntimeException e) { 105 | logger.atSevere().withCause(e).log(); 106 | } finally { 107 | copyTerminated.countDown(); 108 | } 109 | } 110 | 111 | /** Waits for the asynchronous copy to complete. */ 112 | void await() throws InterruptedException { 113 | copyTerminated.await(); 114 | } 115 | 116 | /** Stops the asynchronous copy and blocks until it terminates. */ 117 | void stop() throws InterruptedException { 118 | // Unfortunately, if the streams are blocked, there is no provided method to unblock them. 119 | // That said, interrupting and then closing the streams appears to work for all streams used in 120 | // practice and likely for all conceivably valid streams in existence, so we do that. 121 | // http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4514257 122 | // http://stackoverflow.com/a/4182848 123 | 124 | // Interrupt the copyFuture, in case the input source interruptible, like PipedInputStream. 125 | copyFuture.cancel(true); 126 | 127 | // Close the streams, which should unblock all other kinds of streams. 128 | closeStreams(); 129 | 130 | await(); 131 | } 132 | 133 | private void closeStreams() { 134 | try { 135 | try { 136 | source.close(); 137 | } finally { 138 | sink.close(); 139 | } 140 | } catch (IOException e) { 141 | logger.at(ioExceptionLogLevel.get()).withCause(e).log(); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /java/com/google/iosdevicecontrol/real/DevDiskImages.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.iosdevicecontrol.real; 16 | 17 | import com.google.auto.value.AutoValue; 18 | import com.google.common.base.Splitter; 19 | import java.io.IOException; 20 | import java.nio.file.DirectoryStream; 21 | import java.nio.file.Files; 22 | import java.nio.file.Path; 23 | import java.util.Iterator; 24 | import java.util.function.Predicate; 25 | import java.util.regex.Matcher; 26 | import java.util.regex.Pattern; 27 | import javax.annotation.Nullable; 28 | 29 | /** 30 | * Finds disk images for an iOS product version within a directory that has the structure of the 31 | * Xcode iPhoneOS.platform/DeviceSupport directory. That is, within this directory is any number of 32 | * image subdirectories with the name of an iOS product version. Then each of these subdirectories 33 | * contain a single image and single signature file. 34 | */ 35 | @AutoValue 36 | abstract class DevDiskImages { 37 | private static final Pattern VERSION_PATTERN = Pattern.compile("^(\\d+(?:\\.\\d+)+)"); 38 | private static final Splitter VERSION_NUM_SPLITTER = Splitter.on('.'); 39 | 40 | static final String IMAGE_EXTENSION = "dmg"; 41 | static final String SIGNATURE_EXTENSION = "signature"; 42 | 43 | private static final Predicate NATIVE_READABLE = path -> Files.isReadable(path); 44 | 45 | static DevDiskImages inDirectory(Path rootImagesDir) { 46 | return forTesting(rootImagesDir, NATIVE_READABLE); 47 | } 48 | 49 | static DevDiskImages forTesting(Path rootImagesDir, Predicate readablePredicate) { 50 | return new AutoValue_DevDiskImages(rootImagesDir, readablePredicate); 51 | } 52 | 53 | abstract Path rootImagesDirectory(); 54 | 55 | abstract Predicate readablePredicate(); 56 | 57 | /** 58 | * Finds the disk image matching the specified iOS version. 59 | * 60 | * @throws IllegalStateException - if a matching developer disk image cannot be found 61 | */ 62 | DiskImage findForVersion(String iosVersion) { 63 | Iterable deviceVersionNums = splitVersionString(iosVersion); 64 | if (deviceVersionNums == null) { 65 | throw new IllegalArgumentException("Invalid product version string: " + iosVersion); 66 | } 67 | 68 | // Find the image directory that matches the greatest number of version number components. 69 | Path bestMatchingImageDir = null; 70 | int maxMatchingVersionNums = 0; 71 | try (DirectoryStream dirStream = Files.newDirectoryStream(rootImagesDirectory())) { 72 | for (Path imageDir : dirStream) { 73 | Iterable dirVersionNums = splitVersionString(imageDir.getFileName().toString()); 74 | if (dirVersionNums != null) { 75 | int matchingVersionNums = lengthCommonPrefix(deviceVersionNums, dirVersionNums); 76 | // A matching image directory must match at least two version number components. 77 | if (matchingVersionNums > maxMatchingVersionNums && matchingVersionNums > 1) { 78 | bestMatchingImageDir = imageDir; 79 | maxMatchingVersionNums = matchingVersionNums; 80 | } 81 | } 82 | } 83 | } catch (IOException e) { 84 | throw new IllegalStateException("Error finding developer disk image", e); 85 | } 86 | 87 | if (bestMatchingImageDir == null) { 88 | throw new IllegalStateException("No disk image directory found for version: " + iosVersion); 89 | } 90 | 91 | Path image = findImageFileWithExtension(bestMatchingImageDir, IMAGE_EXTENSION); 92 | Path signature = findImageFileWithExtension(bestMatchingImageDir, SIGNATURE_EXTENSION); 93 | return new AutoValue_DevDiskImages_DiskImage(image, signature); 94 | } 95 | 96 | @AutoValue 97 | abstract static class DiskImage { 98 | abstract Path imagePath(); 99 | abstract Path signaturePath(); 100 | } 101 | 102 | private Path findImageFileWithExtension(Path dir, String ext) { 103 | Path file = null; 104 | try (DirectoryStream dirStream = Files.newDirectoryStream(dir, "*." + ext)) { 105 | Iterator imageIter = dirStream.iterator(); 106 | if (!imageIter.hasNext()) { 107 | throw new IllegalStateException("No " + ext + " image file in " + dir); 108 | } 109 | file = imageIter.next(); 110 | if (imageIter.hasNext()) { 111 | throw new IllegalStateException("Multiple " + ext + " image files in " + dir); 112 | } 113 | } catch (IOException e) { 114 | throw new IllegalStateException("Error finding " + ext + " image file in " + dir, e); 115 | } 116 | if (!readablePredicate().test(file)) { 117 | throw new IllegalStateException("Image file is not not readable: " + file); 118 | } 119 | return file; 120 | } 121 | 122 | @Nullable private static Iterable splitVersionString(String versionString) { 123 | Matcher matcher = VERSION_PATTERN.matcher(versionString); 124 | return matcher.find() ? VERSION_NUM_SPLITTER.split(matcher.group(1)) : null; 125 | } 126 | 127 | private static int lengthCommonPrefix(Iterable i1, Iterable i2) { 128 | int numMatching = 0; 129 | Iterator iter1 = i1.iterator(); 130 | Iterator iter2 = i2.iterator(); 131 | while (iter1.hasNext() && iter2.hasNext() && iter1.next().equals(iter2.next())) { 132 | numMatching++; 133 | } 134 | return numMatching; 135 | } 136 | } 137 | --------------------------------------------------------------------------------