/* * 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); } } } }