/* * Copyright (C) 2007 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.ddmlib; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ddmlib.AdbHelper.AdbResponse; import com.android.ddmlib.FileListingService.FileEntry; import com.android.ddmlib.SyncException.SyncError; import com.android.ddmlib.utils.ArrayHelper; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.InetSocketAddress; import java.nio.channels.SocketChannel; import java.util.ArrayList; import java.util.Date; /** * Sync service class to push/pull to/from devices/emulators, through the debug bridge. * <p/> * To get a {@link SyncService} object, use {@link Device#getSyncService()}. */ public class SyncService { private static final byte[] ID_OKAY = { 'O', 'K', 'A', 'Y' }; private static final byte[] ID_FAIL = { 'F', 'A', 'I', 'L' }; private static final byte[] ID_STAT = { 'S', 'T', 'A', 'T' }; private static final byte[] ID_RECV = { 'R', 'E', 'C', 'V' }; private static final byte[] ID_DATA = { 'D', 'A', 'T', 'A' }; private static final byte[] ID_DONE = { 'D', 'O', 'N', 'E' }; private static final byte[] ID_SEND = { 'S', 'E', 'N', 'D' }; // private final static byte[] ID_LIST = { 'L', 'I', 'S', 'T' }; // private final static byte[] ID_DENT = { 'D', 'E', 'N', 'T' }; private static final NullSyncProgressMonitor sNullSyncProgressMonitor = new NullSyncProgressMonitor(); private static final int S_ISOCK = 0xC000; // type: symbolic link private static final int S_IFLNK = 0xA000; // type: symbolic link private static final int S_IFREG = 0x8000; // type: regular file private static final int S_IFBLK = 0x6000; // type: block device private static final int S_IFDIR = 0x4000; // type: directory private static final int S_IFCHR = 0x2000; // type: character device private static final int S_IFIFO = 0x1000; // type: fifo /* private final static int S_ISUID = 0x0800; // set-uid bit private final static int S_ISGID = 0x0400; // set-gid bit private final static int S_ISVTX = 0x0200; // sticky bit private final static int S_IRWXU = 0x01C0; // user permissions private final static int S_IRUSR = 0x0100; // user: read private final static int S_IWUSR = 0x0080; // user: write private final static int S_IXUSR = 0x0040; // user: execute private final static int S_IRWXG = 0x0038; // group permissions private final static int S_IRGRP = 0x0020; // group: read private final static int S_IWGRP = 0x0010; // group: write private final static int S_IXGRP = 0x0008; // group: execute private final static int S_IRWXO = 0x0007; // other permissions private final static int S_IROTH = 0x0004; // other: read private final static int S_IWOTH = 0x0002; // other: write private final static int S_IXOTH = 0x0001; // other: execute */ private static final int SYNC_DATA_MAX = 64*1024; private static final int REMOTE_PATH_MAX_LENGTH = 1024; /** * Classes which implement this interface provide methods that deal * with displaying transfer progress. */ public interface ISyncProgressMonitor { /** * Sent when the transfer starts * @param totalWork the total amount of work. */ void start(int totalWork); /** * Sent when the transfer is finished or interrupted. */ void stop(); /** * Sent to query for possible cancellation. * @return true if the transfer should be stopped. */ boolean isCanceled(); /** * Sent when a sub task is started. * @param name the name of the sub task. */ void startSubTask(String name); /** * Sent when some progress have been made. * @param work the amount of work done. */ void advance(int work); } public static class FileStat { private final int myMode; private final int mySize; private final Date myLastModified; public FileStat(int mode, int size, int lastModifiedSecs) { myMode = mode; mySize = size; myLastModified = new Date((long)(lastModifiedSecs) * 1000); } public int getMode() { return myMode; } public int getSize() { return mySize; } public Date getLastModified() { return myLastModified; } } /** * A Sync progress monitor that does nothing */ private static class NullSyncProgressMonitor implements ISyncProgressMonitor { @Override public void advance(int work) { } @Override public boolean isCanceled() { return false; } @Override public void start(int totalWork) { } @Override public void startSubTask(String name) { } @Override public void stop() { } } private InetSocketAddress mAddress; private Device mDevice; private SocketChannel mChannel; /** * Buffer used to send data. Allocated when needed and reused afterward. */ private byte[] mBuffer; /** * Creates a Sync service object. * @param address The address to connect to * @param device the {@link Device} that the service connects to. */ SyncService(InetSocketAddress address, Device device) { mAddress = address; mDevice = device; } /** * Opens the sync connection. This must be called before any calls to push[File] / pull[File]. * @return true if the connection opened, false if adb refuse the connection. This can happen * if the {@link Device} is invalid. * @throws TimeoutException in case of timeout on the connection. * @throws AdbCommandRejectedException if adb rejects the command * @throws IOException If the connection to adb failed. */ boolean openSync() throws TimeoutException, AdbCommandRejectedException, IOException { try { mChannel = SocketChannel.open(mAddress); mChannel.configureBlocking(false); // target a specific device AdbHelper.setDevice(mChannel, mDevice); byte[] request = AdbHelper.formAdbRequest("sync:"); //$NON-NLS-1$ AdbHelper.write(mChannel, request, -1, DdmPreferences.getTimeOut()); AdbResponse resp = AdbHelper.readAdbResponse(mChannel, false /* readDiagString */); if (!resp.okay) { Log.w("ddms", "Got unhappy response from ADB sync req: " + resp.message); mChannel.close(); mChannel = null; return false; } } catch (TimeoutException e) { if (mChannel != null) { try { mChannel.close(); } catch (IOException e2) { // we want to throw the original exception, so we ignore this one. } mChannel = null; } throw e; } catch (IOException e) { if (mChannel != null) { try { mChannel.close(); } catch (IOException e2) { // we want to throw the original exception, so we ignore this one. } mChannel = null; } throw e; } return true; } /** * Closes the connection. */ public void close() { if (mChannel != null) { try { mChannel.close(); } catch (IOException e) { // nothing to be done really... } mChannel = null; } } /** * Returns a sync progress monitor that does nothing. This allows background tasks that don't * want/need to display ui, to pass a valid {@link ISyncProgressMonitor}. * <p/>This object can be reused multiple times and can be used by concurrent threads. */ public static ISyncProgressMonitor getNullProgressMonitor() { return sNullSyncProgressMonitor; } /** * Pulls file(s) or folder(s). * @param entries the remote item(s) to pull * @param localPath The local destination. If the entries count is > 1 or * if the unique entry is a folder, this should be a folder. * @param monitor The progress monitor. Cannot be null. * @throws SyncException * @throws IOException * @throws TimeoutException * * @see FileListingService.FileEntry * @see #getNullProgressMonitor() */ public void pull(FileEntry[] entries, String localPath, ISyncProgressMonitor monitor) throws SyncException, IOException, TimeoutException { // first we check the destination is a directory and exists File f = new File(localPath); if (!f.exists()) { throw new SyncException(SyncError.NO_DIR_TARGET); } if (!f.isDirectory()) { throw new SyncException(SyncError.TARGET_IS_FILE); } // get a FileListingService object FileListingService fls = new FileListingService(mDevice); // compute the number of file to move int total = getTotalRemoteFileSize(entries, fls); // start the monitor monitor.start(total); doPull(entries, localPath, fls, monitor); monitor.stop(); } /** * Pulls a single file. * @param remote the remote file * @param localFilename The local destination. * @param monitor The progress monitor. Cannot be null. * * @throws IOException in case of an IO exception. * @throws TimeoutException in case of a timeout reading responses from the device. * @throws SyncException in case of a sync exception. * * @see FileListingService.FileEntry * @see #getNullProgressMonitor() */ public void pullFile(FileEntry remote, String localFilename, ISyncProgressMonitor monitor) throws IOException, SyncException, TimeoutException { int total = remote.getSizeValue(); monitor.start(total); doPullFile(remote.getFullPath(), localFilename, monitor); monitor.stop(); } /** * Pulls a single file. * <p/>Because this method just deals with a String for the remote file instead of a * {@link FileEntry}, the size of the file being pulled is unknown and the * {@link ISyncProgressMonitor} will not properly show the progress * @param remoteFilepath the full path to the remote file * @param localFilename The local destination. * @param monitor The progress monitor. Cannot be null. * * @throws IOException in case of an IO exception. * @throws TimeoutException in case of a timeout reading responses from the device. * @throws SyncException in case of a sync exception. * * @see #getNullProgressMonitor() */ public void pullFile(String remoteFilepath, String localFilename, ISyncProgressMonitor monitor) throws TimeoutException, IOException, SyncException { FileStat fileStat = statFile(remoteFilepath); if (fileStat == null) { // attempts to download anyway } else if (fileStat.getMode() == 0) { throw new SyncException(SyncError.NO_REMOTE_OBJECT); } monitor.start(0); //TODO: use the {@link FileListingService} to get the file size. doPullFile(remoteFilepath, localFilename, monitor); monitor.stop(); } /** * Push several files. * @param local An array of loca files to push * @param remote the remote {@link FileEntry} representing a directory. * @param monitor The progress monitor. Cannot be null. * @throws SyncException if file could not be pushed * @throws IOException in case of I/O error on the connection. * @throws TimeoutException in case of a timeout reading responses from the device. */ public void push(String[] local, FileEntry remote, ISyncProgressMonitor monitor) throws SyncException, IOException, TimeoutException { if (!remote.isDirectory()) { throw new SyncException(SyncError.REMOTE_IS_FILE); } // make a list of File from the list of String ArrayList<File> files = new ArrayList<File>(); for (String path : local) { files.add(new File(path)); } // get the total count of the bytes to transfer File[] fileArray = files.toArray(new File[files.size()]); int total = getTotalLocalFileSize(fileArray); monitor.start(total); doPush(fileArray, remote.getFullPath(), monitor); monitor.stop(); } /** * Push a single file. * @param local the local filepath. * @param remote The remote filepath. * @param monitor The progress monitor. Cannot be null. * * @throws SyncException if file could not be pushed * @throws IOException in case of I/O error on the connection. * @throws TimeoutException in case of a timeout reading responses from the device. */ public void pushFile(String local, String remote, ISyncProgressMonitor monitor) throws SyncException, IOException, TimeoutException { File f = new File(local); if (!f.exists()) { throw new SyncException(SyncError.NO_LOCAL_FILE); } if (f.isDirectory()) { throw new SyncException(SyncError.LOCAL_IS_DIRECTORY); } monitor.start((int)f.length()); doPushFile(local, remote, monitor); monitor.stop(); } /** * compute the recursive file size of all the files in the list. Folder * have a weight of 1. * @param entries * @param fls * @return */ private int getTotalRemoteFileSize(FileEntry[] entries, FileListingService fls) { int count = 0; for (FileEntry e : entries) { int type = e.getType(); if (type == FileListingService.TYPE_DIRECTORY) { // get the children FileEntry[] children = fls.getChildren(e, false, null); count += getTotalRemoteFileSize(children, fls) + 1; } else if (type == FileListingService.TYPE_FILE) { count += e.getSizeValue(); } } return count; } /** * compute the recursive file size of all the files in the list. Folder * have a weight of 1. * This does not check for circular links. * @param files * @return */ private int getTotalLocalFileSize(File[] files) { int count = 0; for (File f : files) { if (f.exists()) { if (f.isDirectory()) { return getTotalLocalFileSize(f.listFiles()) + 1; } else if (f.isFile()) { count += f.length(); } } } return count; } /** * Pulls multiple files/folders recursively. * @param entries The list of entry to pull * @param localPath the localpath to a directory * @param fileListingService a FileListingService object to browse through remote directories. * @param monitor the progress monitor. Must be started already. * * @throws SyncException if file could not be pushed * @throws IOException in case of I/O error on the connection. * @throws TimeoutException in case of a timeout reading responses from the device. */ private void doPull(FileEntry[] entries, String localPath, FileListingService fileListingService, ISyncProgressMonitor monitor) throws SyncException, IOException, TimeoutException { for (FileEntry e : entries) { // check if we're cancelled if (monitor.isCanceled()) { throw new SyncException(SyncError.CANCELED); } // get type (we only pull directory and files for now) int type = e.getType(); if (type == FileListingService.TYPE_DIRECTORY) { monitor.startSubTask(e.getFullPath()); String dest = localPath + File.separator + e.getName(); // make the directory File d = new File(dest); d.mkdir(); // then recursively call the content. Since we did a ls command // to get the number of files, we can use the cache FileEntry[] children = fileListingService.getChildren(e, true, null); doPull(children, dest, fileListingService, monitor); monitor.advance(1); } else if (type == FileListingService.TYPE_FILE) { monitor.startSubTask(e.getFullPath()); String dest = localPath + File.separator + e.getName(); doPullFile(e.getFullPath(), dest, monitor); } } } /** * Pulls a remote file * @param remotePath the remote file (length max is 1024) * @param localPath the local destination * @param monitor the monitor. The monitor must be started already. * @throws SyncException if file could not be pushed * @throws IOException in case of I/O error on the connection. * @throws TimeoutException in case of a timeout reading responses from the device. */ private void doPullFile(String remotePath, String localPath, ISyncProgressMonitor monitor) throws IOException, SyncException, TimeoutException { byte[] msg = null; byte[] pullResult = new byte[8]; final int timeOut = DdmPreferences.getTimeOut(); try { byte[] remotePathContent = remotePath.getBytes(AdbHelper.DEFAULT_ENCODING); if (remotePathContent.length > REMOTE_PATH_MAX_LENGTH) { throw new SyncException(SyncError.REMOTE_PATH_LENGTH); } // create the full request message msg = createFileReq(ID_RECV, remotePathContent); // and send it. AdbHelper.write(mChannel, msg, -1, timeOut); // read the result, in a byte array containing 2 ints // (id, size) AdbHelper.read(mChannel, pullResult, -1, timeOut); // check we have the proper data back if (!checkResult(pullResult, ID_DATA) && !checkResult(pullResult, ID_DONE)) { throw new SyncException(SyncError.TRANSFER_PROTOCOL_ERROR, readErrorMessage(pullResult, timeOut)); } } catch (UnsupportedEncodingException e) { throw new SyncException(SyncError.REMOTE_PATH_ENCODING, e); } // access the destination file File f = new File(localPath); // create the stream to write in the file. We use a new try/catch block to differentiate // between file and network io exceptions. FileOutputStream fos = null; try { fos = new FileOutputStream(f); // the buffer to read the data byte[] data = new byte[SYNC_DATA_MAX]; // loop to get data until we're done. while (true) { // check if we're cancelled if (monitor.isCanceled()) { throw new SyncException(SyncError.CANCELED); } // if we're done, we stop the loop if (checkResult(pullResult, ID_DONE)) { break; } if (!checkResult(pullResult, ID_DATA)) { // hmm there's an error throw new SyncException(SyncError.TRANSFER_PROTOCOL_ERROR, readErrorMessage(pullResult, timeOut)); } int length = ArrayHelper.swap32bitFromArray(pullResult, 4); if (length > SYNC_DATA_MAX) { // buffer overrun! // error and exit throw new SyncException(SyncError.BUFFER_OVERRUN); } // now read the length we received AdbHelper.read(mChannel, data, length, timeOut); // get the header for the next packet. AdbHelper.read(mChannel, pullResult, -1, timeOut); // write the content in the file fos.write(data, 0, length); monitor.advance(length); } fos.flush(); } catch (IOException e) { Log.e("ddms", String.format("Failed to open local file %s for writing, Reason: %s", f.getAbsolutePath(), e.toString())); throw new SyncException(SyncError.FILE_WRITE_ERROR); } finally { if (fos != null) { fos.close(); } } } /** * Push multiple files * @param fileArray * @param remotePath * @param monitor * * @throws SyncException if file could not be pushed * @throws IOException in case of I/O error on the connection. * @throws TimeoutException in case of a timeout reading responses from the device. */ private void doPush(File[] fileArray, String remotePath, ISyncProgressMonitor monitor) throws SyncException, IOException, TimeoutException { for (File f : fileArray) { // check if we're canceled if (monitor.isCanceled()) { throw new SyncException(SyncError.CANCELED); } if (f.exists()) { if (f.isDirectory()) { // append the name of the directory to the remote path String dest = remotePath + "/" + f.getName(); // $NON-NLS-1S monitor.startSubTask(dest); doPush(f.listFiles(), dest, monitor); monitor.advance(1); } else if (f.isFile()) { // append the name of the file to the remote path String remoteFile = remotePath + "/" + f.getName(); // $NON-NLS-1S monitor.startSubTask(remoteFile); doPushFile(f.getAbsolutePath(), remoteFile, monitor); } } } } /** * Push a single file * @param localPath the local file to push * @param remotePath the remote file (length max is 1024) * @param monitor the monitor. The monitor must be started already. * * @throws SyncException if file could not be pushed * @throws IOException in case of I/O error on the connection. * @throws TimeoutException in case of a timeout reading responses from the device. */ private void doPushFile(String localPath, String remotePath, ISyncProgressMonitor monitor) throws SyncException, IOException, TimeoutException { FileInputStream fis = null; byte[] msg; final int timeOut = DdmPreferences.getTimeOut(); File f = new File(localPath); try { byte[] remotePathContent = remotePath.getBytes(AdbHelper.DEFAULT_ENCODING); if (remotePathContent.length > REMOTE_PATH_MAX_LENGTH) { throw new SyncException(SyncError.REMOTE_PATH_LENGTH); } // create the stream to read the file fis = new FileInputStream(f); // create the header for the action msg = createSendFileReq(ID_SEND, remotePathContent, 0644); // and send it. We use a custom try/catch block to make the difference between // file and network IO exceptions. AdbHelper.write(mChannel, msg, -1, timeOut); System.arraycopy(ID_DATA, 0, getBuffer(), 0, ID_DATA.length); // look while there is something to read while (true) { // check if we're canceled if (monitor.isCanceled()) { throw new SyncException(SyncError.CANCELED); } // read up to SYNC_DATA_MAX int readCount = fis.read(getBuffer(), 8, SYNC_DATA_MAX); if (readCount == -1) { // we reached the end of the file break; } // now send the data to the device // first write the amount read ArrayHelper.swap32bitsToArray(readCount, getBuffer(), 4); // now write it AdbHelper.write(mChannel, getBuffer(), readCount+8, timeOut); // and advance the monitor monitor.advance(readCount); } } catch (UnsupportedEncodingException e) { throw new SyncException(SyncError.REMOTE_PATH_ENCODING, e); } finally { // close the local file if (fis != null) { fis.close(); } } // create the DONE message long time = f.lastModified() / 1000; msg = createReq(ID_DONE, (int)time); // and send it. AdbHelper.write(mChannel, msg, -1, timeOut); // read the result, in a byte array containing 2 ints // (id, size) byte[] result = new byte[8]; AdbHelper.read(mChannel, result, -1 /* full length */, timeOut); if (!checkResult(result, ID_OKAY)) { throw new SyncException(SyncError.TRANSFER_PROTOCOL_ERROR, readErrorMessage(result, timeOut)); } } /** * Reads an error message from the opened {@link #mChannel}. * @param result the current adb result. Must contain both FAIL and the length of the message. * @param timeOut * @return * @throws TimeoutException in case of a timeout reading responses from the device. * @throws IOException */ private String readErrorMessage(byte[] result, final int timeOut) throws TimeoutException, IOException { if (checkResult(result, ID_FAIL)) { int len = ArrayHelper.swap32bitFromArray(result, 4); if (len > 0) { AdbHelper.read(mChannel, getBuffer(), len, timeOut); String message = new String(getBuffer(), 0, len); Log.e("ddms", "transfer error: " + message); return message; } } return null; } /** * Returns the stat info of the remote file. * @param path the remote file * @return an FileStat containing the mode, size and last modified info if all went well or null * otherwise * @throws IOException * @throws TimeoutException in case of a timeout reading responses from the device. */ @Nullable public FileStat statFile(@NonNull String path) throws TimeoutException, IOException { // create the stat request message. byte[] msg = createFileReq(ID_STAT, path); AdbHelper.write(mChannel, msg, -1 /* full length */, DdmPreferences.getTimeOut()); // read the result, in a byte array containing 4 ints // (id, mode, size, time) byte[] statResult = new byte[16]; AdbHelper.read(mChannel, statResult, -1 /* full length */, DdmPreferences.getTimeOut()); // check we have the proper data back if (!checkResult(statResult, ID_STAT)) { return null; } final int mode = ArrayHelper.swap32bitFromArray(statResult, 4); final int size = ArrayHelper.swap32bitFromArray(statResult, 8); final int lastModifiedSecs = ArrayHelper.swap32bitFromArray(statResult, 12); return new FileStat(mode, size, lastModifiedSecs); } /** * Create a command with a code and an int values * @param command * @param value * @return */ private static byte[] createReq(byte[] command, int value) { byte[] array = new byte[8]; System.arraycopy(command, 0, array, 0, 4); ArrayHelper.swap32bitsToArray(value, array, 4); return array; } /** * Creates the data array for a stat request. * @param command the 4 byte command (ID_STAT, ID_RECV, ...) * @param path The path of the remote file on which to execute the command * @return the byte[] to send to the device through adb */ private static byte[] createFileReq(byte[] command, String path) { byte[] pathContent = null; try { pathContent = path.getBytes(AdbHelper.DEFAULT_ENCODING); } catch (UnsupportedEncodingException e) { return null; } return createFileReq(command, pathContent); } /** * Creates the data array for a file request. This creates an array with a 4 byte command + the * remote file name. * @param command the 4 byte command (ID_STAT, ID_RECV, ...). * @param path The path, as a byte array, of the remote file on which to * execute the command. * @return the byte[] to send to the device through adb */ private static byte[] createFileReq(byte[] command, byte[] path) { byte[] array = new byte[8 + path.length]; System.arraycopy(command, 0, array, 0, 4); ArrayHelper.swap32bitsToArray(path.length, array, 4); System.arraycopy(path, 0, array, 8, path.length); return array; } private static byte[] createSendFileReq(byte[] command, byte[] path, int mode) { // make the mode into a string String modeStr = "," + (mode & 0777); // $NON-NLS-1S byte[] modeContent = null; try { modeContent = modeStr.getBytes(AdbHelper.DEFAULT_ENCODING); } catch (UnsupportedEncodingException e) { return null; } byte[] array = new byte[8 + path.length + modeContent.length]; System.arraycopy(command, 0, array, 0, 4); ArrayHelper.swap32bitsToArray(path.length + modeContent.length, array, 4); System.arraycopy(path, 0, array, 8, path.length); System.arraycopy(modeContent, 0, array, 8 + path.length, modeContent.length); return array; } /** * Checks the result array starts with the provided code * @param result The result array to check * @param code The 4 byte code. * @return true if the code matches. */ private static boolean checkResult(byte[] result, byte[] code) { return !(result[0] != code[0] || result[1] != code[1] || result[2] != code[2] || result[3] != code[3]); } private static int getFileType(int mode) { if ((mode & S_ISOCK) == S_ISOCK) { return FileListingService.TYPE_SOCKET; } if ((mode & S_IFLNK) == S_IFLNK) { return FileListingService.TYPE_LINK; } if ((mode & S_IFREG) == S_IFREG) { return FileListingService.TYPE_FILE; } if ((mode & S_IFBLK) == S_IFBLK) { return FileListingService.TYPE_BLOCK; } if ((mode & S_IFDIR) == S_IFDIR) { return FileListingService.TYPE_DIRECTORY; } if ((mode & S_IFCHR) == S_IFCHR) { return FileListingService.TYPE_CHARACTER; } if ((mode & S_IFIFO) == S_IFIFO) { return FileListingService.TYPE_FIFO; } return FileListingService.TYPE_OTHER; } /** * Retrieve the buffer, allocating if necessary * @return */ private byte[] getBuffer() { if (mBuffer == null) { // create the buffer used to read. // we read max SYNC_DATA_MAX, but we need 2 4 bytes at the beginning. mBuffer = new byte[SYNC_DATA_MAX + 8]; } return mBuffer; } }