Skip to content

Commit

Permalink
Add unit tests for altool notarizer.
Browse files Browse the repository at this point in the history
  • Loading branch information
netomi authored and mbarbero committed Sep 12, 2023
1 parent 65550f5 commit e81f0ed
Show file tree
Hide file tree
Showing 8 changed files with 341 additions and 192 deletions.
Expand Up @@ -63,7 +63,14 @@ public static Result startAndWait(ProcessBuilder processBuilder, Duration timeou
Thread.currentThread().interrupt();
}

try (AutoValue_NativeProcess_Result result = new AutoValue_NativeProcess_Result(p.exitValue(), arg0, out, err)) {
Result.Builder builder =
Result.builder()
.exitValue(p.exitValue())
.arg0(arg0)
.stdout(out)
.stderr(err);

try (Result result = builder.build()) {
return result.log();
}
}
Expand Down Expand Up @@ -162,5 +169,19 @@ public void close() {
deleteIfExists(stdout());
deleteIfExists(stderr());
}

public static Result.Builder builder() {
return new AutoValue_NativeProcess_Result.Builder();
}

@AutoValue.Builder
public static abstract class Builder {
public abstract Result.Builder exitValue(int exitValue);
public abstract Result.Builder arg0(String arg0);
public abstract Result.Builder stdout(Path stdout);
public abstract Result.Builder stderr(Path stderr);
public abstract Result build();
}

}
}
Expand Up @@ -20,74 +20,39 @@
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

