/*
* Copyright 2015-present Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.facebook.buck.apple.simulator;
import com.facebook.buck.log.Logger;
import com.facebook.buck.util.ProcessExecutor;
import com.facebook.buck.util.ProcessExecutorParams;
import com.facebook.buck.util.UserIdFetcher;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Launches, queries, and kills Apple's {@code CoreSimulator} and related {@code launchd} services.
*/
public class AppleCoreSimulatorServiceController {
private static final Logger LOG = Logger.get(AppleCoreSimulatorServiceController.class);
private static final Pattern LAUNCHCTL_LIST_OUTPUT_PATTERN =
Pattern.compile("^(\\S+)\\s+(\\S+)\\s+(.*)$");
private static final Pattern LAUNCHCTL_PRINT_PATH_PATTERN =
Pattern.compile("^\\s*path\\s*=\\s*(.*)$");
private static final Pattern CORE_SIMULATOR_SERVICE_PATTERN =
Pattern.compile("(?i:com\\.apple\\.CoreSimulator\\.CoreSimulatorService)");
private static final Pattern ALL_SIMULATOR_SERVICES_PATTERN =
Pattern.compile(
"(?i:com\\.apple\\.iphonesimulator|UIKitApplication|SimulatorBridge|iOS Simulator|"
+ "com\\.apple\\.CoreSimulator)");
private static final int LAUNCHCTL_EXIT_SUCCESS = 0;
private static final int LAUNCHCTL_EXIT_NO_SUCH_PROCESS = 3;
private final ProcessExecutor processExecutor;
public AppleCoreSimulatorServiceController(ProcessExecutor processExecutor) {
this.processExecutor = processExecutor;
}
/**
* Returns the path on disk to the running Core Simulator service, if any. Returns {@code
* Optional.empty()} unless exactly one Core Simulator service is running.
*/
public Optional<Path> getCoreSimulatorServicePath(UserIdFetcher userIdFetcher)
throws IOException, InterruptedException {
ImmutableSet<String> coreSimulatorServiceNames =
getMatchingServiceNames(CORE_SIMULATOR_SERVICE_PATTERN);
if (coreSimulatorServiceNames.size() != 1) {
LOG.debug("Could not get core simulator service name (got %s)", coreSimulatorServiceNames);
return Optional.empty();
}
String coreSimulatorServiceName = Iterables.getOnlyElement(coreSimulatorServiceNames);
ImmutableList<String> launchctlPrintCommand =
ImmutableList.of(
"launchctl",
"print",
String.format("user/%d/%s", userIdFetcher.getUserId(), coreSimulatorServiceName));
LOG.debug(
"Getting status of service %s with %s", coreSimulatorServiceName, launchctlPrintCommand);
ProcessExecutorParams launchctlPrintParams =
ProcessExecutorParams.builder().setCommand(launchctlPrintCommand).build();
ProcessExecutor.LaunchedProcess launchctlPrintProcess =
processExecutor.launchProcess(launchctlPrintParams);
Optional<Path> result = Optional.empty();
try (InputStreamReader stdoutReader =
new InputStreamReader(launchctlPrintProcess.getInputStream(), StandardCharsets.UTF_8);
BufferedReader bufferedStdoutReader = new BufferedReader(stdoutReader)) {
String line;
while ((line = bufferedStdoutReader.readLine()) != null) {
Matcher matcher = LAUNCHCTL_PRINT_PATH_PATTERN.matcher(line);
if (matcher.matches()) {
String path = matcher.group(1);
LOG.debug("Found path of service %s: %s", coreSimulatorServiceName, path);
result = Optional.of(Paths.get(path));
break;
}
}
} finally {
processExecutor.destroyLaunchedProcess(launchctlPrintProcess);
processExecutor.waitForLaunchedProcess(launchctlPrintProcess);
}
return result;
}
private ImmutableSet<String> getMatchingServiceNames(Pattern serviceNamePattern)
throws IOException, InterruptedException {
ImmutableList<String> launchctlListCommand = ImmutableList.of("launchctl", "list");
LOG.debug("Getting list of services with %s", launchctlListCommand);
ProcessExecutorParams launchctlListParams =
ProcessExecutorParams.builder().setCommand(launchctlListCommand).build();
ProcessExecutor.LaunchedProcess launchctlListProcess =
processExecutor.launchProcess(launchctlListParams);
ImmutableSet.Builder<String> resultBuilder = ImmutableSet.builder();
try (InputStreamReader stdoutReader =
new InputStreamReader(launchctlListProcess.getInputStream(), StandardCharsets.UTF_8);
BufferedReader bufferedStdoutReader = new BufferedReader(stdoutReader)) {
String line;
while ((line = bufferedStdoutReader.readLine()) != null) {
Matcher launchctlListOutputMatcher = LAUNCHCTL_LIST_OUTPUT_PATTERN.matcher(line);
if (launchctlListOutputMatcher.matches()) {
String serviceName = launchctlListOutputMatcher.group(3);
Matcher serviceNameMatcher = serviceNamePattern.matcher(serviceName);
if (serviceNameMatcher.find()) {
LOG.debug("Found matching service name: %s", serviceName);
resultBuilder.add(serviceName);
}
}
}
} finally {
processExecutor.destroyLaunchedProcess(launchctlListProcess);
processExecutor.waitForLaunchedProcess(launchctlListProcess);
}
return resultBuilder.build();
}
/** Kills any running simulator processes or services. */
public boolean killSimulatorProcesses() throws IOException, InterruptedException {
ImmutableSet<String> simulatorServiceNames =
getMatchingServiceNames(ALL_SIMULATOR_SERVICES_PATTERN);
LOG.debug("Killing simulator services: %s", simulatorServiceNames);
boolean result = true;
for (String simulatorServiceName : simulatorServiceNames) {
if (!killService(simulatorServiceName)) {
result = false;
}
}
return result;
}
private boolean killService(String serviceName) throws IOException, InterruptedException {
ImmutableList<String> launchctlRemoveCommand =
ImmutableList.of("launchctl", "remove", serviceName);
LOG.debug("Killing simulator process with with %s", launchctlRemoveCommand);
ProcessExecutorParams launchctlRemoveParams =
ProcessExecutorParams.builder().setCommand(launchctlRemoveCommand).build();
ProcessExecutor.Result result = processExecutor.launchAndExecute(launchctlRemoveParams);
int launchctlExitCode = result.getExitCode();
LOG.debug("Command %s exited with code %d", launchctlRemoveParams, launchctlExitCode);
switch (launchctlExitCode) {
case LAUNCHCTL_EXIT_SUCCESS:
case LAUNCHCTL_EXIT_NO_SUCH_PROCESS:
// The process could have exited by itself or already been terminated by the time
// we told it to die, so we have to treat "no such process" as success.
return true;
default:
LOG.error(result.getMessageForUnexpectedResult(launchctlRemoveCommand.toString()));
return false;
}
}
}