blob: 3812307cca4f9dbcce570fa1560c260877b6f1b9 [file] [log] [blame]
/*
* Copyright (C) 2018 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.fastdeploy;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
import com.android.fastdeploy.PatchFormatException;
import com.android.fastdeploy.ApkArchive;
import com.android.fastdeploy.APKDump;
import com.android.fastdeploy.APKMetaData;
import com.android.fastdeploy.PatchUtils;
import com.google.protobuf.ByteString;
public final class DeployAgent {
private static final int BUFFER_SIZE = 128 * 1024;
private static final int AGENT_VERSION = 0x00000003;
public static void main(String[] args) {
int exitCode = 0;
try {
if (args.length < 1) {
showUsage(0);
}
String commandString = args[0];
switch (commandString) {
case "dump": {
if (args.length != 3) {
showUsage(1);
}
String requiredVersion = args[1];
if (AGENT_VERSION == Integer.parseInt(requiredVersion)) {
String packageName = args[2];
String packagePath = getFilenameFromPackageName(packageName);
if (packagePath != null) {
dumpApk(packageName, packagePath);
} else {
exitCode = 3;
}
} else {
System.out.printf("0x%08X\n", AGENT_VERSION);
exitCode = 4;
}
break;
}
case "apply": {
if (args.length < 3) {
showUsage(1);
}
String patchPath = args[1];
String outputParam = args[2];
InputStream deltaInputStream = null;
if (patchPath.compareTo("-") == 0) {
deltaInputStream = System.in;
} else {
deltaInputStream = new FileInputStream(patchPath);
}
if (outputParam.equals("-o")) {
OutputStream outputStream = null;
if (args.length > 3) {
String outputPath = args[3];
if (!outputPath.equals("-")) {
outputStream = new FileOutputStream(outputPath);
}
}
if (outputStream == null) {
outputStream = System.out;
}
writePatchToStream(deltaInputStream, outputStream);
} else if (outputParam.equals("-pm")) {
String[] sessionArgs = null;
if (args.length > 3) {
int numSessionArgs = args.length - 3;
sessionArgs = new String[numSessionArgs];
for (int i = 0; i < numSessionArgs; i++) {
sessionArgs[i] = args[i + 3];
}
}
exitCode = applyPatch(deltaInputStream, sessionArgs);
}
break;
}
default:
showUsage(1);
break;
}
} catch (Exception e) {
System.err.println("Error: " + e);
e.printStackTrace();
System.exit(2);
}
System.exit(exitCode);
}
private static void showUsage(int exitCode) {
System.err.println(
"usage: deployagent <command> [<args>]\n\n" +
"commands:\n" +
"dump VERSION PKGNAME dump info for an installed package given that " +
"VERSION equals current agent's version\n" +
"apply PATCHFILE [-o|-pm] apply a patch from PATCHFILE " +
"(- for stdin) to an installed package\n" +
" -o <FILE> directs output to FILE, default or - for stdout\n" +
" -pm <ARGS> directs output to package manager, passes <ARGS> to " +
"'pm install-create'\n"
);
System.exit(exitCode);
}
private static Process executeCommand(String command) throws IOException {
try {
Process p;
p = Runtime.getRuntime().exec(command);
p.waitFor();
return p;
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
private static String getFilenameFromPackageName(String packageName) throws IOException {
StringBuilder commandBuilder = new StringBuilder();
commandBuilder.append("pm list packages -f " + packageName);
Process p = executeCommand(commandBuilder.toString());
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
String packagePrefix = "package:";
String packageSuffix = "=" + packageName;
String line = "";
while ((line = reader.readLine()) != null) {
if (line.endsWith(packageSuffix)) {
int packageIndex = line.indexOf(packagePrefix);
if (packageIndex == -1) {
throw new IOException("error reading package list");
}
int equalsIndex = line.lastIndexOf(packageSuffix);
String fileName =
line.substring(packageIndex + packagePrefix.length(), equalsIndex);
return fileName;
}
}
return null;
}
private static void dumpApk(String packageName, String packagePath) throws IOException {
File apk = new File(packagePath);
ApkArchive.Dump dump = new ApkArchive(apk).extractMetadata();
APKDump.Builder apkDumpBuilder = APKDump.newBuilder();
apkDumpBuilder.setName(packageName);
if (dump.cd != null) {
apkDumpBuilder.setCd(ByteString.copyFrom(dump.cd));
}
if (dump.signature != null) {
apkDumpBuilder.setSignature(ByteString.copyFrom(dump.signature));
}
apkDumpBuilder.setAbsolutePath(apk.getAbsolutePath());
apkDumpBuilder.build().writeTo(System.out);
}
private static int createInstallSession(String[] args) throws IOException {
StringBuilder commandBuilder = new StringBuilder();
commandBuilder.append("pm install-create ");
for (int i = 0; args != null && i < args.length; i++) {
commandBuilder.append(args[i] + " ");
}
Process p = executeCommand(commandBuilder.toString());
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line = "";
String successLineStart = "Success: created install session [";
String successLineEnd = "]";
while ((line = reader.readLine()) != null) {
if (line.startsWith(successLineStart) && line.endsWith(successLineEnd)) {
return Integer.parseInt(line.substring(successLineStart.length(),
line.lastIndexOf(successLineEnd)));
}
}
return -1;
}
private static int commitInstallSession(int sessionId) throws IOException {
StringBuilder commandBuilder = new StringBuilder();
commandBuilder.append(String.format("pm install-commit %d -- - ", sessionId));
Process p = executeCommand(commandBuilder.toString());
return p.exitValue();
}
private static int applyPatch(InputStream deltaStream, String[] sessionArgs)
throws IOException, PatchFormatException {
int sessionId = createInstallSession(sessionArgs);
if (sessionId < 0) {
System.err.println("PM Create Session Failed");
return -1;
}
int writeExitCode = writePatchedDataToSession(deltaStream, sessionId);
if (writeExitCode == 0) {
return commitInstallSession(sessionId);
} else {
return -1;
}
}
private static long writePatchToStream(InputStream patchData,
OutputStream outputStream) throws IOException, PatchFormatException {
long newSize = readPatchHeader(patchData);
long bytesWritten = writePatchedDataToStream(newSize, patchData, outputStream);
outputStream.flush();
if (bytesWritten != newSize) {
throw new PatchFormatException(String.format(
"output size mismatch (expected %ld but wrote %ld)", newSize, bytesWritten));
}
return bytesWritten;
}
private static long readPatchHeader(InputStream patchData)
throws IOException, PatchFormatException {
byte[] signatureBuffer = new byte[PatchUtils.SIGNATURE.length()];
try {
PatchUtils.readFully(patchData, signatureBuffer);
} catch (IOException e) {
throw new PatchFormatException("truncated signature");
}
String signature = new String(signatureBuffer);
if (!PatchUtils.SIGNATURE.equals(signature)) {
throw new PatchFormatException("bad signature");
}
long newSize = PatchUtils.readLELong(patchData);
if (newSize < 0) {
throw new PatchFormatException("bad newSize: " + newSize);
}
return newSize;
}
// Note that this function assumes patchData has been seek'ed to the start of the delta stream
// (i.e. the signature has already been read by readPatchHeader). For a stream that points to
// the start of a patch file call writePatchToStream
private static long writePatchedDataToStream(long newSize, InputStream patchData,
OutputStream outputStream) throws IOException {
String deviceFile = PatchUtils.readString(patchData);
RandomAccessFile oldDataFile = new RandomAccessFile(deviceFile, "r");
FileChannel oldData = oldDataFile.getChannel();
WritableByteChannel newData = Channels.newChannel(outputStream);
long newDataBytesWritten = 0;
byte[] buffer = new byte[BUFFER_SIZE];
while (newDataBytesWritten < newSize) {
long newDataLen = PatchUtils.readLELong(patchData);
if (newDataLen > 0) {
PatchUtils.pipe(patchData, outputStream, buffer, newDataLen);
}
long oldDataOffset = PatchUtils.readLELong(patchData);
long oldDataLen = PatchUtils.readLELong(patchData);
if (oldDataLen >= 0) {
long offset = oldDataOffset;
long len = oldDataLen;
while (len > 0) {
long chunkLen = Math.min(len, 1024*1024*1024);
oldData.transferTo(offset, chunkLen, newData);
offset += chunkLen;
len -= chunkLen;
}
}
newDataBytesWritten += newDataLen + oldDataLen;
}
return newDataBytesWritten;
}
private static int writePatchedDataToSession(InputStream patchData, int sessionId)
throws IOException, PatchFormatException {
try {
Process p;
long newSize = readPatchHeader(patchData);
String command = String.format("pm install-write -S %d %d -- -", newSize, sessionId);
p = Runtime.getRuntime().exec(command);
OutputStream sessionOutputStream = p.getOutputStream();
long bytesWritten = writePatchedDataToStream(newSize, patchData, sessionOutputStream);
sessionOutputStream.flush();
p.waitFor();
if (bytesWritten != newSize) {
throw new PatchFormatException(
String.format("output size mismatch (expected %d but wrote %)", newSize,
bytesWritten));
}
return p.exitValue();
} catch (InterruptedException e) {
e.printStackTrace();
}
return -1;
}
}