blob: e6c1b4d8c991853b3e0c68abbcbc0b2fbd928fba [file] [log] [blame]
/*
* Copyright (C) 2014 The Android Open Source Project
*
* 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.android.builder.png;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.builder.tasks.BooleanLatch;
import com.android.builder.tasks.Job;
import com.android.utils.GrabProcessOutput;
import com.android.utils.ILogger;
import com.google.common.base.Objects;
import com.google.common.base.Strings;
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* interface to the aapt long running process.
*/
public class AaptProcess {
private static final int DEFAULT_SLAVE_APPT_TIMEOUT_IN_SECONDS = 5;
private static final int SLAVE_AAPT_TIMEOUT_IN_SECONDS =
System.getenv("SLAVE_AAPT_TIMEOUT") == null
? DEFAULT_SLAVE_APPT_TIMEOUT_IN_SECONDS
: Integer.parseInt(System.getenv("SLAVE_AAPT_TIMEOUT"));
private final Process mProcess;
private final ILogger mLogger;
private final ProcessOutputFacade mProcessOutputFacade = new ProcessOutputFacade();
private final List<String> mMessages = new ArrayList<String>();
private final AtomicBoolean mReady = new AtomicBoolean(false);
private final BooleanLatch mReadyLatch = new BooleanLatch();
private final OutputStreamWriter mWriter;
private AaptProcess(@NonNull Process process, @NonNull ILogger iLogger)
throws InterruptedException {
mProcess = process;
mLogger = iLogger;
GrabProcessOutput.grabProcessOutput(process, GrabProcessOutput.Wait.ASYNC,
mProcessOutputFacade);
mWriter = new OutputStreamWriter(mProcess.getOutputStream());
}
/**
* Notifies the slave process of a new crunching request, do not block on completion, the
* notification will be issued through the job parameter's
* {@link com.android.builder.tasks.Job#finished()} or
* {@link com.android.builder.tasks.Job#error()}
* functions.
*
* @param in the source file to crunch
* @param out where to place the crunched file
* @param job the job to notify when the crunching is finished successfully or not.
* @throws IOException
*/
public void crunch(@NonNull File in, @NonNull File out, @NonNull Job<AaptProcess> job)
throws IOException {
mLogger.verbose("Process(" + mProcess.hashCode() + ")" + in.getName() +
"job: " + job.toString());
if (!mReady.get()) {
throw new RuntimeException("AAPT process not ready to receive commands");
}
NotifierProcessOutput notifier =
new NotifierProcessOutput(job, mProcessOutputFacade, mLogger);
mProcessOutputFacade.setNotifier(notifier);
mWriter.write("s\n");
mWriter.write(in.getAbsolutePath());
mWriter.write("\n");
mWriter.write(out.getAbsolutePath());
mWriter.write("\n");
mWriter.flush();
mLogger.verbose("Processed(" + mProcess.hashCode() + ")" + in.getName() +
"job: " + job.toString());
mMessages.add("Process(" + mProcess.hashCode() + ") processed " + in.getName() +
"job: " + job.toString());
}
public void waitForReady() throws InterruptedException {
if (!mReadyLatch.await(TimeUnit.NANOSECONDS.convert(
SLAVE_AAPT_TIMEOUT_IN_SECONDS, TimeUnit.SECONDS))) {
throw new RuntimeException("Timed out while waiting for slave aapt process, "
+ "try setting environment variable SLAVE_AAPT_TIMEOUT to a value bigger than "
+ SLAVE_AAPT_TIMEOUT_IN_SECONDS + " seconds");
}
mLogger.info("Slave %1$s is ready", hashCode());
}
@Override
public String toString() {
return Objects.toStringHelper(this)
.add("ready", mReady.get())
.add("process", mProcess.hashCode())
.toString();
}
/**
* Shutdowns the slave process and release all resources.
*
* @throws IOException
* @throws InterruptedException
*/
public void shutdown() throws IOException, InterruptedException {
mReady.set(false);
mWriter.write("quit\n");
mWriter.flush();
mProcess.waitFor();
mLogger.verbose("Process (%1$s) processed %2$s files", mProcess.hashCode(),
mMessages.size());
for (String message : mMessages) {
mLogger.verbose(message);
}
}
public static class Builder {
private final String mAaptLocation;
private final ILogger mLogger;
public Builder(@NonNull String aaptPath, @NonNull ILogger iLogger) {
mAaptLocation = aaptPath;
mLogger = iLogger;
}
public AaptProcess start() throws IOException, InterruptedException {
String[] command = new String[] {
mAaptLocation,
"m",
};
mLogger.verbose("Trying to start %1$s", command[0]);
Process process = new ProcessBuilder(command).start();
mLogger.verbose("Started %1$d", process.hashCode());
return new AaptProcess(process, mLogger);
}
}
private class ProcessOutputFacade implements GrabProcessOutput.IProcessOutput {
@Nullable NotifierProcessOutput notifier = null;
AtomicBoolean ready = new AtomicBoolean(false);
synchronized void setNotifier(@NonNull NotifierProcessOutput notifierProcessOutput) {
if (notifier != null) {
throw new RuntimeException("Notifier already set, threading issue");
}
notifier = notifierProcessOutput;
}
synchronized void reset() {
notifier = null;
}
@Nullable
synchronized NotifierProcessOutput getNotifier() {
return notifier;
}
@Override
public synchronized void out(@Nullable String line) {
// an empty message or aapt startup message are ignored.
if (Strings.isNullOrEmpty(line)) {
return;
}
if (line.equals("Ready")) {
AaptProcess.this.mReady.set(true);
AaptProcess.this.mReadyLatch.signal();
return;
}
NotifierProcessOutput delegate = getNotifier();
mLogger.verbose("AAPT out(%1$s): %2$s", mProcess.hashCode(), line);
if (delegate != null) {
mLogger.verbose("AAPT out(%1$s): -> %2$s", mProcess.hashCode(), delegate.mJob);
delegate.out(line);
} else {
mLogger.error(null, "AAPT out(%1$s) : No Delegate set : lost message:%2$s",
mProcess.hashCode(), line);
}
}
@Override
public synchronized void err(@Nullable String line) {
if (Strings.isNullOrEmpty(line)) {
return;
}
NotifierProcessOutput delegate = getNotifier();
if (delegate != null) {
mLogger.verbose("AAPT err(%1$s): %2$s -> %3$s", mProcess.hashCode(), line,
delegate.mJob);
delegate.err(line);
} else {
if (!mReady.get()) {
if (line.equals("ERROR: Unknown command 'm'")) {
throw new RuntimeException("Invalid aapt version, version 21 or above is required");
}
mLogger.error(null, "AAPT err(%1$s): %2$s", mProcess.hashCode(), line);
} else {
mLogger.error(null, "AAPT err(%1$s) : No Delegate set : lost message:%2$s",
mProcess.hashCode(), line);
}
}
}
Process getProcess() {
return mProcess;
}
}
private static class NotifierProcessOutput implements GrabProcessOutput.IProcessOutput {
@NonNull private final Job<AaptProcess> mJob;
@NonNull private final ProcessOutputFacade mOwner;
@NonNull private final ILogger mLogger;
NotifierProcessOutput(
@NonNull Job<AaptProcess> job,
@NonNull ProcessOutputFacade owner,
@NonNull ILogger iLogger) {
mOwner = owner;
mJob = job;
mLogger = iLogger;
}
@Override
public void out(@Nullable String line) {
if (line != null) {
mLogger.verbose("AAPT notify(%1$s): %2$s", mJob, line);
if (line.equalsIgnoreCase("Done")) {
mOwner.reset();
mJob.finished();
} else if (line.equalsIgnoreCase("Error")) {
mOwner.reset();
mJob.error();
} else {
mLogger.verbose("AAPT(%1$s) discarded: %2$s", mJob, line);
}
}
}
@Override
public void err(@Nullable String line) {
if (line != null) {
mLogger.verbose("AAPT warning(%1$s), Job(%2$s): %3$s",
mOwner.getProcess().hashCode(), mJob, line);
mLogger.warning("AAPT: %3$s",
mOwner.getProcess().hashCode(), mJob, line);
}
}
}
}