public class AltoolNotarizer implements NotarizationTool {
public class AltoolNotarizer extends NotarizationTool {

private static final Logger LOGGER = LoggerFactory.getLogger(AltoolNotarizer.class);

private static final Pattern UPLOADID_PATTERN = Pattern.compile(".*The upload ID is ([A-Za-z0-9\\\\-]*).*");
private static final Pattern ERROR_MESSAGE_PATTERN = Pattern.compile(".*\"(.*)\".*");

private static final String APPLEID_PASSWORD_ENV_VAR_NAME = "APPLEID_PASSWORD";

private static final Logger LOGGER = LoggerFactory.getLogger(AltoolNotarizer.class);
private static final String TMPDIR = "TMPDIR";

@Override
public NotarizerResult upload(String appleIDUsername,
String appleIDPassword,
String primaryBundleId,
Path fileToNotarize,
Duration uploadTimeout) throws ExecutionException, IOException {
protected List<String> getUploadCommand(String appleIDUsername, String primaryBundleId, Path fileToNotarize) {
List<String> cmd = ImmutableList.<String>builder()
.add("xcrun", "altool")
.add("--notarize-app")
.add("--output-format", "xml")
.add("--username", appleIDUsername)
.add("--password", "@env:" + APPLEID_PASSWORD_ENV_VAR_NAME)
.add("--primary-bundle-id", primaryBundleId)
.add("--file", fileToNotarize.toString()).build();

Path xcrunTempFolder =
Files.createTempDirectory(fileToNotarize.getParent(),
com.google.common.io.Files.getNameWithoutExtension(fileToNotarize.toString())+ "-xcrun-notarize-app-");

ProcessBuilder processBuilder = new ProcessBuilder().command(cmd);
processBuilder.environment().put(APPLEID_PASSWORD_ENV_VAR_NAME, appleIDPassword);
processBuilder.environment().put(TMPDIR, xcrunTempFolder.toString());

try(NativeProcess.Result nativeProcessResult = NativeProcess.startAndWait(processBuilder, uploadTimeout)) {
NotarizerResult result = analyzeResult(nativeProcessResult, fileToNotarize);
LOGGER.trace("Notarization upload result:\n" + result.toString());
return result;
} catch (TimeoutException e) {
LOGGER.error("Timeout happened during notarization upload of file " + fileToNotarize, e);
throw new ExecutionException("Timeout happened during notarization upload", e);
} catch (IOException e) {
LOGGER.error("IOException happened during notarization upload of file " + fileToNotarize, e);
throw new ExecutionException("IOException happened during notarization upload", e);
} finally {
if (Files.exists(xcrunTempFolder)) {
LOGGER.trace("Deleting xcrun-notarize-app temporary folder " + xcrunTempFolder);
try (Stream<File> filesToDelete = Files.walk(xcrunTempFolder).sorted(Comparator.reverseOrder()).map(Path::toFile)) {
filesToDelete.forEach(File::delete);
} catch (IOException e) {
LOGGER.warn("IOException happened during deletion of xcrun-notarize-app temporary folder " + xcrunTempFolder, e);
}
}
}
.add("xcrun", "altool")
.add("--notarize-app")
.add("--output-format", "xml")
.add("--username", appleIDUsername)
.add("--password", "@env:" + APPLEID_PASSWORD_ENV_VAR_NAME)
.add("--primary-bundle-id", primaryBundleId)
.add("--file", fileToNotarize.toString()).build();

return cmd;
}

private NotarizerResult analyzeResult(NativeProcess.Result nativeProcessResult, Path fileToNotarize) throws ExecutionException {
protected NotarizerResult analyzeSubmissionResult(NativeProcess.Result nativeProcessResult,
Path fileToNotarize) throws ExecutionException {

NotarizerResult.Builder resultBuilder = NotarizerResult.builder();
try {
PListDict plist = PListDict.fromXML(nativeProcessResult.stdoutAsStream());
Expand Down Expand Up @@ -167,54 +132,27 @@ private Optional<String> parseAppleRequestID(String message) {
}

@Override
public NotarizationInfoResult retrieveInfo(String appleIDUsername,
String appleIDPassword,
String appleRequestUUID,
Duration pollingTimeout,
OkHttpClient httpClient) throws ExecutionException, IOException {
protected List<String> getInfoCommand(String appleIDUsername, String appleRequestUUID) {
List<String> cmd = ImmutableList.<String>builder().add("xcrun", "altool")
.add("--notarization-info", appleRequestUUID.toString())
.add("--output-format", "xml")
.add("--username", appleIDUsername)
.add("--password", "@env:" + APPLEID_PASSWORD_ENV_VAR_NAME)
.build();
.add("--notarization-info", appleRequestUUID.toString())
.add("--output-format", "xml")
.add("--username", appleIDUsername)
.add("--password", "@env:" + APPLEID_PASSWORD_ENV_VAR_NAME)
.build();

Path xcrunTempFolder = Files.createTempDirectory("-xcrun-notarization-info-");
return cmd;
}

ProcessBuilder processBuilder = new ProcessBuilder().command(cmd);
processBuilder.environment().put(APPLEID_PASSWORD_ENV_VAR_NAME, appleIDPassword);
processBuilder.environment().put(TMPDIR, xcrunTempFolder.toString());
@Override
protected NotarizationInfoResult analyzeInfoResult(NativeProcess.Result nativeProcessResult,
String appleRequestUUID,
OkHttpClient httpClient) throws ExecutionException {

NotarizationInfoResult.Builder resultBuilder = NotarizationInfoResult.builder();
try (NativeProcess.Result result = NativeProcess.startAndWait(processBuilder, pollingTimeout)) {
analyseResults(result, resultBuilder, appleRequestUUID, httpClient);
} catch (IOException e) {
LOGGER.error("Error while retrieving notarization info of request '" + appleRequestUUID + "'", e);
throw new ExecutionException("Failed to retrieve notarization info", e);
} catch (TimeoutException e) {
LOGGER.error("Timeout while retrieving notarization info of request '" + appleRequestUUID + "'", e);
throw new ExecutionException("Timeout while retrieving notarization info", e);
} finally {
LOGGER.trace("Deleting xcrun-notarization-info temporary folder " + xcrunTempFolder);
try (Stream<File> filesToDelete = Files.walk(xcrunTempFolder).sorted(Comparator.reverseOrder()).map(Path::toFile)) {
filesToDelete.forEach(File::delete);
} catch (IOException e) {
LOGGER.warn("IOException happened during deletion of xcrun-notarization-info temporary folder " + xcrunTempFolder, e);
}
}
NotarizationInfoResult result = resultBuilder.build();
LOGGER.trace("Notarization info retriever result:\n{}", result);
return result;
}

private void analyseResults(NativeProcess.Result result,
NotarizationInfoResult.Builder resultBuilder,
String appleRequestUUID,
OkHttpClient httpClient) throws ExecutionException {
try {
PListDict plist = PListDict.fromXML(result.stdoutAsStream());
PListDict plist = PListDict.fromXML(nativeProcessResult.stdoutAsStream());

if (result.exitValue() == 0) {
if (nativeProcessResult.exitValue() == 0) {
Map<?, ?> notarizationInfoList = (Map<?, ?>) plist.get("notarization-info");
if (notarizationInfoList != null && !notarizationInfoList.isEmpty()) {
parseNotarizationInfo(plist, notarizationInfoList, resultBuilder, httpClient);
Expand All @@ -236,22 +174,23 @@ private void analyseResults(NativeProcess.Result result,
.status(NotarizationInfoResult.Status.NOTARIZATION_IN_PROGRESS)
.message("The software asset has already been uploaded. Notarization in progress");
default:
resultBuilder.message("Failed to notarize the requested file. Remote service error code = " + firstProductErrorCode.getAsInt() + " (xcrun altool exit value ="+result.exitValue()+").");
resultBuilder.message("Failed to notarize the requested file. Remote service error code = " + firstProductErrorCode.getAsInt() + " (xcrun altool exit value ="+nativeProcessResult.exitValue()+").");
break;
}
} else {
Optional<String> errorMessage = plist.messageFromFirstProductError();
if (errorMessage.isPresent()) {
resultBuilder.message("Failed to notarize the requested file (xcrun altool exit value ="+result.exitValue()+"). Reason: " + errorMessage.get());
resultBuilder.message("Failed to notarize the requested file (xcrun altool exit value ="+nativeProcessResult.exitValue()+"). Reason: " + errorMessage.get());
} else {
resultBuilder.message("Failed to notarize the requested file (xcrun altool exit value ="+result.exitValue()+").");
resultBuilder.message("Failed to notarize the requested file (xcrun altool exit value ="+nativeProcessResult.exitValue()+").");
}
}
}
} catch (IOException | SAXException e) {
LOGGER.error("Cannot parse notarization info for request '" + appleRequestUUID + "'", e);
throw new ExecutionException("Failed to retrieve notarization info.", e);
}
return resultBuilder.build();
}

private void parseNotarizationInfo(PListDict plist, Map<?, ?> notarizationInfo,
Expand Down Expand Up @@ -284,6 +223,10 @@ private void parseNotarizationInfo(PListDict plist, Map<?, ?> notarizationInfo,
}

private String extractLogFromServer(Map<?, ?> notarizationInfo, OkHttpClient httpClient) {
if (httpClient == null) {
return "Can not retrieve log, httpClient is null";
}

Object logFileUrlStr = notarizationInfo.get("LogFileURL");
if (logFileUrlStr instanceof String) {
HttpUrl logfileUrl = HttpUrl.parse((String)logFileUrlStr);
Expand Down
Expand Up @@ -8,22 +8,107 @@
package org.eclipse.cbi.ws.macos.notarization.xcrun.common;

import okhttp3.OkHttpClient;
import org.eclipse.cbi.ws.macos.notarization.process.NativeProcess;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.stream.Stream;

public interface NotarizationTool {
NotarizerResult upload(String appleIDUsername,
String appleIDPassword,
String primaryBundleId,
Path fileToNotarize,
Duration uploadTimeout) throws ExecutionException, IOException;

NotarizationInfoResult retrieveInfo(String appleIDUsername,
String appleIDPassword,
String appleRequestUUID,
Duration pollingTimeout,
OkHttpClient httpClient) throws ExecutionException, IOException;
public abstract class NotarizationTool {
protected static final String APPLEID_PASSWORD_ENV_VAR_NAME = "APPLEID_PASSWORD";

private static final Logger LOGGER = LoggerFactory.getLogger(NotarizationTool.class);
private static final String TMPDIR = "TMPDIR";

public NotarizerResult upload(String appleIDUsername,
String appleIDPassword,
String primaryBundleId,
Path fileToNotarize,
Duration uploadTimeout) throws ExecutionException, IOException {

List<String> cmd = getUploadCommand(appleIDUsername, primaryBundleId, fileToNotarize);

Path xcrunTempFolder =
Files.createTempDirectory(fileToNotarize.getParent(),
com.google.common.io.Files.getNameWithoutExtension(fileToNotarize.toString())+ "-xcrun-notarize-app-");

ProcessBuilder processBuilder = new ProcessBuilder().command(cmd);
processBuilder.environment().put(APPLEID_PASSWORD_ENV_VAR_NAME, appleIDPassword);
processBuilder.environment().put(TMPDIR, xcrunTempFolder.toString());

try(NativeProcess.Result nativeProcessResult = NativeProcess.startAndWait(processBuilder, uploadTimeout)) {
NotarizerResult result = analyzeSubmissionResult(nativeProcessResult, fileToNotarize);
LOGGER.trace("Notarization upload result:\n" + result.toString());
return result;
} catch (TimeoutException e) {
LOGGER.error("Timeout happened during notarization upload of file " + fileToNotarize, e);
throw new ExecutionException("Timeout happened during notarization upload", e);
} catch (IOException e) {
LOGGER.error("IOException happened during notarization upload of file " + fileToNotarize, e);
throw new ExecutionException("IOException happened during notarization upload", e);
} finally {
if (Files.exists(xcrunTempFolder)) {
LOGGER.trace("Deleting xcrun-notarize-app temporary folder " + xcrunTempFolder);
try (Stream<File> filesToDelete = Files.walk(xcrunTempFolder).sorted(Comparator.reverseOrder()).map(Path::toFile)) {
filesToDelete.forEach(File::delete);
} catch (IOException e) {
LOGGER.warn("IOException happened during deletion of xcrun-notarize-app temporary folder " + xcrunTempFolder, e);
}
}
}
}

protected abstract List<String> getUploadCommand(String appleIDUsername, String primaryBundleId, Path fileToNotarize);

protected abstract NotarizerResult analyzeSubmissionResult(NativeProcess.Result nativeProcessResult,
Path fileToNotarize) throws ExecutionException;

public NotarizationInfoResult retrieveInfo(String appleIDUsername,
String appleIDPassword,
String appleRequestUUID,
Duration pollingTimeout,
OkHttpClient httpClient) throws ExecutionException, IOException {

List<String> cmd = getInfoCommand(appleIDUsername, appleRequestUUID);

Path xcrunTempFolder = Files.createTempDirectory("-xcrun-notarization-info-");

ProcessBuilder processBuilder = new ProcessBuilder().command(cmd);
processBuilder.environment().put(APPLEID_PASSWORD_ENV_VAR_NAME, appleIDPassword);
processBuilder.environment().put(TMPDIR, xcrunTempFolder.toString());

try (NativeProcess.Result nativeProcessResult = NativeProcess.startAndWait(processBuilder, pollingTimeout)) {
NotarizationInfoResult result = analyzeInfoResult(nativeProcessResult, appleRequestUUID, httpClient);
LOGGER.trace("Notarization info retriever result:\n{}", result);
return result;
} catch (IOException e) {
LOGGER.error("Error while retrieving notarization info of request '" + appleRequestUUID + "'", e);
throw new ExecutionException("Failed to retrieve notarization info", e);
} catch (TimeoutException e) {
LOGGER.error("Timeout while retrieving notarization info of request '" + appleRequestUUID + "'", e);
throw new ExecutionException("Timeout while retrieving notarization info", e);
} finally {
LOGGER.trace("Deleting xcrun-notarization-info temporary folder " + xcrunTempFolder);
try (Stream<File> filesToDelete = Files.walk(xcrunTempFolder).sorted(Comparator.reverseOrder()).map(Path::toFile)) {
filesToDelete.forEach(File::delete);
} catch (IOException e) {
LOGGER.warn("IOException happened during deletion of xcrun-notarization-info temporary folder " + xcrunTempFolder, e);
}
}
}

protected abstract List<String> getInfoCommand(String appleIDUsername, String appleRequestUUID);

protected abstract NotarizationInfoResult analyzeInfoResult(NativeProcess.Result nativeProcessResult,
String appleRequestUUID,
OkHttpClient httpClient) throws ExecutionException;
}

0 comments on commit e81f0ed

Please sign in to comment.