From: hyunsik.noh Date: Thu, 22 Nov 2012 07:26:32 +0000 (+0900) Subject: [Title] sdblib merged into common-eplugin.git from sdblib.git X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=8a70fbf08c8ffdc0927c36b073ad39e35ea02ad2;p=sdk%2Fide%2Fcommon-eplugin.git [Title] sdblib merged into common-eplugin.git from sdblib.git [Type] [Module]common [Priority] [CQ#] [Redmine#] [Problem] [Cause] [Solution] [TestCase] Change-Id: I0a6c77c760bee0c9fdba915326818017a09321b7 --- diff --git a/.gitignore b/.gitignore index 9e3955f..e5fa4bd 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,8 @@ bin *.class common-eplugin_*.tar.gz common-eplugin_*_*.zip -common-eplugin.package.linux -common-eplugin.package.windows +sdblib_*_*.zip +sdblib.package.* +common-eplugin.package.* #log files .log diff --git a/org.tizen.common.connection/META-INF/MANIFEST.MF b/org.tizen.common.connection/META-INF/MANIFEST.MF index f3a3e07..71ab8a0 100644 --- a/org.tizen.common.connection/META-INF/MANIFEST.MF +++ b/org.tizen.common.connection/META-INF/MANIFEST.MF @@ -12,7 +12,8 @@ Require-Bundle: org.eclipse.ui, Bundle-RequiredExecutionEnvironment: JavaSE-1.6 Bundle-ActivationPolicy: lazy Import-Package: org.eclipse.ui.console, - org.tizen.common.util + org.tizen.common.util, + org.tizen.sdblib Bundle-ClassPath: . Export-Package: org.tizen.common.connection; uses:="org.eclipse.jface.resource, diff --git a/org.tizen.common.feature/feature.xml b/org.tizen.common.feature/feature.xml index 19475de..1113880 100644 --- a/org.tizen.common.feature/feature.xml +++ b/org.tizen.common.feature/feature.xml @@ -63,4 +63,11 @@ BY CLICKING THE "I AGREE" BUTTON OR BY USING ANY PART OF TIZEN SDK, YO version="0.0.0" unpack="false"/> + + diff --git a/org.tizen.common.sdblib/.classpath b/org.tizen.common.sdblib/.classpath new file mode 100644 index 0000000..ad32c83 --- /dev/null +++ b/org.tizen.common.sdblib/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/org.tizen.common.sdblib/.project b/org.tizen.common.sdblib/.project new file mode 100644 index 0000000..20024d8 --- /dev/null +++ b/org.tizen.common.sdblib/.project @@ -0,0 +1,28 @@ + + + org.tizen.common.sdblib + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/org.tizen.common.sdblib/.settings/org.eclipse.jdt.core.prefs b/org.tizen.common.sdblib/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..db78d86 --- /dev/null +++ b/org.tizen.common.sdblib/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,8 @@ +#Thu Nov 22 14:29:16 KST 2012 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/org.tizen.common.sdblib/META-INF/MANIFEST.MF b/org.tizen.common.sdblib/META-INF/MANIFEST.MF new file mode 100644 index 0000000..d0e3a0b --- /dev/null +++ b/org.tizen.common.sdblib/META-INF/MANIFEST.MF @@ -0,0 +1,7 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Sdblib +Bundle-SymbolicName: org.tizen.common.sdblib +Bundle-Version: 1.0.0.qualifier +Bundle-RequiredExecutionEnvironment: JavaSE-1.6 +Export-Package: org.tizen.sdblib diff --git a/org.tizen.common.sdblib/build.properties b/org.tizen.common.sdblib/build.properties new file mode 100644 index 0000000..34d2e4d --- /dev/null +++ b/org.tizen.common.sdblib/build.properties @@ -0,0 +1,4 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + . diff --git a/org.tizen.common.sdblib/src/org/tizen/sdblib/ArrayHelper.java b/org.tizen.common.sdblib/src/org/tizen/sdblib/ArrayHelper.java new file mode 100644 index 0000000..1c60c08 --- /dev/null +++ b/org.tizen.common.sdblib/src/org/tizen/sdblib/ArrayHelper.java @@ -0,0 +1,90 @@ +/* + * 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 org.tizen.sdblib; + +/** + * Utility class providing array to int/long conversion for data received from devices through sdb. + */ +public final class ArrayHelper { + + /** + * Swaps an unsigned value around, and puts the result in an array that can be sent to a device. + * @param value The value to swap. + * @param dest the destination array + * @param offset the offset in the array where to put the swapped value. + * Array length must be at least offset + 4 + */ + public static void swap32bitsToArray(int value, byte[] dest, int offset) { + dest[offset] = (byte)(value & 0x000000FF); + dest[offset + 1] = (byte)((value & 0x0000FF00) >> 8); + dest[offset + 2] = (byte)((value & 0x00FF0000) >> 16); + dest[offset + 3] = (byte)((value & 0xFF000000) >> 24); + } + + /** + * Reads a signed 32 bit integer from an array coming from a device. + * @param value the array containing the int + * @param offset the offset in the array at which the int starts + * @return the integer read from the array + */ + public static int swap32bitFromArray(byte[] value, int offset) { + int v = 0; + v |= ((int)value[offset]) & 0x000000FF; + v |= (((int)value[offset + 1]) & 0x000000FF) << 8; + v |= (((int)value[offset + 2]) & 0x000000FF) << 16; + v |= (((int)value[offset + 3]) & 0x000000FF) << 24; + + return v; + } + + /** + * Reads an unsigned 16 bit integer from an array coming from a device, + * and returns it as an 'int' + * @param value the array containing the 16 bit int (2 byte). + * @param offset the offset in the array at which the int starts + * Array length must be at least offset + 2 + * @return the integer read from the array. + */ + public static int swapU16bitFromArray(byte[] value, int offset) { + int v = 0; + v |= ((int)value[offset]) & 0x000000FF; + v |= (((int)value[offset + 1]) & 0x000000FF) << 8; + + return v; + } + + /** + * Reads a signed 64 bit integer from an array coming from a device. + * @param value the array containing the int + * @param offset the offset in the array at which the int starts + * Array length must be at least offset + 8 + * @return the integer read from the array + */ + public static long swap64bitFromArray(byte[] value, int offset) { + long v = 0; + v |= ((long)value[offset]) & 0x00000000000000FFL; + v |= (((long)value[offset + 1]) & 0x00000000000000FFL) << 8; + v |= (((long)value[offset + 2]) & 0x00000000000000FFL) << 16; + v |= (((long)value[offset + 3]) & 0x00000000000000FFL) << 24; + v |= (((long)value[offset + 4]) & 0x00000000000000FFL) << 32; + v |= (((long)value[offset + 5]) & 0x00000000000000FFL) << 40; + v |= (((long)value[offset + 6]) & 0x00000000000000FFL) << 48; + v |= (((long)value[offset + 7]) & 0x00000000000000FFL) << 56; + + return v; + } +} diff --git a/org.tizen.common.sdblib/src/org/tizen/sdblib/Device.java b/org.tizen.common.sdblib/src/org/tizen/sdblib/Device.java new file mode 100644 index 0000000..e2b9aa8 --- /dev/null +++ b/org.tizen.common.sdblib/src/org/tizen/sdblib/Device.java @@ -0,0 +1,264 @@ +/* + * 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 org.tizen.sdblib; + +import java.io.File; +import java.io.IOException; + +import org.tizen.sdblib.SyncService.SyncResult; + + + +/** + * A Device. It can be a physical device or an emulator. + */ +final class Device implements IDevice { + + private final static int INSTALL_TIMEOUT = 2*60*1000; //2min + + /** Emulator Serial Number regexp. */ + final static String RE_EMULATOR_SN = "emulator-(\\d+)"; //$NON-NLS-1$ + + final static String COMMAND_ARCHITECTURE_INFO = "uname -m"; + + /** Serial number of the device */ + private String mSerialNumber = null; + + private Arch mArch = Arch.ARM; + + private String mDeviceName = null; + + /** State of the device. */ + private DeviceState mState = null; + + private DeviceMonitor mMonitor; + + private static final String LOG_TAG = "Device"; + + private SdbShellProcess sdbShellProcess = null; + + public String getSerialNumber() { + return mSerialNumber; + } + + @Override + public String getDeviceName() { + return mDeviceName; + } + + public DeviceState getState() { + return mState; + } + + /** + * Changes the state of the device. + */ + void setState(DeviceState state) { + mState = state; + } + + @Override + public String toString() { + return mSerialNumber; + } + + public boolean isOnline() { + return mState == DeviceState.ONLINE; + } + + public boolean isEmulator() { + return mSerialNumber.matches(RE_EMULATOR_SN); + } + + public boolean isOffline() { + return mState == DeviceState.OFFLINE; + } + + public SyncService getSyncService() + throws TimeoutException, SdbCommandRejectedException, IOException { + SyncService syncService = new SyncService(SmartDevelopmentBridge.getSocketAddress(), this); + if (syncService.openSync()) { + return syncService; + } + + return null; + } + + public FileListingService getFileListingService() { + return new FileListingService(this); + } + + public SdbShellProcess executeShellCommand(String command) throws IOException { + SmartDevelopmentBridge sdb = SmartDevelopmentBridge.getBridge(); + if (sdb == null) + return null; + String sdbPath = sdb.getSdbOsLocation(); + if (sdbPath == null) + return null; + if (mSerialNumber == null) + return null; + + String sdbShellCmd = sdbPath + " -s " + mSerialNumber + " shell"; + Process pSdb = Runtime.getRuntime().exec(sdbShellCmd); + sdbShellProcess = new SdbShellProcess(pSdb, command); + return sdbShellProcess; + } + + public void executeShellCommand(String command, IShellOutputReceiver receiver) + throws TimeoutException, SdbCommandRejectedException, ShellCommandUnresponsiveException, + IOException { + SdbHelper.executeRemoteCommand(SmartDevelopmentBridge.getSocketAddress(), command, this, + receiver, SdbPreferences.getTimeOut()); + } + + public void executeShellCommand(String command, IShellOutputReceiver receiver, + int maxTimeToOutputResponse) + throws TimeoutException, SdbCommandRejectedException, ShellCommandUnresponsiveException, + IOException { + SdbHelper.executeRemoteCommand(SmartDevelopmentBridge.getSocketAddress(), command, this, + receiver, maxTimeToOutputResponse); + } + + public void runEventLogService(LogReceiver receiver) + throws TimeoutException, SdbCommandRejectedException, IOException { + SdbHelper.runEventLogService(SmartDevelopmentBridge.getSocketAddress(), this, receiver); + } + + public void runLogService(String logname, LogReceiver receiver) + throws TimeoutException, SdbCommandRejectedException, IOException { + SdbHelper.runLogService(SmartDevelopmentBridge.getSocketAddress(), this, logname, receiver); + } + + public void createForward(int localPort, int remotePort) + throws TimeoutException, SdbCommandRejectedException, IOException { + SdbHelper.createForward(SmartDevelopmentBridge.getSocketAddress(), this, localPort, remotePort); + } + + public void removeForward(int localPort, int remotePort) + throws TimeoutException, SdbCommandRejectedException, IOException { + SdbHelper.removeForward(SmartDevelopmentBridge.getSocketAddress(), this, localPort, remotePort); + } + + Device(DeviceMonitor monitor, String serialNumber, DeviceState deviceState) { + mMonitor = monitor; + mSerialNumber = serialNumber; + mState = deviceState; + } + + Device(DeviceMonitor monitor, String serialNumber, DeviceState deviceState, String deviceName) { + mMonitor = monitor; + mSerialNumber = serialNumber; + mState = deviceState; + this.mDeviceName = deviceName; + } + + DeviceMonitor getMonitor() { + return mMonitor; + } + + void update(int changeMask) { + mMonitor.getServer().deviceChanged(this, changeMask); + } + + public String syncPackageToDevice(String localFilePath) + throws IOException, SdbCommandRejectedException, TimeoutException { + try { + String packageFileName = getFileName(localFilePath); + String remoteFilePath = String.format("/opt/apps/tmp/%1$s", packageFileName); //$NON-NLS-1$ + + Log.d(packageFileName, String.format("Uploading %1$s onto device '%2$s'", + packageFileName, getSerialNumber())); + + SyncService sync = getSyncService(); + if (sync != null) { + String message = String.format("Uploading file onto device '%1$s'", + getSerialNumber()); + Log.d(LOG_TAG, message); + SyncResult result = sync.pushFile(localFilePath, remoteFilePath, + SyncService.getNullProgressMonitor()); + + if (result.getCode() != SyncService.RESULT_OK) { + throw new IOException(String.format("Unable to upload file: %1$s", + result.getMessage())); + } + } else { + throw new IOException("Unable to open sync connection!"); + } + return remoteFilePath; + } catch (TimeoutException e) { + Log.e(LOG_TAG, "Unable to open sync connection! Timeout."); + throw e; + } catch (IOException e) { + Log.e(LOG_TAG, String.format("Unable to open sync connection! reason: %1$s", + e.getMessage())); + throw e; + } + } + + /** + * Helper method to retrieve the file name given a local file path + * @param filePath full directory path to file + * @return {@link String} file name + */ + private String getFileName(String filePath) { + return new File(filePath).getName(); + } + + /** + * {@inheritDoc} + */ + public void removeRemotePackage(String remoteFilePath) + throws TimeoutException, SdbCommandRejectedException, ShellCommandUnresponsiveException, + IOException { + // now we delete the app we sync'ed + try { + executeShellCommand("rm " + remoteFilePath, new NullOutputReceiver(), INSTALL_TIMEOUT); + } catch (IOException e) { + Log.e(LOG_TAG, String.format("Failed to delete temporary package: %1$s", + e.getMessage())); + throw e; + } + } + + public Arch getArch() + { + return mArch; + } + + public void checkArchitecture() + { + try + { + executeShellCommand(COMMAND_ARCHITECTURE_INFO, new ArchInfoReceiver()); + } catch (Exception e) + { + // sdb failed somehow, we do nothing. + } + } + + class ArchInfoReceiver extends MultiLineReceiver + { + @Override + public void processNewLines(String[] lines) + { + if(lines[0] != null && !lines[0].contains(Arch.ARM.getArch())) + { + mArch = Arch.X86; + } + } + } +} diff --git a/org.tizen.common.sdblib/src/org/tizen/sdblib/DeviceMonitor.java b/org.tizen.common.sdblib/src/org/tizen/sdblib/DeviceMonitor.java new file mode 100644 index 0000000..bcc82cf --- /dev/null +++ b/org.tizen.common.sdblib/src/org/tizen/sdblib/DeviceMonitor.java @@ -0,0 +1,415 @@ +/* + * 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 org.tizen.sdblib; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousCloseException; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; + +import org.tizen.sdblib.Device.ArchInfoReceiver; +import org.tizen.sdblib.IDevice.DeviceState; +import org.tizen.sdblib.SdbHelper.SdbResponse; + + +/** + * A Device monitor. This connects to the Smart Development Bridge and get device. + */ +final class DeviceMonitor { + private byte[] mLengthBuffer = new byte[4]; + + private boolean mQuit = false; + + private SmartDevelopmentBridge mServer; + + private SocketChannel mMainSdbConnection = null; + private boolean mMonitoring = false; + private int mConnectionAttempt = 0; + private int mRestartAttemptCount = 0; + private boolean mInitialDeviceListDone = false; + + private Selector mSelector; + ArchInfoReceiver receiver = null; + private final ArrayList mDevices = new ArrayList(); + + /** + * Creates a new {@link DeviceMonitor} object and links it to the running + * {@link SmartDevelopmentBridge} object. + * @param server the running {@link SmartDevelopmentBridge}. + */ + DeviceMonitor(SmartDevelopmentBridge server) { + mServer = server; + } + + /** + * Starts the monitoring. + */ + void start() { + new Thread("Device List Monitor") { //$NON-NLS-1$ + @Override + public void run() { + deviceMonitorLoop(); + } + }.start(); + } + + /** + * Stops the monitoring. + */ + void stop() { + mQuit = true; + + // wake up the main loop thread by closing the main connection to sdb. + try { + if (mMainSdbConnection != null) { + mMainSdbConnection.close(); + } + } catch (IOException e1) { + } + + // wake up the secondary loop by closing the selector. + if (mSelector != null) { + mSelector.wakeup(); + } + } + + + + /** + * Returns if the monitor is currently connected to the debug bridge server. + * @return + */ + boolean isMonitoring() { + return mMonitoring; + } + + int getConnectionAttemptCount() { + return mConnectionAttempt; + } + + int getRestartAttemptCount() { + return mRestartAttemptCount; + } + + /** + * Returns the devices. + */ + Device[] getDevices() { + synchronized (mDevices) { + return mDevices.toArray(new Device[mDevices.size()]); + } + } + + boolean hasInitialDeviceList() { + return mInitialDeviceListDone; + } + + SmartDevelopmentBridge getServer() { + return mServer; + } + + /** + * Monitors the devices. This connects to the Debug Bridge + */ + private void deviceMonitorLoop() { + do { + try { + if (mMainSdbConnection == null) { + Log.d("DeviceMonitor", "Opening sdb connection"); + mMainSdbConnection = openSdbConnection(); + if (mMainSdbConnection == null) { + if (mServer.startSdb() == false) { + mRestartAttemptCount++; + Log.e("DeviceMonitor", + "sdb restart attempts: " + mRestartAttemptCount); + } + waitABit(); + } else { + Log.d("DeviceMonitor", "Connected to sdb for device monitoring"); + } + } + + if (mMainSdbConnection != null && mMonitoring == false) { + mMonitoring = sendDeviceListMonitoringRequest(); + } + + if (mMonitoring) { + // read the length of the incoming message + int length = readLength(mMainSdbConnection, mLengthBuffer); + + if (length >= 0) { + // read the incoming message + processIncomingDeviceData(length); + + // flag the fact that we have build the list at least once. + mInitialDeviceListDone = true; + } + } + } catch (AsynchronousCloseException ace) { + // this happens because of a call to Quit. We do nothing, and the loop will break. + } catch (TimeoutException ioe) { + handleExpectioninMonitorLoop(ioe); + } catch (IOException ioe) { + handleExpectioninMonitorLoop(ioe); + } + } while (mQuit == false); + } + + private void handleExpectioninMonitorLoop(Exception e) { + if (mQuit == false) { + if (e instanceof TimeoutException) { + Log.e("DeviceMonitor", "Sdb connection Error: timeout"); + } else { + Log.e("DeviceMonitor", "Sdb connection Error:" + e.getMessage()); + } + mMonitoring = false; + if (mMainSdbConnection != null) { + try { + mMainSdbConnection.close(); + } catch (IOException ioe) { + // we can safely ignore that one. + } + mMainSdbConnection = null; + } + } + } + + /** + * Sleeps for a little bit. + */ + private void waitABit() { + try { + Thread.sleep(1000); + } catch (InterruptedException e1) { + } + } + + /** + * Attempts to connect to the debug bridge server. + * @return a connect socket if success, null otherwise + */ + private SocketChannel openSdbConnection() { + Log.d("DeviceMonitor", "Connecting to sdb for Device List Monitoring..."); + + SocketChannel sdbChannel = null; + try { + sdbChannel = SocketChannel.open(SmartDevelopmentBridge.getSocketAddress()); + sdbChannel.socket().setTcpNoDelay(true); + } catch (IOException e) { + } + + return sdbChannel; + } + + /** + * + * @return + * @throws IOException + */ + private boolean sendDeviceListMonitoringRequest() throws TimeoutException, IOException { + byte[] request = SdbHelper.formSdbRequest("host:track-devices"); //$NON-NLS-1$ + + try { + SdbHelper.write(mMainSdbConnection, request); + + SdbResponse resp = SdbHelper.readSdbResponse(mMainSdbConnection, + false /* readDiagString */); + + if (resp.okay == false) { + // request was refused by sdb! + Log.e("DeviceMonitor", "sdb refused request: " + resp.message); + } + + return resp.okay; + } catch (IOException e) { + Log.e("DeviceMonitor", "Sending Tracking request failed!"); + mMainSdbConnection.close(); + throw e; + } + } + + /** + * Processes an incoming device message from the socket + * @param socket + * @param length + * @throws IOException + */ + private void processIncomingDeviceData(int length) throws IOException { + ArrayList list = new ArrayList(); + + if (length > 0) { + byte[] buffer = new byte[length]; + String result = read(mMainSdbConnection, buffer); + + String[] devices = result.split("\n"); // $NON-NLS-1$ + + for (String d : devices) { + String[] param = d.split("\t"); // $NON-NLS-1$ + if (param.length == 3) { + // new sdb uses only serial numbers to identify devices + Device device = new Device(this, param[0].trim() /*serialnumber*/, + DeviceState.getState(param[1].trim()), + param[2].trim() /*device name*/); + + //add the device to the list + list.add(device); + } + } + } + + // now merge the new devices with the old ones. + updateDevices(list); + } + + /** + * Updates the device list with the new items received from the monitoring service. + */ + private void updateDevices(ArrayList newList) { + // because we are going to call mServer.deviceDisconnected which will acquire this lock + // we lock it first, so that the SmartDevelopmentBridge lock is always locked first. + synchronized (SmartDevelopmentBridge.getLock()) { + // array to store the devices that must be queried for information. + // it's important to not do it inside the synchronized loop as this could block + // the whole workspace (this lock is acquired during build too). + ArrayList devicesToQuery = new ArrayList(); + synchronized (mDevices) { + // For each device in the current list, we look for a matching the new list. + // * if we find it, we update the current object with whatever new information + // there is + // (mostly state change, if the device becomes ready, we query for build info). + // We also remove the device from the new list to mark it as "processed" + // * if we do not find it, we remove it from the current list. + // Once this is done, the new list contains device we aren't monitoring yet, so we + // add them to the list, and start monitoring them. + for (int d = 0 ; d < mDevices.size() ;) { + Device device = mDevices.get(d); + // look for a similar device in the new list. + int count = newList.size(); + boolean foundMatch = false; + for (int dd = 0 ; dd < count ; dd++) { + Device newDevice = newList.get(dd); + // see if it matches in id and serial number. + if (newDevice.getSerialNumber().equals(device.getSerialNumber())) { + foundMatch = true; + + // update the state if needed. + if (device.getState() != newDevice.getState()) { + device.setState(newDevice.getState()); + device.update(Device.CHANGE_STATE); + //should to check architecture if device is an emulator and state changed from "off-lined" to "on-lined" + if(device.isEmulator() && device.getState() == DeviceState.ONLINE) + { + device.checkArchitecture(); + } + } + // remove the new device from the list since it's been used + newList.remove(dd); + break; + } + } + + if (foundMatch == false) { + // the device is gone, we need to remove it, and keep current index + // to process the next one. + removeDevice(device); + mServer.deviceDisconnected(device); + } else { + // process the next one + d++; + } + } + + // at this point we should still have some new devices in newList, so we + // process them. + for (Device newDevice : newList) { + //should to check architecture if new device is an emulator on-lined + if(newDevice.isOnline() && newDevice.isEmulator()) + { + newDevice.checkArchitecture(); + } + // add them to the list + mDevices.add(newDevice); + mServer.deviceConnected(newDevice); + + // look for their build info. + if (newDevice.isOnline()) { + devicesToQuery.add(newDevice); + } + } + } + } + newList.clear(); + } + + private void removeDevice(Device device) { + mDevices.remove(device); + } + + + /** + * Reads the length of the next message from a socket. + * @param socket The {@link SocketChannel} to read from. + * @return the length, or 0 (zero) if no data is available from the socket. + * @throws IOException if the connection failed. + */ + private int readLength(SocketChannel socket, byte[] buffer) throws IOException { + String msg = read(socket, buffer); + + if (msg != null) { + try { + return Integer.parseInt(msg, 16); + } catch (NumberFormatException nfe) { + // we'll throw an exception below. + } + } + + // we receive something we can't read. It's better to reset the connection at this point. + throw new IOException("Unable to read length"); + } + + /** + * Fills a buffer from a socket. + * @param socket + * @param buffer + * @return the content of the buffer as a string, or null if it failed to convert the buffer. + * @throws IOException + */ + private String read(SocketChannel socket, byte[] buffer) throws IOException { + ByteBuffer buf = ByteBuffer.wrap(buffer, 0, buffer.length); + + while (buf.position() != buf.limit()) { + int count; + + count = socket.read(buf); + if (count < 0) { + throw new IOException("EOF"); + } + } + + try { + return new String(buffer, 0, buf.position(), SdbHelper.DEFAULT_ENCODING); + } catch (UnsupportedEncodingException e) { + // we'll return null below. + } + + return null; + } + +} diff --git a/org.tizen.common.sdblib/src/org/tizen/sdblib/FileListingService.java b/org.tizen.common.sdblib/src/org/tizen/sdblib/FileListingService.java new file mode 100644 index 0000000..be6e84a --- /dev/null +++ b/org.tizen.common.sdblib/src/org/tizen/sdblib/FileListingService.java @@ -0,0 +1,1003 @@ +/* + * 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 org.tizen.sdblib; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Provides {@link Device} side file listing service. + *

+ * To get an instance for a known {@link Device}, call + * {@link Device#getFileListingService()}. + */ +public final class FileListingService +{ + + /** Pattern to find filenames that match "*.apk" */ + private final static Pattern sApkPattern = Pattern.compile(".*\\.apk", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$ + + private final static String PM_FULL_LISTING = "pm list packages -f"; //$NON-NLS-1$ + + /** + * Pattern to parse the output of the 'pm -lf' command.
+ * The output format looks like:
+ * /data/app/myapp.apk=com.mypackage.myapp + */ + private final static Pattern sPmPattern = Pattern.compile("^package:(.+?)=(.+)$"); //$NON-NLS-1$ + + /** Top level data folder. */ + public final static String DIRECTORY_DATA = "data"; //$NON-NLS-1$ + /** Top level sdcard folder. */ + public final static String DIRECTORY_SDCARD = "sdcard"; //$NON-NLS-1$ + /** Top level mount folder. */ + public final static String DIRECTORY_MNT = "mnt"; //$NON-NLS-1$ + /** Top level system folder. */ + public final static String DIRECTORY_SYSTEM = "system"; //$NON-NLS-1$ + /** Top level temp folder. */ + public final static String DIRECTORY_TEMP = "tmp"; //$NON-NLS-1$ + /** Application folder. */ + public final static String DIRECTORY_APP = "app"; //$NON-NLS-1$ + + public static final long REFRESH_RATE = 5000L; + /** + * Refresh test has to be slightly lower for precision issue. + */ + static final long REFRESH_TEST = (long) (REFRESH_RATE * .8); + + /** Entry type: File */ + public static final int TYPE_FILE = 0; + /** Entry type: Directory */ + public static final int TYPE_DIRECTORY = 1; + /** Entry type: Directory Link */ + public static final int TYPE_DIRECTORY_LINK = 2; + /** Entry type: Block */ + public static final int TYPE_BLOCK = 3; + /** Entry type: Character */ + public static final int TYPE_CHARACTER = 4; + /** Entry type: Link */ + public static final int TYPE_LINK = 5; + /** Entry type: Socket */ + public static final int TYPE_SOCKET = 6; + /** Entry type: FIFO */ + public static final int TYPE_FIFO = 7; + /** Entry type: Other */ + public static final int TYPE_OTHER = 8; + /** Entry type: root */ + public static final int TYPE_ROOT_EMULATOR = 9; + public static final int TYPE_ROOT_DEVICE = 10; + + /** Device side file separator. */ + public static final String FILE_SEPARATOR = "/"; //$NON-NLS-1$ + + private static final String FILE_ROOT = "/"; //$NON-NLS-1$ + + /** + * Regexp pattern to parse the result from ls. Do not think the same format + * between Emulator and Device such as "ls -l --time-style=long-iso" + */ + + private static String LS_COMMAND_TIMESTYLE = " --time-style=long-iso" ; + +// private static String patternAnsiColor = "\\033\\[[0-9;]*m"; + private static String patternPermisions = "([bcdlsp-][-r][-w][-xsS][-r][-w][-xsS][-r][-w][-xstST])"; + private static String patternFilecount = "(\\d{1,})"; + private static String patternOwner = "(\\S+)"; + private static String patternGroup = "(\\S+)"; + private static String patternSize = "([\\d\\s,]*)"; + private static String patternDate = "(\\d{4}-\\d{2}-\\d{2}|[a-zA-Z]{3}\\s+\\d{1,2})"; + private static String patternTime = "(\\d{1,2}:\\d{1,2}|[0-9]{4})"; + private static String patternName = "(.*)"; + private static String patternLs = "^" + patternPermisions + "\\s+" + patternFilecount + "\\s+" + patternOwner + "\\s+" + patternGroup + "\\s+" + + patternSize + "\\s+" + patternDate + "\\s+" + patternTime + "\\s" + patternName + "$"; + + // private static Pattern sLsPattern = Pattern.compile( + // "^([bcdlsp-][-r][-w][-xsS][-r][-w][-xsS][-r][-w][-xstST])\\s+(\\S+)\\s+(\\S+)\\s+([\\d\\s,]*)\\s+(\\d{4}-\\d\\d-\\d\\d)\\s+(\\d\\d:\\d\\d)\\s+(.*)$"); //$NON-NLS-1$ + private static Pattern sLsPattern = Pattern.compile(patternLs); +// private static Pattern ansiColorReplacePattern = Pattern.compile(patternAnsiColor); + private final Device mDevice; + private FileEntry mRoot; + + private final ArrayList mThreadList = new ArrayList(); + + /** + * Represents an entry in a directory. This can be a file or a directory. + */ + public final static class FileEntry + { + /** Pattern to escape filenames for shell command consumption. */ + private final static Pattern sEscapePattern = Pattern.compile("([\\\\\"$])"); //$NON-NLS-1$ + + /** + * Comparator object for FileEntry + */ + private static Comparator sEntryComparator = new Comparator() + { + @Override + public int compare(FileEntry o1, FileEntry o2) + { + if (o1 instanceof FileEntry && o2 instanceof FileEntry) + { + FileEntry fe1 = (FileEntry) o1; + FileEntry fe2 = (FileEntry) o2; + return fe1.name.compareTo(fe2.name); + } + return 0; + } + }; + + FileEntry parent; + String name; + String info; + String permissions; + String size; + String date; + String time; + String owner; + String group; + String linkPath; + FileListingService fileListingService; + int type; + boolean isAppPackage; + + boolean isRoot; + + /** + * Indicates whether the entry content has been fetched yet, or not. + */ + long fetchTime = 0; + + final ArrayList mChildren = new ArrayList(); + + /** + * Creates a new file entry. + * + * @param parent + * parent entry or null if entry is root + * @param name + * name of the entry. + * @param type + * entry type. Can be one of the following: + * {@link FileListingService#TYPE_FILE}, + * {@link FileListingService#TYPE_DIRECTORY}, + * {@link FileListingService#TYPE_OTHER}. + */ + public FileEntry(FileEntry parent, String name, int type, boolean isRoot, FileListingService service) + { + this.parent = parent; + this.name = name; + this.type = type; + this.isRoot = isRoot; + this.fileListingService = service; + } + + /** + * Returns the name of the entry + */ + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + /** + * Returns the size string of the entry, as returned by ls. + */ + public String getSize() + { + return size; + } + + /** + * Returns the size of the entry. + */ + public int getSizeValue() + { + return Integer.parseInt(size); + } + + /** + * Returns the date string of the entry, as returned by ls. + */ + public String getDate() + { + return date; + } + + /** + * Returns the time string of the entry, as returned by ls. + */ + public String getTime() + { + return time; + } + + /** + * Returns the permission string of the entry, as returned by + * ls. + */ + public String getPermissions() + { + return permissions; + } + + /** + * Returns the extra info for the entry. + *

+ * For a link, it will be a description of the link. + *

+ * For an application apk file it will be the application package as + * returned by the Package Manager. + */ + public String getInfo() + { + return info; + } + + public String getLinkFullPath() + { + return linkPath; + } + + /** + * Return the full path of the entry. + * + * @return a path string using {@link FileListingService#FILE_SEPARATOR} + * as separator. + */ + public String getFullPath() + { + if (isRoot) + { + return FILE_ROOT; + } + StringBuilder pathBuilder = new StringBuilder(); + fillPathBuilder(pathBuilder, false); + + return pathBuilder.toString(); + } + + /** + * Return the fully escaped path of the entry. This path is safe to use + * in a shell command line. + * + * @return a path string using {@link FileListingService#FILE_SEPARATOR} + * as separator + */ + public String getFullEscapedPath() + { + StringBuilder pathBuilder = new StringBuilder(); + fillPathBuilder(pathBuilder, true); + + return pathBuilder.toString(); + } + + /** + * Returns the path as a list of segments. + */ + public String[] getPathSegments() + { + ArrayList list = new ArrayList(); + fillPathSegments(list); + + return list.toArray(new String[list.size()]); + } + + /** + * Returns true if the entry is a directory, false otherwise; + */ + public int getType() + { + return type; + } + + public FileListingService getFileListingService() + { + return fileListingService; + } + + /** + * Returns if the entry is a folder or a link to a folder. + */ + public boolean isDirectory() + { + return type == TYPE_DIRECTORY || type == TYPE_DIRECTORY_LINK + || type == TYPE_ROOT_DEVICE || type == TYPE_ROOT_EMULATOR ; + } + + /** + * Returns the parent entry. + */ + public FileEntry getParent() + { + return parent; + } + + /** + * Returns the cached children of the entry. This returns the cache + * created from calling FileListingService.getChildren(). + */ + public FileEntry[] getCachedChildren() + { + return mChildren.toArray(new FileEntry[mChildren.size()]); + } + + /** + * Returns the child {@link FileEntry} matching the name. This uses the + * cached children list. + * + * @param name + * the name of the child to return. + * @return the FileEntry matching the name or null. + */ + public FileEntry findChild(String name) + { + for (FileEntry entry : mChildren) + { + if (entry.name.equals(name)) + { + return entry; + } + } + return null; + } + + /** + * Returns whether the entry is the root. + */ + public boolean isRoot() + { + return isRoot; + } + + void addChild(FileEntry child) + { + mChildren.add(child); + } + + void setChildren(ArrayList newChildren) + { + mChildren.clear(); + mChildren.addAll(newChildren); + } + + boolean needFetch() + { + if (fetchTime == 0) + { + return true; + } + long current = System.currentTimeMillis(); + if (current - fetchTime > REFRESH_TEST) + { + return true; + } + + return false; + } + + /** + * Returns if the entry is a valid application package. + */ + public boolean isApplicationPackage() + { + return isAppPackage; + } + + /** + * Returns if the file name is an application package name. + */ + public boolean isAppFileName() + { + Matcher m = sApkPattern.matcher(name); + return m.matches(); + } + + /** + * Recursively fills the pathBuilder with the full path + * + * @param pathBuilder + * a StringBuilder used to create the path. + * @param escapePath + * Whether the path need to be escaped for consumption by a + * shell command line. + */ + protected void fillPathBuilder(StringBuilder pathBuilder, boolean escapePath) + { + if (isRoot) + { + return; + } + + if (parent != null) + { + parent.fillPathBuilder(pathBuilder, escapePath); + } + pathBuilder.append(FILE_SEPARATOR); + pathBuilder.append(escapePath ? escape(name) : name); + } + + /** + * Recursively fills the segment list with the full path. + * + * @param list + * The list of segments to fill. + */ + protected void fillPathSegments(ArrayList list) + { + if (isRoot) + { + return; + } + + if (parent != null) + { + parent.fillPathSegments(list); + } + + list.add(name); + } + + /** + * Returns an escaped version of the entry name. + * + * @param entryName + */ + private String escape(String entryName) + { + return sEscapePattern.matcher(entryName).replaceAll("\\\\$1"); //$NON-NLS-1$ + } + } + + private class LsReceiver extends MultiLineReceiver + { + + private final ArrayList mEntryList; + private final FileEntry[] mCurrentChildren; + private final FileEntry mParentEntry; +// private final boolean supportAnsiColor; + /** + * Create an ls receiver/parser. + * + * @param currentChildren + * The list of current children. To prevent collapse during + * update, reusing the same FileEntry objects for files that + * were already there is paramount. + * @param entryList + * the list of new children to be filled by the receiver. + * @param linkList + * the list of link path to compute post ls, to figure out if + * the link pointed to a file or to a directory. + * @param bSupportAnsiColor + * decision to get result from "ls" command without ansicode for color + */ + public LsReceiver(FileEntry parentEntry, ArrayList entryList) + { + mParentEntry = parentEntry; + mCurrentChildren = parentEntry.getCachedChildren(); + mEntryList = entryList; + } + + @Override + public void processNewLines(String[] lines) + { + for (String line : lines) + { + // no need to handle empty lines. + if (line.length() == 0) + { + continue; + } + + +// if(!supportAnsiColor) +// { +// StringBuffer buffer = new StringBuffer(); +// +// Matcher matcher = ansiColorReplacePattern.matcher(line); +// +// while(matcher.find()) +// { +// matcher.appendReplacement(buffer, ""); +// } +// } +// matcher.appendTail(buffer); +// line = buffer.toString(); +// } + + // run the line through the regexp + Matcher m = sLsPattern.matcher(line); + if (m.matches() == false) + { + continue; + } + + // get the name + String name = m.group(8); + // get the rest of the groups + String permissions = m.group(1); + String owner = m.group(3); + String group = m.group(4); + String size = m.group(5); + String date = m.group(6); + String time = m.group(7); + String info = null; + String linkTo = null; + int objectType = getFileType(permissions.charAt(0)); + + // now check what we may be linking to + if (objectType == TYPE_LINK) + { + String[] segments = name.split("\\s->\\s"); //$NON-NLS-1$ + + // we should have 2 segments + if (segments.length == 2) + { + // update the entry name to not + // contain the link + name = segments[0]; + + // and the link name + info = segments[1]; + linkTo = segments[1]; + + // add an arrow in front to specify + // it's a link. + info = "-> " + info; //$NON-NLS-1$; + // now get the path to the link + String[] pathSegments = info.split(FILE_SEPARATOR); + + if (pathSegments.length >= 1) + { + // FIXME : not fully impl + String command = "ls -al "; + if (linkTo.charAt(0) != '/') + command = command + "/"; + command = command + linkTo; + try + { + LsLinkReceiver receiver = new LsLinkReceiver(); + mDevice.executeShellCommand(command, receiver); + if (receiver.isDirectoryLink == true) + objectType = TYPE_DIRECTORY_LINK; + } catch (Exception e) + { + // sdb failed somehow, we do nothing. + } + } + } + } + // get the entry, either from an existing one, or a + // new one + FileEntry entry = getExistingEntry(name); + if (entry == null) + { + entry = new FileEntry(mParentEntry, name, objectType, false /* isRoot */, mParentEntry.fileListingService); + } + + // add some misc info + entry.permissions = permissions; + entry.size = size; + entry.date = date; + entry.time = time; + entry.owner = owner; + entry.group = group; + if (objectType == TYPE_LINK || objectType == TYPE_DIRECTORY_LINK) + { + entry.info = info; + entry.linkPath = linkTo; + } + + mEntryList.add(entry); + } + } + + final class LsLinkReceiver extends MultiLineReceiver + { + boolean isDirectoryLink = false; + + @Override + public void processNewLines(String[] lines) + { + for (String line : lines) + { + if (line.length() > 0) + { + Matcher m = sLsPattern.matcher(line); + if (m.matches()) + { + int objectType = getFileType(m.group(1).charAt(0)); + String name = m.group(8); + if (".".equals(name) && objectType == TYPE_DIRECTORY) + { + isDirectoryLink = true; + break; + } + } + } + } + + } + + @Override + public boolean isCancelled() + { + return false; + } + } + + /** + * Queries for an already existing Entry per name + * + * @param name + * the name of the entry + * @return the existing FileEntry or null if no entry with a matching + * name exists. + */ + private FileEntry getExistingEntry(String name) + { + for (int i = 0; i < mCurrentChildren.length; i++) + { + FileEntry e = mCurrentChildren[i]; + + // since we're going to "erase" the one we use, we need to + // check that the item is not null. + if (e != null) + { + // compare per name, case-sensitive. + if (name.equals(e.name)) + { + // erase from the list + mCurrentChildren[i] = null; + + // and return the object + return e; + } + } + } + + // couldn't find any matching object, return null + return null; + } + + @Override + public boolean isCancelled() + { + return false; + } + + public void finishLinks() + { + // TODO Handle links in the listing service + } + + private int getFileType(char type) + { + // and the type + int objectType = TYPE_OTHER; + switch (type) + { + case '-': + objectType = TYPE_FILE; + break; + case 'b': + objectType = TYPE_BLOCK; + break; + case 'c': + objectType = TYPE_CHARACTER; + break; + case 'd': + objectType = TYPE_DIRECTORY; + break; + case 'l': + objectType = TYPE_LINK; + break; + case 's': + objectType = TYPE_SOCKET; + break; + case 'p': + objectType = TYPE_FIFO; + break; + } + return objectType; + } + } + + /** + * Classes which implement this interface provide a method that deals with + * asynchronous result from ls command on the device. + * + * @see FileListingService#getChildren(com.samsung.sdblib.FileListingService.FileEntry, + * boolean, com.samsung.sdblib.FileListingService.IListingReceiver) + */ + public interface IListingReceiver + { + public void setChildren(FileEntry entry, FileEntry[] children); + + public void refreshEntry(FileEntry entry); + } + + /** + * Creates a File Listing Service for a specified {@link Device}. + * + * @param device + * The Device the service is connected to. + */ + FileListingService(Device device) + { + mDevice = device; + } + + /** + * Returns the root element. + * + * @return the {@link FileEntry} object representing the root element or + * null if the device is invalid. + */ + public FileEntry getRoot() + { + if (mDevice != null) + { + if (mRoot == null) + { + int deviceType = FileListingService.TYPE_ROOT_DEVICE; + if (mDevice.isEmulator()) + deviceType = FileListingService.TYPE_ROOT_EMULATOR; + + mRoot = new FileEntry(null, mDevice.getDeviceName(), deviceType, true, this); + } + return mRoot; + } + return null; + } + + public FileEntry[] getChildren(final FileEntry entry, boolean useCache, final IListingReceiver receiver) + { + return getChildren(entry, useCache, receiver, false); + } + + public FileEntry[] getChildrenWithAnsicodeColor(final FileEntry entry, boolean useCache, final IListingReceiver receiver) + { + return getChildren(entry, useCache, receiver, true); + } + + /** + * Returns the children of a {@link FileEntry}. + *

+ * This method supports a cache mechanism and synchronous and asynchronous + * modes. + *

+ * If receiver is null, the device side + * ls command is done synchronously, and the method will return + * upon completion of the command.
+ * If receiver is non null, the command is launched + * is a separate thread and upon completion, the receiver will be notified + * of the result. + *

+ * The result for each ls command is cached in the parent + * FileEntry. useCache allows usage of this cache, + * but only if the cache is valid. The cache is valid only for + * {@link FileListingService#REFRESH_RATE} ms. After that a new + * ls command is always executed. + *

+ * If the cache is valid and useCache == true, the method will + * always simply return the value of the cache, whether a + * {@link IListingReceiver} has been provided or not. + * + * @param entry + * The parent entry. + * @param useCache + * A flag to use the cache or to force a new ls command. + * @param receiver + * A receiver for asynchronous calls. + * @param bSupportAnsiColor + * A boolean value to get result from "ls" command without ansicode for color + * @return The list of children or null for asynchronous calls. + * + * @see FileEntry#getCachedChildren() + */ + private FileEntry[] getChildren(final FileEntry entry, boolean useCache, final IListingReceiver receiver, final boolean bSupportAnsiColor) + { + // first thing we do is check the cache, and if we already have a recent + // enough children list, we just return that. + if (useCache && entry.needFetch() == false) + { + return entry.getCachedChildren(); + } + + // if there's no receiver, then this is a synchronous call, and we + // return the result of ls + if (receiver == null) + { + doLs(entry, bSupportAnsiColor); + return entry.getCachedChildren(); + } + + // this is a asynchronous call. + // we launch a thread that will do ls and give the listing + // to the receiver + Thread t = new Thread("ls " + entry.getFullEscapedPath()) { //$NON-NLS-1$ + @Override + public void run() + { + doLs(entry, bSupportAnsiColor); + + receiver.setChildren(entry, entry.getCachedChildren()); + + final FileEntry[] children = entry.getCachedChildren(); + if (children.length > 0 && children[0].isApplicationPackage()) + { + final HashMap map = new HashMap(); + + for (FileEntry child : children) + { + String path = child.getFullEscapedPath(); + map.put(path, child); + } + + // call pm. + String command = PM_FULL_LISTING; + try + { + mDevice.executeShellCommand(command, new MultiLineReceiver() + { + @Override + public void processNewLines(String[] lines) + { + for (String line : lines) + { + if (line.length() > 0) + { + // get the filepath and package + // from the line + Matcher m = sPmPattern.matcher(line); + if (m.matches()) + { + // get the children with + // that path + FileEntry entry = map.get(m.group(1)); + if (entry != null) + { + entry.info = m.group(2); + receiver.refreshEntry(entry); + } + } + } + } + } + + @Override + public boolean isCancelled() + { + return false; + } + }); + } catch (Exception e) + { + // sdb failed somehow, we do nothing. + } + } + + // if another thread is pending, launch it + synchronized (mThreadList) + { + // first remove ourselves from the list + mThreadList.remove(this); + + // then launch the next one if applicable. + if (mThreadList.size() > 0) + { + Thread t = mThreadList.get(0); + t.start(); + } + } + } + }; + + // we don't want to run multiple ls on the device at the same time, so + // we + // store the thread in a list and launch it only if there's no other + // thread running. + // the thread will launch the next one once it's done. + synchronized (mThreadList) + { + // add to the list + mThreadList.add(t); + + // if it's the only one, launch it. + if (mThreadList.size() == 1) + { + t.start(); + } + } + + // and we return null. + return null; + } + + public IDevice getDevice() + { + return mDevice; + } + + private void doLs(FileEntry entry, boolean supportAnsiColor) + { + // create a list that will receive the list of the entries + ArrayList entryList = new ArrayList(); + + try + { + // create the command + String lsCommand = null; + String teeCommand = ""; + if (entry.getType() == FileListingService.TYPE_LINK || entry.getType() == FileListingService.TYPE_DIRECTORY_LINK) + { + if (entry.getLinkFullPath().charAt(0) == '/') + lsCommand = "ls -l " + getStringWithDoubleQuote(entry.getLinkFullPath()); //$NON-NLS-1$ + else + lsCommand = "ls -l " + getStringWithDoubleQuote("/" + entry.getLinkFullPath()); //$NON-NLS-1$ + } else + { + lsCommand = "ls -l " + getStringWithDoubleQuote("/" + entry.getFullEscapedPath()); //$NON-NLS-1$ + } + + if(!supportAnsiColor) + { + teeCommand = " 2> /dev/null | tee /dev/null"; + } + + // create the receiver object that will parse the result from ls + LsReceiver receiver = new LsReceiver(entry, entryList); + // call ls. + mDevice.executeShellCommand(lsCommand + LS_COMMAND_TIMESTYLE + teeCommand, receiver); + + // finish the process of the receiver to handle links + receiver.finishLinks(); + + //if fail to execute command, retry without timesytle + if(entryList.size() == 0) + { + mDevice.executeShellCommand(lsCommand + teeCommand, receiver); + receiver.finishLinks(); + } + + } catch (Exception e) + { + // catch all and do nothing. + } + + // at this point we need to refresh the viewer + entry.fetchTime = System.currentTimeMillis(); + + // sort the children and set them as the new children + Collections.sort(entryList, FileEntry.sEntryComparator); + entry.setChildren(entryList); + } + + private String getStringWithDoubleQuote(String str) + { + return "\"" + str + "\""; + } +} \ No newline at end of file diff --git a/org.tizen.common.sdblib/src/org/tizen/sdblib/IDevice.java b/org.tizen.common.sdblib/src/org/tizen/sdblib/IDevice.java new file mode 100644 index 0000000..8423f49 --- /dev/null +++ b/org.tizen.common.sdblib/src/org/tizen/sdblib/IDevice.java @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2008 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 org.tizen.sdblib; + +import java.io.IOException; + +/** + * A Device. It can be a physical device or an emulator. + */ +public interface IDevice { + /** Serial number of the first connected emulator. */ + public final static String FIRST_EMULATOR_SN = "emulator-26100"; //$NON-NLS-1$ + /** Device change bit mask: {@link DeviceState} change. */ + public static final int CHANGE_STATE = 0x0001; + /** Device change bit mask: {@link Client} list change. */ + public static final int CHANGE_CLIENT_LIST = 0x0002; + /** Device change bit mask: build info change. */ + public static final int CHANGE_BUILD_INFO = 0x0004; + + /** + * The architecture of a device. + * Returns {@link Arch#X86} or {@link Arch#ARM} about device architecture. + */ + public static enum Arch{ + X86("x86"), + ARM("arm"); + + private String arch; + + Arch(String arch) + { + this.arch = arch; + } + + public String getArch() + { + return arch; + } + } + + /** + * The state of a device. + */ + public static enum DeviceState { + OFFLINE("offline"), //$NON-NLS-1$ + ONLINE("device"); //$NON-NLS-1$ + + private String mState; + + DeviceState(String state) { + mState = state; + } + + /** + * Returns a {@link DeviceState} from the string returned by sdb devices. + * + * @param state the device state. + * @return a {@link DeviceState} object or null if the state is unknown. + */ + public static DeviceState getState(String state) { + for (DeviceState deviceState : values()) { + if (deviceState.mState.equals(state)) { + return deviceState; + } + } + return null; + } + } + + /** + * Returns the serial number of the device. + */ + public String getSerialNumber(); + + /** + * Returns the name of the device. + */ + public String getDeviceName(); + + public DeviceState getState(); + + /** + * Returns if the device is ready. + * + * @return true if {@link #getState()} returns {@link DeviceState#ONLINE}. + */ + public boolean isOnline(); + + /** + * Returns true if the device is an emulator. + */ + public boolean isEmulator(); + + /** + * Returns {@link Arch} about device architecture. + */ + public Arch getArch(); + + /** + * Returns if the device is offline. + * + * @return true if {@link #getState()} returns {@link DeviceState#OFFLINE}. + */ + public boolean isOffline(); + + /** + * Returns a {@link SyncService} object to push / pull files to and from the device. + * + * @return null if the SyncService couldn't be created. This can happen if sdb + * refuse to open the connection because the {@link IDevice} is invalid + * (or got disconnected). + * @throws TimeoutException in case of timeout on the connection. + * @throws SdbCommandRejectedException if sdb rejects the command + * @throws IOException if the connection with sdb failed. + */ + public SyncService getSyncService() + throws TimeoutException, SdbCommandRejectedException, IOException; + + /** + * Returns a {@link FileListingService} for this device. + */ + public FileListingService getFileListingService(); + + /** + * Executes a shell command on the device and return SdbShellProcess + * + * @param command the shell command to execute + * @throws IOException in case of I/O error on the connection. + * + * @see SdbShellProcess + */ + public SdbShellProcess executeShellCommand(String command) throws IOException; + + /** + * Executes a shell command on the device, and sends the result to a receiver + *

This is similar to calling + * executeShellCommand(command, receiver, SdbPreferences.getTimeOut()). + * + * @param command the shell command to execute + * @param receiver the {@link IShellOutputReceiver} that will receives the output of the shell + * command + * @throws TimeoutException in case of timeout on the connection. + * @throws SdbCommandRejectedException if sdb rejects the command + * @throws ShellCommandUnresponsiveException in case the shell command doesn't send output + * for a given time. + * @throws IOException in case of I/O error on the connection. + * + * @see #executeShellCommand(String, IShellOutputReceiver, int) + * @see SdbPreferences#getTimeOut() + */ + public void executeShellCommand(String command, IShellOutputReceiver receiver) + throws TimeoutException, SdbCommandRejectedException, ShellCommandUnresponsiveException, + IOException; + + /** + * Executes a shell command on the device, and sends the result to a receiver. + *

maxTimeToOutputResponse is used as a maximum waiting time when expecting the + * command output from the device.
+ * At any time, if the shell command does not output anything for a period longer than + * maxTimeToOutputResponse, then the method will throw + * {@link ShellCommandUnresponsiveException}. + *

For commands like log output, a maxTimeToOutputResponse value of 0, meaning + * that the method will never throw and will block until the receiver's + * {@link IShellOutputReceiver#isCancelled()} returns true, should be + * used. + * + * @param command the shell command to execute + * @param receiver the {@link IShellOutputReceiver} that will receives the output of the shell + * command + * @param maxTimeToOutputResponse the maximum amount of time during which the command is allowed + * to not output any response. A value of 0 means the method will wait forever + * (until the receiver cancels the execution) for command output and + * never throw. + * @throws TimeoutException in case of timeout on the connection when sending the command. + * @throws SdbCommandRejectedException if sdb rejects the command. + * @throws ShellCommandUnresponsiveException in case the shell command doesn't send any output + * for a period longer than maxTimeToOutputResponse. + * @throws IOException in case of I/O error on the connection. + * + * @see SdbPreferences#getTimeOut() + */ + public void executeShellCommand(String command, IShellOutputReceiver receiver, + int maxTimeToOutputResponse) + throws TimeoutException, SdbCommandRejectedException, ShellCommandUnresponsiveException, + IOException; + + /** + * Runs the event log service and outputs the event log to the {@link LogReceiver}. + *

This call is blocking until {@link LogReceiver#isCancelled()} returns true. + * @param receiver the receiver to receive the event log entries. + * @throws TimeoutException in case of timeout on the connection. This can only be thrown if the + * timeout happens during setup. Once logs start being received, no timeout will occur as it's + * not possible to detect a difference between no log and timeout. + * @throws SdbCommandRejectedException if sdb rejects the command + * @throws IOException in case of I/O error on the connection. + */ + public void runEventLogService(LogReceiver receiver) + throws TimeoutException, SdbCommandRejectedException, IOException; + + /** + * Runs the log service for the given log and outputs the log to the {@link LogReceiver}. + *

This call is blocking until {@link LogReceiver#isCancelled()} returns true. + * + * @param logname the logname of the log to read from. + * @param receiver the receiver to receive the event log entries. + * @throws TimeoutException in case of timeout on the connection. This can only be thrown if the + * timeout happens during setup. Once logs start being received, no timeout will + * occur as it's not possible to detect a difference between no log and timeout. + * @throws SdbCommandRejectedException if sdb rejects the command + * @throws IOException in case of I/O error on the connection. + */ + public void runLogService(String logname, LogReceiver receiver) + throws TimeoutException, SdbCommandRejectedException, IOException; + + /** + * Creates a port forwarding between a local and a remote port. + * + * @param localPort the local port to forward + * @param remotePort the remote port. + * @return true if success. + * @throws TimeoutException in case of timeout on the connection. + * @throws SdbCommandRejectedException if sdb rejects the command + * @throws IOException in case of I/O error on the connection. + */ + public void createForward(int localPort, int remotePort) + throws TimeoutException, SdbCommandRejectedException, IOException; + + /** + * Removes a port forwarding between a local and a remote port. + * + * @param localPort the local port to forward + * @param remotePort the remote port. + * @return true if success. + * @throws TimeoutException in case of timeout on the connection. + * @throws SdbCommandRejectedException if sdb rejects the command + * @throws IOException in case of I/O error on the connection. + */ + public void removeForward(int localPort, int remotePort) + throws TimeoutException, SdbCommandRejectedException, IOException; + + /** + * Pushes a file to device + * + * @param localFilePath the absolute path to file on local host + * @return {@link String} destination path on device for file + * @throws TimeoutException in case of timeout on the connection. + * @throws SdbCommandRejectedException if sdb rejects the command + * @throws IOException in case of I/O error on the connection. + */ + public String syncPackageToDevice(String localFilePath) + throws TimeoutException, SdbCommandRejectedException, IOException; + + /** + * Removes a file from device. + * + * @param remoteFilePath path on device of file to remove + * @throws TimeoutException in case of timeout on the connection. + * @throws SdbCommandRejectedException if sdb rejects the command + * @throws ShellCommandUnresponsiveException if the device didn't respond for long time when + * performing the action. + * @throws IOException if file removal failed + */ + public void removeRemotePackage(String remoteFilePath) + throws TimeoutException, SdbCommandRejectedException, ShellCommandUnresponsiveException, + IOException; +} diff --git a/org.tizen.common.sdblib/src/org/tizen/sdblib/IShellOutputReceiver.java b/org.tizen.common.sdblib/src/org/tizen/sdblib/IShellOutputReceiver.java new file mode 100644 index 0000000..76b63e7 --- /dev/null +++ b/org.tizen.common.sdblib/src/org/tizen/sdblib/IShellOutputReceiver.java @@ -0,0 +1,44 @@ +/* + * 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 org.tizen.sdblib; + +/** + * Classes which implement this interface provide methods that deal with out from a remote shell + * command on a device/emulator. + */ +public interface IShellOutputReceiver { + /** + * Called every time some new data is available. + * @param data The new data. + * @param offset The offset at which the new data starts. + * @param length The length of the new data. + */ + public void addOutput(byte[] data, int offset, int length); + + /** + * Called at the end of the process execution (unless the process was + * canceled). This allows the receiver to terminate and flush whatever + * data was not yet processed. + */ + public void flush(); + + /** + * Cancel method to stop the execution of the remote shell command. + * @return true to cancel the execution of the command. + */ + public boolean isCancelled(); +}; diff --git a/org.tizen.common.sdblib/src/org/tizen/sdblib/Log.java b/org.tizen.common.sdblib/src/org/tizen/sdblib/Log.java new file mode 100644 index 0000000..45f0e2e --- /dev/null +++ b/org.tizen.common.sdblib/src/org/tizen/sdblib/Log.java @@ -0,0 +1,358 @@ +/* + * 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 org.tizen.sdblib; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * Log class that mirrors the API in main SLP sources. + *

Default behavior outputs the log to {@link System#out}. Use + * {@link #setLogOutput(com.samsung.sdblib.Log.ILogOutput)} to redirect the log somewhere else. + */ +public final class Log { + + /** + * Log Level enum. + */ + public enum LogLevel { + VERBOSE(2, "Verbose", 'V'), //$NON-NLS-1$ + DEBUG(3, "Debug", 'D'), //$NON-NLS-1$ + INFO(4, "Info", 'I'), //$NON-NLS-1$ + WARN(5, "Warning", 'W'), //$NON-NLS-1$ + ERROR(6, "Error", 'E'), //$NON-NLS-1$ + ASSERT(7, "Assert", 'A'); //$NON-NLS-1$ + + private int mPriorityLevel; + private String mStringValue; + private char mPriorityLetter; + + LogLevel(int intPriority, String stringValue, char priorityChar) { + mPriorityLevel = intPriority; + mStringValue = stringValue; + mPriorityLetter = priorityChar; + } + + public static LogLevel getByString(String value) { + for (LogLevel mode : values()) { + if (mode.mStringValue.equals(value)) { + return mode; + } + } + + return null; + } + + /** + * Returns the {@link LogLevel} enum matching the specified letter. + * @param letter the letter matching a LogLevel enum + * @return a LogLevel object or null if no match were found. + */ + public static LogLevel getByLetter(char letter) { + for (LogLevel mode : values()) { + if (mode.mPriorityLetter == letter) { + return mode; + } + } + + return null; + } + + /** + * Returns the {@link LogLevel} enum matching the specified letter. + *

+ * The letter is passed as a {@link String} argument, but only the first character + * is used. + * @param letter the letter matching a LogLevel enum + * @return a LogLevel object or null if no match were found. + */ + public static LogLevel getByLetterString(String letter) { + if (letter.length() > 0) { + return getByLetter(letter.charAt(0)); + } + + return null; + } + + /** + * Returns the letter identifying the priority of the {@link LogLevel}. + */ + public char getPriorityLetter() { + return mPriorityLetter; + } + + /** + * Returns the numerical value of the priority. + */ + public int getPriority() { + return mPriorityLevel; + } + + /** + * Returns a non translated string representing the LogLevel. + */ + public String getStringValue() { + return mStringValue; + } + } + + /** + * Classes which implement this interface provides methods that deal with outputting log + * messages. + */ + public interface ILogOutput { + /** + * Sent when a log message needs to be printed. + * @param logLevel The {@link LogLevel} enum representing the priority of the message. + * @param tag The tag associated with the message. + * @param message The message to display. + */ + public void printLog(LogLevel logLevel, String tag, String message); + + /** + * Sent when a log message needs to be printed, and, if possible, displayed to the user + * in a dialog box. + * @param logLevel The {@link LogLevel} enum representing the priority of the message. + * @param tag The tag associated with the message. + * @param message The message to display. + */ + public void printAndPromptLog(LogLevel logLevel, String tag, String message); + } + + private static LogLevel mLevel = SdbPreferences.getLogLevel(); + + private static ILogOutput sLogOutput; + + private static final char[] mSpaceLine = new char[72]; + private static final char[] mHexDigit = new char[] + { '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f' }; + static { + /* prep for hex dump */ + int i = mSpaceLine.length-1; + while (i >= 0) + mSpaceLine[i--] = ' '; + mSpaceLine[0] = mSpaceLine[1] = mSpaceLine[2] = mSpaceLine[3] = '0'; + mSpaceLine[4] = '-'; + } + + static final class Config { + static final boolean LOGV = true; + static final boolean LOGD = true; + }; + + private Log() {} + + /** + * Outputs a {@link LogLevel#VERBOSE} level message. + * @param tag The tag associated with the message. + * @param message The message to output. + */ + public static void v(String tag, String message) { + println(LogLevel.VERBOSE, tag, message); + } + + /** + * Outputs a {@link LogLevel#DEBUG} level message. + * @param tag The tag associated with the message. + * @param message The message to output. + */ + public static void d(String tag, String message) { + println(LogLevel.DEBUG, tag, message); + } + + /** + * Outputs a {@link LogLevel#INFO} level message. + * @param tag The tag associated with the message. + * @param message The message to output. + */ + public static void i(String tag, String message) { + println(LogLevel.INFO, tag, message); + } + + /** + * Outputs a {@link LogLevel#WARN} level message. + * @param tag The tag associated with the message. + * @param message The message to output. + */ + public static void w(String tag, String message) { + println(LogLevel.WARN, tag, message); + } + + /** + * Outputs a {@link LogLevel#ERROR} level message. + * @param tag The tag associated with the message. + * @param message The message to output. + */ + public static void e(String tag, String message) { + println(LogLevel.ERROR, tag, message); + } + + /** + * Outputs a log message and attempts to display it in a dialog. + * @param tag The tag associated with the message. + * @param message The message to output. + */ + public static void logAndDisplay(LogLevel logLevel, String tag, String message) { + if (sLogOutput != null) { + sLogOutput.printAndPromptLog(logLevel, tag, message); + } else { + println(logLevel, tag, message); + } + } + + /** + * Outputs a {@link LogLevel#ERROR} level {@link Throwable} information. + * @param tag The tag associated with the message. + * @param throwable The {@link Throwable} to output. + */ + public static void e(String tag, Throwable throwable) { + if (throwable != null) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + + throwable.printStackTrace(pw); + println(LogLevel.ERROR, tag, throwable.getMessage() + '\n' + sw.toString()); + } + } + + static void setLevel(LogLevel logLevel) { + mLevel = logLevel; + } + + /** + * Sets the {@link ILogOutput} to use to print the logs. If not set, {@link System#out} + * will be used. + * @param logOutput The {@link ILogOutput} to use to print the log. + */ + public static void setLogOutput(ILogOutput logOutput) { + sLogOutput = logOutput; + } + + /** + * Show hex dump. + *

+ * Local addition. Output looks like: + * 1230- 00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff 0123456789abcdef + *

+ * Uses no string concatenation; creates one String object per line. + */ + static void hexDump(String tag, LogLevel level, byte[] data, int offset, int length) { + + int kHexOffset = 6; + int kAscOffset = 55; + char[] line = new char[mSpaceLine.length]; + int addr, baseAddr, count; + int i, ch; + boolean needErase = true; + + //Log.w(tag, "HEX DUMP: off=" + offset + ", length=" + length); + + baseAddr = 0; + while (length != 0) { + if (length > 16) { + // full line + count = 16; + } else { + // partial line; re-copy blanks to clear end + count = length; + needErase = true; + } + + if (needErase) { + System.arraycopy(mSpaceLine, 0, line, 0, mSpaceLine.length); + needErase = false; + } + + // output the address (currently limited to 4 hex digits) + addr = baseAddr; + addr &= 0xffff; + ch = 3; + while (addr != 0) { + line[ch] = mHexDigit[addr & 0x0f]; + ch--; + addr >>>= 4; + } + + // output hex digits and ASCII chars + ch = kHexOffset; + for (i = 0; i < count; i++) { + byte val = data[offset + i]; + + line[ch++] = mHexDigit[(val >>> 4) & 0x0f]; + line[ch++] = mHexDigit[val & 0x0f]; + ch++; + + if (val >= 0x20 && val < 0x7f) + line[kAscOffset + i] = (char) val; + else + line[kAscOffset + i] = '.'; + } + + println(level, tag, new String(line)); + + // advance to next chunk of data + length -= count; + offset += count; + baseAddr += count; + } + + } + + /** + * Dump the entire contents of a byte array with DEBUG priority. + */ + static void hexDump(byte[] data) { + hexDump("sdb", LogLevel.DEBUG, data, 0, data.length); + } + + /* currently prints to stdout; could write to a log window */ + private static void println(LogLevel logLevel, String tag, String message) { + if (logLevel.getPriority() >= mLevel.getPriority()) { + if (sLogOutput != null) { + sLogOutput.printLog(logLevel, tag, message); + } else { + printLog(logLevel, tag, message); + } + } + } + + /** + * Prints a log message. + * @param logLevel + * @param tag + * @param message + */ + public static void printLog(LogLevel logLevel, String tag, String message) { + System.out.print(getLogFormatString(logLevel, tag, message)); + } + + /** + * Formats a log message. + * @param logLevel + * @param tag + * @param message + */ + public static String getLogFormatString(LogLevel logLevel, String tag, String message) { + SimpleDateFormat formatter = new SimpleDateFormat("hh:mm:ss"); + return String.format("%s %c/%s: %s\n", formatter.format(new Date()), + logLevel.getPriorityLetter(), tag, message); + } +} + + diff --git a/org.tizen.common.sdblib/src/org/tizen/sdblib/LogReceiver.java b/org.tizen.common.sdblib/src/org/tizen/sdblib/LogReceiver.java new file mode 100644 index 0000000..ac7ec70 --- /dev/null +++ b/org.tizen.common.sdblib/src/org/tizen/sdblib/LogReceiver.java @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2008 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 org.tizen.sdblib; + +import java.security.InvalidParameterException; + +/** + * Receiver able to provide low level parsing for device-side log services. + */ +public final class LogReceiver { + + private final static int ENTRY_HEADER_SIZE = 20; // 2*2 + 4*4; see LogEntry. + + /** + * Represents a log entry and its raw data. + */ + public final static class LogEntry { + /* + * See //device/include/utils/logger.h + */ + /** 16bit unsigned: length of the payload. */ + public int len; /* This is normally followed by a 16 bit padding */ + /** pid of the process that generated this {@link LogEntry} */ + public int pid; + /** tid of the process that generated this {@link LogEntry} */ + public int tid; + /** Seconds since epoch. */ + public int sec; + /** nanoseconds. */ + public int nsec; + /** The entry's raw data. */ + public byte[] data; + }; + + /** + * Classes which implement this interface provide a method that deals + * with {@link LogEntry} objects coming from log service through a {@link LogReceiver}. + *

This interface provides two methods. + *

    + *
  • {@link #newEntry(com.samsung.sdblib.log.LogReceiver.LogEntry)} provides a + * first level of parsing, extracting {@link LogEntry} objects out of the log service output.
  • + *
  • {@link #newData(byte[], int, int)} provides a way to receive the raw information + * coming directly from the log service.
  • + *
+ */ + public interface ILogListener { + /** + * Sent when a new {@link LogEntry} has been parsed by the {@link LogReceiver}. + * @param entry the new log entry. + */ + public void newEntry(LogEntry entry); + + /** + * Sent when new raw data is coming from the log service. + * @param data the raw data buffer. + * @param offset the offset into the buffer signaling the beginning of the new data. + * @param length the length of the new data. + */ + public void newData(byte[] data, int offset, int length); + } + + /** Current {@link LogEntry} being read, before sending it to the listener. */ + private LogEntry mCurrentEntry; + + /** Temp buffer to store partial entry headers. */ + private byte[] mEntryHeaderBuffer = new byte[ENTRY_HEADER_SIZE]; + /** Offset in the partial header buffer */ + private int mEntryHeaderOffset = 0; + /** Offset in the partial entry data */ + private int mEntryDataOffset = 0; + + /** Listener waiting for receive fully read {@link LogEntry} objects */ + private ILogListener mListener; + + private boolean mIsCancelled = false; + + /** + * Creates a {@link LogReceiver} with an {@link ILogListener}. + *

+ * The {@link ILogListener} will receive new log entries as they are parsed, in the form + * of {@link LogEntry} objects. + * @param listener the listener to receive new log entries. + */ + public LogReceiver(ILogListener listener) { + mListener = listener; + } + + + /** + * Parses new data coming from the log service. + * @param data the data buffer + * @param offset the offset into the buffer signaling the beginning of the new data. + * @param length the length of the new data. + */ + public void parseNewData(byte[] data, int offset, int length) { + // notify the listener of new raw data + if (mListener != null) { + mListener.newData(data, offset, length); + } + + // loop while there is still data to be read and the receiver has not be cancelled. + while (length > 0 && mIsCancelled == false) { + // first check if we have no current entry. + if (mCurrentEntry == null) { + if (mEntryHeaderOffset + length < ENTRY_HEADER_SIZE) { + // if we don't have enough data to finish the header, save + // the data we have and return + System.arraycopy(data, offset, mEntryHeaderBuffer, mEntryHeaderOffset, length); + mEntryHeaderOffset += length; + return; + } else { + // we have enough to fill the header, let's do it. + // did we store some part at the beginning of the header? + if (mEntryHeaderOffset != 0) { + // copy the rest of the entry header into the header buffer + int size = ENTRY_HEADER_SIZE - mEntryHeaderOffset; + System.arraycopy(data, offset, mEntryHeaderBuffer, mEntryHeaderOffset, + size); + + // create the entry from the header buffer + mCurrentEntry = createEntry(mEntryHeaderBuffer, 0); + + // since we used the whole entry header buffer, we reset the offset + mEntryHeaderOffset = 0; + + // adjust current offset and remaining length to the beginning + // of the entry data + offset += size; + length -= size; + } else { + // create the entry directly from the data array + mCurrentEntry = createEntry(data, offset); + + // adjust current offset and remaining length to the beginning + // of the entry data + offset += ENTRY_HEADER_SIZE; + length -= ENTRY_HEADER_SIZE; + } + } + } + + // at this point, we have an entry, and offset/length have been updated to skip + // the entry header. + + // if we have enough data for this entry or more, we'll need to end this entry + if (length >= mCurrentEntry.len - mEntryDataOffset) { + // compute and save the size of the data that we have to read for this entry, + // based on how much we may already have read. + int dataSize = mCurrentEntry.len - mEntryDataOffset; + + // we only read what we need, and put it in the entry buffer. + System.arraycopy(data, offset, mCurrentEntry.data, mEntryDataOffset, dataSize); + + // notify the listener of a new entry + if (mListener != null) { + mListener.newEntry(mCurrentEntry); + } + + // reset some flags: we have read 0 data of the current entry. + // and we have no current entry being read. + mEntryDataOffset = 0; + mCurrentEntry = null; + + // and update the data buffer info to the end of the current entry / start + // of the next one. + offset += dataSize; + length -= dataSize; + } else { + // we don't have enough data to fill this entry, so we store what we have + // in the entry itself. + System.arraycopy(data, offset, mCurrentEntry.data, mEntryDataOffset, length); + + // save the amount read for the data. + mEntryDataOffset += length; + return; + } + } + } + + /** + * Returns whether this receiver is canceling the remote service. + */ + public boolean isCancelled() { + return mIsCancelled; + } + + /** + * Cancels the current remote service. + */ + public void cancel() { + mIsCancelled = true; + } + + /** + * Creates a {@link LogEntry} from the array of bytes. This expects the data buffer size + * to be at least offset + {@link #ENTRY_HEADER_SIZE}. + * @param data the data buffer the entry is read from. + * @param offset the offset of the first byte from the buffer representing the entry. + * @return a new {@link LogEntry} or null if some error happened. + */ + private LogEntry createEntry(byte[] data, int offset) { + if (data.length < offset + ENTRY_HEADER_SIZE) { + throw new InvalidParameterException( + "Buffer not big enough to hold full LoggerEntry header"); + } + + // create the new entry and fill it. + LogEntry entry = new LogEntry(); + entry.len = ArrayHelper.swapU16bitFromArray(data, offset); + + // we've read only 16 bits, but since there's also a 16 bit padding, + // we can skip right over both. + offset += 4; + + entry.pid = ArrayHelper.swap32bitFromArray(data, offset); + offset += 4; + entry.tid = ArrayHelper.swap32bitFromArray(data, offset); + offset += 4; + entry.sec = ArrayHelper.swap32bitFromArray(data, offset); + offset += 4; + entry.nsec = ArrayHelper.swap32bitFromArray(data, offset); + offset += 4; + + // allocate the data + entry.data = new byte[entry.len]; + + return entry; + } + +} diff --git a/org.tizen.common.sdblib/src/org/tizen/sdblib/MultiLineReceiver.java b/org.tizen.common.sdblib/src/org/tizen/sdblib/MultiLineReceiver.java new file mode 100644 index 0000000..2ad0e5f --- /dev/null +++ b/org.tizen.common.sdblib/src/org/tizen/sdblib/MultiLineReceiver.java @@ -0,0 +1,153 @@ +/* + * 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 org.tizen.sdblib; + +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; + +/** + * Base implementation of {@link IShellOutputReceiver}, that takes the raw data + * coming from the socket, and convert it into {@link String} objects. + *

+ * Additionally, it splits the string by lines. + *

+ * Classes extending it must implement {@link #processNewLines(String[])} which + * receives new parsed lines as they become available. + */ +public abstract class MultiLineReceiver implements IShellOutputReceiver { + + private String OUTPUT_DECODING = "UTF-8"; + private boolean mTrimLines = true; + + /** unfinished message line, stored for next packet */ + private String mUnfinishedLine = null; + + private final ArrayList mArray = new ArrayList(); + + /** + * Set the trim lines flag. + * + * @param trim + * hether the lines are trimmed, or not. + */ + public void setTrimLine(boolean trim) { + mTrimLines = trim; + } + + /* + * (non-Javadoc) + * + * @see com.samsung.sdblib.sdb.IShellOutputReceiver#addOutput( byte[], int, + * int) + */ + public final void addOutput(byte[] data, int offset, int length) { + if (isCancelled() == false) { + String s = null; + try { + s = new String(data, offset, length, OUTPUT_DECODING); //$NON-NLS-1$ + + } catch (UnsupportedEncodingException e) { + // normal encoding didn't work, try the default one + s = new String(data, offset, length); + } + + // ok we've got a string + if (s != null) { + // if we had an unfinished line we add it. + if (mUnfinishedLine != null) { + s = mUnfinishedLine + s; + mUnfinishedLine = null; + } + + // now we split the lines + mArray.clear(); + int start = 0; + do { + int index = s.indexOf("\r\n", start); //$NON-NLS-1$ + + // if \r\n was not found, this is an unfinished line + // and we store it to be processed for the next packet + if (index == -1) { + mUnfinishedLine = s.substring(start); + break; + } + + // so we found a \r\n; + // extract the line + String line = s.substring(start, index); + if (mTrimLines) { + line = line.trim(); + } + mArray.add(line); + + // move start to after the \r\n we found + start = index + 2; + } while (true); + + if (mArray.size() > 0) { + // at this point we've split all the lines. + // make the array + String[] lines = mArray.toArray(new String[mArray.size()]); + + // send it for final processing + processNewLines(lines); + } + } + } + } + + /* + * (non-Javadoc) + * + * @see com.samsung.sdblib.sdb.IShellOutputReceiver#flush() + */ + public final void flush() { + if (mUnfinishedLine != null && !mUnfinishedLine.isEmpty()) { + processNewLines(new String[] { mUnfinishedLine }); + } + + done(); + } + + /** + * Terminates the process. This is called after the last lines have been + * through {@link #processNewLines(String[])}. + */ + public void done() { + // do nothing. + } + + /* + * (non-Javadoc) + * + * @see com.samsung.sdblib.sdb.IShellOutputReceiver#isCancelled() + */ + public boolean isCancelled() { + return false; + } + + /** + * Called when new lines are being received by the remote process. + *

+ * It is guaranteed that the lines are complete when they are given to this + * method. + * + * @param lines + * The array containing the new lines. + */ + public abstract void processNewLines(String[] lines); +} diff --git a/org.tizen.common.sdblib/src/org/tizen/sdblib/NullOutputReceiver.java b/org.tizen.common.sdblib/src/org/tizen/sdblib/NullOutputReceiver.java new file mode 100644 index 0000000..1c30a82 --- /dev/null +++ b/org.tizen.common.sdblib/src/org/tizen/sdblib/NullOutputReceiver.java @@ -0,0 +1,50 @@ +/* + * 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 org.tizen.sdblib; + +/** + * Implementation of {@link IShellOutputReceiver} that does nothing. + *

This can be used to execute a remote shell command when the output is not needed. + */ +public final class NullOutputReceiver implements IShellOutputReceiver { + + private static NullOutputReceiver sReceiver = new NullOutputReceiver(); + + public static IShellOutputReceiver getReceiver() { + return sReceiver; + } + + /* (non-Javadoc) + * @see com.samsung.sdblib.sdb.IShellOutputReceiver#addOutput(byte[], int, int) + */ + public void addOutput(byte[] data, int offset, int length) { + } + + /* (non-Javadoc) + * @see com.samsung.sdblib.sdb.IShellOutputReceiver#flush() + */ + public void flush() { + } + + /* (non-Javadoc) + * @see com.samsung.sdblib.sdb.IShellOutputReceiver#isCancelled() + */ + public boolean isCancelled() { + return false; + } + +} diff --git a/org.tizen.common.sdblib/src/org/tizen/sdblib/SdbCommandRejectedException.java b/org.tizen.common.sdblib/src/org/tizen/sdblib/SdbCommandRejectedException.java new file mode 100644 index 0000000..1209939 --- /dev/null +++ b/org.tizen.common.sdblib/src/org/tizen/sdblib/SdbCommandRejectedException.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2010 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 org.tizen.sdblib; + +import java.io.IOException; + +/** + * Exception thrown when sdb refuses a command. + */ +public class SdbCommandRejectedException extends IOException { + private static final long serialVersionUID = 1L; + private final boolean mIsDeviceOffline; + private final boolean mErrorDuringDeviceSelection; + + SdbCommandRejectedException(String message) { + super(message); + mIsDeviceOffline = "device offline".equals(message); + mErrorDuringDeviceSelection = false; + } + + SdbCommandRejectedException(String message, boolean errorDuringDeviceSelection) { + super(message); + mErrorDuringDeviceSelection = errorDuringDeviceSelection; + mIsDeviceOffline = "device offline".equals(message); + } + + /** + * Returns true if the error is due to the device being offline. + */ + public boolean isDeviceOffline() { + return mIsDeviceOffline; + } + + /** + * Returns whether sdb refused to target a given device for the command. + *

If false, sdb refused the command itself, if true, it refused to target the given + * device. + */ + public boolean wasErrorDuringDeviceSelection() { + return mErrorDuringDeviceSelection; + } +} diff --git a/org.tizen.common.sdblib/src/org/tizen/sdblib/SdbHelper.java b/org.tizen.common.sdblib/src/org/tizen/sdblib/SdbHelper.java new file mode 100644 index 0000000..58ec7e5 --- /dev/null +++ b/org.tizen.common.sdblib/src/org/tizen/sdblib/SdbHelper.java @@ -0,0 +1,738 @@ +/* + * 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 org.tizen.sdblib; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; + +/** + * Helper class to handle requests and connections to sdb. + *

{@link DebugBridgeServer} is the public API to connection to sdb, while {@link SdbHelper} + * does the low level stuff. + *

This currently uses spin-wait non-blocking I/O. A Selector would be more efficient, + * but seems like overkill for what we're doing here. + */ +final class SdbHelper { + + // public static final long kOkay = 0x59414b4fL; + // public static final long kFail = 0x4c494146L; + + static final int WAIT_TIME = 5; // spin-wait sleep, in ms + + static final String DEFAULT_ENCODING = "ISO-8859-1"; //$NON-NLS-1$ + static final String UTF_ENCODING = "UTF-8"; + + /** do not instantiate */ + private SdbHelper() { + } + + /** + * Response from SDB. + */ + static class SdbResponse { + public SdbResponse() { + message = ""; + } + + public boolean okay; // first 4 bytes in response were "OKAY"? + + public String message; // diagnostic string if #okay is false + } + + /** + * Create and connect a new pass-through socket, from the host to a port on + * the device. + * + * @param sdbSockAddr + * @param device the device to connect to. Can be null in which case the connection will be + * to the first available device. + * @param devicePort the port we're opening + * @throws TimeoutException in case of timeout on the connection. + * @throws IOException in case of I/O error on the connection. + * @throws SdbCommandRejectedException if sdb rejects the command + */ + public static SocketChannel open(InetSocketAddress sdbSockAddr, + Device device, int devicePort) + throws IOException, TimeoutException, SdbCommandRejectedException { + + SocketChannel sdbChan = SocketChannel.open(sdbSockAddr); + try { + sdbChan.socket().setTcpNoDelay(true); + sdbChan.configureBlocking(false); + + // if the device is not -1, then we first tell sdb we're looking to + // talk to a specific device + setDevice(sdbChan, device); + + byte[] req = createsdbForwardRequest(null, devicePort); + // Log.hexDump(req); + + write(sdbChan, req); + + SdbResponse resp = readSdbResponse(sdbChan, false); + if (resp.okay == false) { + throw new SdbCommandRejectedException(resp.message); + } + + sdbChan.configureBlocking(true); + } catch (TimeoutException e) { + sdbChan.close(); + throw e; + } catch (IOException e) { + sdbChan.close(); + throw e; + } + + return sdbChan; + } + + /** + * Creates and connects a new pass-through socket, from the host to a port on + * the device. + * + * @param sdbSockAddr + * @param device the device to connect to. Can be null in which case the connection will be + * to the first available device. + * @param pid the process pid to connect to. + * @throws TimeoutException in case of timeout on the connection. + * @throws SdbCommandRejectedException if sdb rejects the command + * @throws IOException in case of I/O error on the connection. + */ + public static SocketChannel createPassThroughConnection(InetSocketAddress sdbSockAddr, + Device device, int pid) + throws TimeoutException, SdbCommandRejectedException, IOException { + + SocketChannel sdbChan = SocketChannel.open(sdbSockAddr); + try { + sdbChan.socket().setTcpNoDelay(true); + sdbChan.configureBlocking(false); + + // if the device is not -1, then we first tell sdb we're looking to + // talk to a specific device + setDevice(sdbChan, device); + + byte[] req = createJdwpForwardRequest(pid); + // Log.hexDump(req); + + write(sdbChan, req); + + SdbResponse resp = readSdbResponse(sdbChan, false /* readDiagString */); + if (resp.okay == false) { + throw new SdbCommandRejectedException(resp.message); + } + + sdbChan.configureBlocking(true); + } catch (TimeoutException e) { + sdbChan.close(); + throw e; + } catch (IOException e) { + sdbChan.close(); + throw e; + } + + return sdbChan; + } + + /** + * Creates a port forwarding request for sdb. This returns an array + * containing "####tcp:{port}:{addStr}". + * @param addrStr the host. Can be null. + * @param port the port on the device. This does not need to be numeric. + */ + private static byte[] createsdbForwardRequest(String addrStr, int port) { + String reqStr; + + if (addrStr == null) + reqStr = "tcp:" + port; + else + reqStr = "tcp:" + port + ":" + addrStr; + return formSdbRequest(reqStr); + } + + /** + * Creates a port forwarding request to a jdwp process. This returns an array + * containing "####jwdp:{pid}". + * @param pid the jdwp process pid on the device. + */ + private static byte[] createJdwpForwardRequest(int pid) { + String reqStr = String.format("jdwp:%1$d", pid); //$NON-NLS-1$ + return formSdbRequest(reqStr); + } + + /** + * Create an ASCII string preceeded by four hex digits. The opening "####" + * is the length of the rest of the string, encoded as ASCII hex (case + * doesn't matter). "port" and "host" are what we want to forward to. If + * we're on the host side connecting into the device, "addrStr" should be + * null. + */ + static byte[] formSdbRequest(String req) { + + byte [] result; + byte [] reqByte; + byte [] lengthByte; + + try { + reqByte = req.getBytes(UTF_ENCODING); + + //info about reqByte length with 4 digit ex) 001D + lengthByte = String.format("%04X", reqByte.length).getBytes(); + + result = new byte[reqByte.length + 4]; + System.arraycopy(lengthByte, 0, result, 0, 4); + System.arraycopy(reqByte, 0, result, 4, reqByte.length); + + } catch (UnsupportedEncodingException uee) { + uee.printStackTrace(); // not expected + return null; + } + assert result.length == reqByte.length + 4; + return result; + } + + /** + * Reads the response from sdb after a command. + * @param chan The socket channel that is connected to sdb. + * @param readDiagString If true, we're expecting an OKAY response to be + * followed by a diagnostic string. Otherwise, we only expect the + * diagnostic string to follow a FAIL. + * @throws TimeoutException in case of timeout on the connection. + * @throws IOException in case of I/O error on the connection. + */ + static SdbResponse readSdbResponse(SocketChannel chan, boolean readDiagString) + throws TimeoutException, IOException { + + SdbResponse resp = new SdbResponse(); + + byte[] reply = new byte[4]; + read(chan, reply); + + if (isOkay(reply)) { + resp.okay = true; + } else { + readDiagString = true; // look for a reason after the FAIL + resp.okay = false; + } + + // not a loop -- use "while" so we can use "break" + try { + while (readDiagString) { + // length string is in next 4 bytes + byte[] lenBuf = new byte[4]; + read(chan, lenBuf); + + String lenStr = replyToString(lenBuf); + + int len; + try { + len = Integer.parseInt(lenStr, 16); + } catch (NumberFormatException nfe) { + Log.w("sdb", "Expected digits, got '" + lenStr + "': " + + lenBuf[0] + " " + lenBuf[1] + " " + lenBuf[2] + " " + + lenBuf[3]); + Log.w("sdb", "reply was " + replyToString(reply)); + break; + } + + byte[] msg = new byte[len]; + read(chan, msg); + + resp.message = replyToString(msg); + Log.v("sdb", "Got reply '" + replyToString(reply) + "', diag='" + + resp.message + "'"); + + break; + } + } catch (Exception e) { + // ignore those, since it's just reading the diagnose string, the response will + // contain okay==false anyway. + } + + return resp; + } + + /** + * Executes a shell command on the device without output receiver. + * + * @param sdbSockAddr the {@link InetSocketAddress} to sdb. + * @param command the shell command to execute + * @param device the {@link IDevice} on which to execute the command. + * @throws SdbCommandRejectedException if sdb rejects the command + * @throws IOException in case of I/O error on the connection. + * + */ + static void executeRemoteCommand(InetSocketAddress sdbSockAddr, + String command, IDevice device) + throws SdbCommandRejectedException, IOException { + Log.v("sdb", "execute: running " + command); + + SocketChannel sdbChan = null; + try { + sdbChan = SocketChannel.open(sdbSockAddr); + sdbChan.configureBlocking(false); + + // if the device is not -1, then we first tell sdb we're looking to + // talk + // to a specific device + setDevice(sdbChan, device); + + byte[] request = formSdbRequest("shell:" + command); //$NON-NLS-1$ + write(sdbChan, request); + + SdbResponse resp = readSdbResponse(sdbChan, false /* readDiagString */); + if (resp.okay == false) { + Log.e("sdb", "sdb rejected shell command (" + command + "): " + resp.message); + throw new SdbCommandRejectedException(resp.message); + } + + byte[] data = new byte[16384]; + ByteBuffer buf = ByteBuffer.wrap(data); + int exitCount = 0; + while(true) { + int count = 0; + sdbChan.read(buf); + if (count < 0) { + // met EOF + break; + } else if (count == 0) { + if (exitCount < 5) { + try { + int wait = WAIT_TIME * 5; + exitCount++; + Thread.sleep(wait); + } catch (InterruptedException ie) { + } + } else + break; + } else { + buf.rewind(); + } + } + } finally { + if (sdbChan != null) { + sdbChan.close(); + } + } + } + + /** + * Executes a shell command on the device and retrieve the output. The output is + * handed to rcvr as it arrives. + * + * @param sdbSockAddr the {@link InetSocketAddress} to sdb. + * @param command the shell command to execute + * @param device the {@link IDevice} on which to execute the command. + * @param rcvr the {@link IShellOutputReceiver} that will receives the output of the shell + * command + * @param maxTimeToOutputResponse max time between command output. If more time passes + * between command output, the method will throw + * {@link ShellCommandUnresponsiveException}. A value of 0 means the method will + * wait forever for command output and never throw. + * @throws TimeoutException in case of timeout on the connection when sending the command. + * @throws SdbCommandRejectedException if sdb rejects the command + * @throws ShellCommandUnresponsiveException in case the shell command doesn't send any output + * for a period longer than maxTimeToOutputResponse. + * @throws IOException in case of I/O error on the connection. + * + * @see SdbPreferences#getTimeOut() + */ + static void executeRemoteCommand(InetSocketAddress sdbSockAddr, + String command, IDevice device, IShellOutputReceiver rcvr, int maxTimeToOutputResponse) + throws TimeoutException, SdbCommandRejectedException, ShellCommandUnresponsiveException, + IOException { + Log.v("sdb", "execute: running " + command); + + SocketChannel sdbChan = null; + try { + sdbChan = SocketChannel.open(sdbSockAddr); + sdbChan.configureBlocking(false); + + // if the device is not -1, then we first tell sdb we're looking to + // talk + // to a specific device + setDevice(sdbChan, device); + + byte[] request = formSdbRequest("shell:" + command); //$NON-NLS-1$ + write(sdbChan, request); + + SdbResponse resp = readSdbResponse(sdbChan, false /* readDiagString */); + if (resp.okay == false) { + Log.e("sdb", "sdb rejected shell command (" + command + "): " + resp.message); + throw new SdbCommandRejectedException(resp.message); + } + + byte[] data = new byte[16384]; + ByteBuffer buf = ByteBuffer.wrap(data); + int timeToResponseCount = 0; + while (true) { + int count; + + if (rcvr != null && rcvr.isCancelled()) { + Log.v("sdb", "execute: cancelled"); + break; + } + + count = sdbChan.read(buf); + if (count < 0) { + // we're at the end, we flush the output + rcvr.flush(); + Log.v("sdb", "execute '" + command + "' on '" + device + "' : EOF hit. Read: " + + count); + break; + } else if (count == 0) { + try { + int wait = WAIT_TIME * 5; + timeToResponseCount += wait; + if (maxTimeToOutputResponse > 0 && + timeToResponseCount > maxTimeToOutputResponse) { + throw new ShellCommandUnresponsiveException(); + } + Thread.sleep(wait); + } catch (InterruptedException ie) { + } + } else { + // reset timeout + timeToResponseCount = 0; + + // send data to receiver if present + if (rcvr != null) { + rcvr.addOutput(buf.array(), buf.arrayOffset(), buf.position()); + } + buf.rewind(); + } + } + } finally { + if (sdbChan != null) { + sdbChan.close(); + } + Log.v("sdb", "execute: returning"); + } + } + + /** + * Runs the Event log service on the {@link Device}, and provides its output to the + * {@link LogReceiver}. + *

This call is blocking until {@link LogReceiver#isCancelled()} returns true. + * @param sdbSockAddr the socket address to connect to sdb + * @param device the Device on which to run the service + * @param rcvr the {@link LogReceiver} to receive the log output + * @throws TimeoutException in case of timeout on the connection. + * @throws SdbCommandRejectedException if sdb rejects the command + * @throws IOException in case of I/O error on the connection. + */ + public static void runEventLogService(InetSocketAddress sdbSockAddr, Device device, + LogReceiver rcvr) throws TimeoutException, SdbCommandRejectedException, IOException { + runLogService(sdbSockAddr, device, "events", rcvr); //$NON-NLS-1$ + } + + /** + * Runs a log service on the {@link Device}, and provides its output to the {@link LogReceiver}. + *

This call is blocking until {@link LogReceiver#isCancelled()} returns true. + * @param sdbSockAddr the socket address to connect to sdb + * @param device the Device on which to run the service + * @param logName the name of the log file to output + * @param rcvr the {@link LogReceiver} to receive the log output + * @throws TimeoutException in case of timeout on the connection. + * @throws SdbCommandRejectedException if sdb rejects the command + * @throws IOException in case of I/O error on the connection. + */ + public static void runLogService(InetSocketAddress sdbSockAddr, Device device, String logName, + LogReceiver rcvr) throws TimeoutException, SdbCommandRejectedException, IOException { + SocketChannel sdbChan = null; + + try { + sdbChan = SocketChannel.open(sdbSockAddr); + sdbChan.configureBlocking(false); + + // if the device is not -1, then we first tell sdb we're looking to talk + // to a specific device + setDevice(sdbChan, device); + + byte[] request = formSdbRequest("log:" + logName); + write(sdbChan, request); + + SdbResponse resp = readSdbResponse(sdbChan, false /* readDiagString */); + if (resp.okay == false) { + throw new SdbCommandRejectedException(resp.message); + } + + byte[] data = new byte[16384]; + ByteBuffer buf = ByteBuffer.wrap(data); + while (true) { + int count; + + if (rcvr != null && rcvr.isCancelled()) { + break; + } + + count = sdbChan.read(buf); + if (count < 0) { + break; + } else if (count == 0) { + try { + Thread.sleep(WAIT_TIME * 5); + } catch (InterruptedException ie) { + } + } else { + if (rcvr != null) { + rcvr.parseNewData(buf.array(), buf.arrayOffset(), buf.position()); + } + buf.rewind(); + } + } + } finally { + if (sdbChan != null) { + sdbChan.close(); + } + } + } + + /** + * Creates a port forwarding between a local and a remote port. + * @param sdbSockAddr the socket address to connect to sdb + * @param device the device on which to do the port fowarding + * @param localPort the local port to forward + * @param remotePort the remote port. + * @throws TimeoutException in case of timeout on the connection. + * @throws SdbCommandRejectedException if sdb rejects the command + * @throws IOException in case of I/O error on the connection. + */ + public static void createForward(InetSocketAddress sdbSockAddr, Device device, int localPort, + int remotePort) throws TimeoutException, SdbCommandRejectedException, IOException { + + SocketChannel sdbChan = null; + try { + sdbChan = SocketChannel.open(sdbSockAddr); + sdbChan.configureBlocking(false); + + byte[] request = formSdbRequest(String.format( + "host-serial:%1$s:forward:tcp:%2$d;tcp:%3$d", //$NON-NLS-1$ + device.getSerialNumber(), localPort, remotePort)); + + write(sdbChan, request); + + SdbResponse resp = readSdbResponse(sdbChan, false /* readDiagString */); + if (resp.okay == false) { + Log.w("create-forward", "Error creating forward: " + resp.message); + throw new SdbCommandRejectedException(resp.message); + } + } finally { + if (sdbChan != null) { + sdbChan.close(); + } + } + } + + /** + * Remove a port forwarding between a local and a remote port. + * @param sdbSockAddr the socket address to connect to sdb + * @param device the device on which to remove the port fowarding + * @param localPort the local port of the forward + * @param remotePort the remote port. + * @throws TimeoutException in case of timeout on the connection. + * @throws SdbCommandRejectedException if sdb rejects the command + * @throws IOException in case of I/O error on the connection. + */ + public static void removeForward(InetSocketAddress sdbSockAddr, Device device, int localPort, + int remotePort) throws TimeoutException, SdbCommandRejectedException, IOException { + + SocketChannel sdbChan = null; + try { + sdbChan = SocketChannel.open(sdbSockAddr); + sdbChan.configureBlocking(false); + + byte[] request = formSdbRequest(String.format( + "host-serial:%1$s:killforward:tcp:%2$d;tcp:%3$d", //$NON-NLS-1$ + device.getSerialNumber(), localPort, remotePort)); + + write(sdbChan, request); + + SdbResponse resp = readSdbResponse(sdbChan, false /* readDiagString */); + if (resp.okay == false) { + Log.w("remove-forward", "Error creating forward: " + resp.message); + throw new SdbCommandRejectedException(resp.message); + } + } finally { + if (sdbChan != null) { + sdbChan.close(); + } + } + } + + /** + * Checks to see if the first four bytes in "reply" are OKAY. + */ + static boolean isOkay(byte[] reply) { + return reply[0] == (byte)'O' && reply[1] == (byte)'K' + && reply[2] == (byte)'A' && reply[3] == (byte)'Y'; + } + + /** + * Converts an sdb reply to a string. + */ + static String replyToString(byte[] reply) { + String result; + try { + result = new String(reply, DEFAULT_ENCODING); + } catch (UnsupportedEncodingException uee) { + uee.printStackTrace(); // not expected + result = ""; + } + return result; + } + + /** + * Reads from the socket until the array is filled, or no more data is coming (because + * the socket closed or the timeout expired). + *

This uses the default time out value. + * + * @param chan the opened socket to read from. It must be in non-blocking + * mode for timeouts to work + * @param data the buffer to store the read data into. + * @throws TimeoutException in case of timeout on the connection. + * @throws IOException in case of I/O error on the connection. + */ + static void read(SocketChannel chan, byte[] data) throws TimeoutException, IOException { + read(chan, data, -1, SdbPreferences.getTimeOut()); + } + + /** + * Reads from the socket until the array is filled, the optional length + * is reached, or no more data is coming (because the socket closed or the + * timeout expired). After "timeout" milliseconds since the + * previous successful read, this will return whether or not new data has + * been found. + * + * @param chan the opened socket to read from. It must be in non-blocking + * mode for timeouts to work + * @param data the buffer to store the read data into. + * @param length the length to read or -1 to fill the data buffer completely + * @param timeout The timeout value. A timeout of zero means "wait forever". + */ + static void read(SocketChannel chan, byte[] data, int length, int timeout) + throws TimeoutException, IOException { + ByteBuffer buf = ByteBuffer.wrap(data, 0, length != -1 ? length : data.length); + int numWaits = 0; + + while (buf.position() != buf.limit()) { + int count; + + count = chan.read(buf); + if (count < 0) { + Log.d("sdb", "read: channel EOF"); + throw new IOException("EOF"); + } else if (count == 0) { + // TODO: need more accurate timeout? + if (timeout != 0 && numWaits * WAIT_TIME > timeout) { + Log.d("sdb", "read: timeout"); + throw new TimeoutException(); + } + // non-blocking spin + try { + Thread.sleep(WAIT_TIME); + } catch (InterruptedException ie) { + } + numWaits++; + } else { + numWaits = 0; + } + } + } + + /** + * Write until all data in "data" is written or the connection fails or times out. + *

This uses the default time out value. + * @param chan the opened socket to write to. + * @param data the buffer to send. + * @throws TimeoutException in case of timeout on the connection. + * @throws IOException in case of I/O error on the connection. + */ + static void write(SocketChannel chan, byte[] data) throws TimeoutException, IOException { + write(chan, data, -1, SdbPreferences.getTimeOut()); + } + + /** + * Write until all data in "data" is written, the optional length is reached, + * the timeout expires, or the connection fails. Returns "true" if all + * data was written. + * @param chan the opened socket to write to. + * @param data the buffer to send. + * @param length the length to write or -1 to send the whole buffer. + * @param timeout The timeout value. A timeout of zero means "wait forever". + * @throws TimeoutException in case of timeout on the connection. + * @throws IOException in case of I/O error on the connection. + */ + static void write(SocketChannel chan, byte[] data, int length, int timeout) + throws TimeoutException, IOException { + ByteBuffer buf = ByteBuffer.wrap(data, 0, length != -1 ? length : data.length); + int numWaits = 0; + + while (buf.position() != buf.limit()) { + int count; + + count = chan.write(buf); + if (count < 0) { + Log.d("sdb", "write: channel EOF"); + throw new IOException("channel EOF"); + } else if (count == 0) { + // TODO: need more accurate timeout? + if (timeout != 0 && numWaits * WAIT_TIME > timeout) { + Log.d("sdb", "write: timeout"); + throw new TimeoutException(); + } + // non-blocking spin + try { + Thread.sleep(WAIT_TIME); + } catch (InterruptedException ie) { + } + numWaits++; + } else { + numWaits = 0; + } + } + } + + /** + * tells sdb to talk to a specific device + * + * @param sdbChan the socket connection to sdb + * @param device The device to talk to. + * @throws TimeoutException in case of timeout on the connection. + * @throws SdbCommandRejectedException if sdb rejects the command + * @throws IOException in case of I/O error on the connection. + */ + static void setDevice(SocketChannel sdbChan, IDevice device) + throws TimeoutException, SdbCommandRejectedException, IOException { + // if the device is not -1, then we first tell sdb we're looking to talk + // to a specific device + if (device != null) { + String msg = "host:transport:" + device.getSerialNumber(); //$NON-NLS-1$ + byte[] device_query = formSdbRequest(msg); + + write(sdbChan, device_query); + + SdbResponse resp = readSdbResponse(sdbChan, false /* readDiagString */); + if (resp.okay == false) { + throw new SdbCommandRejectedException(resp.message, + true/*errorDuringDeviceSelection*/); + } + } + } +} diff --git a/org.tizen.common.sdblib/src/org/tizen/sdblib/SdbPreferences.java b/org.tizen.common.sdblib/src/org/tizen/sdblib/SdbPreferences.java new file mode 100644 index 0000000..4e4d326 --- /dev/null +++ b/org.tizen.common.sdblib/src/org/tizen/sdblib/SdbPreferences.java @@ -0,0 +1,79 @@ +/* + * 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 org.tizen.sdblib; + +import org.tizen.sdblib.Log.LogLevel; + +/** + * Preferences for the sdb library. + *

This class does not handle storing the preferences. It is merely a central point for + * applications using the sdblib to override the default values. + *

Various components of the sdblib query this class to get their values. + *

Calls to some set##() methods will update the components using the values + * right away, while other methods will have no effect once {@link AndroidDebugBridge#init(boolean)} + * has been called. + *

Check the documentation of each method. + */ +public final class SdbPreferences { + + public final static LogLevel DEFAULT_LOG_LEVEL = LogLevel.ERROR; + /** Default timeout values for sdb connection (milliseconds) */ + public static final int DEFAULT_TIMEOUT = 5000; // standard delay, in ms + + private static LogLevel sLogLevel = DEFAULT_LOG_LEVEL; + private static int sTimeOut = DEFAULT_TIMEOUT; + + /** + * Returns the minimum {@link LogLevel} being displayed. + */ + public static LogLevel getLogLevel() { + return sLogLevel; + } + + /** + * Sets the minimum {@link LogLevel} to display. + *

This change takes effect right away. + */ + public static void setLogLevel(String value) { + sLogLevel = LogLevel.getByString(value); + + Log.setLevel(sLogLevel); + } + + /** + * Returns the timeout to be used in sdb connections (milliseconds). + */ + public static int getTimeOut() { + return sTimeOut; + } + + /** + * Sets the timeout value for sdb connection. + *

This change takes effect for newly created connections only. + * @param timeOut the timeout value (milliseconds). + */ + public static void setTimeOut(int timeOut) { + sTimeOut = timeOut; + } + + /** + * Non accessible constructor. + */ + private SdbPreferences() { + // pass, only static methods in the class. + } +} diff --git a/org.tizen.common.sdblib/src/org/tizen/sdblib/SdbShellProcess.java b/org.tizen.common.sdblib/src/org/tizen/sdblib/SdbShellProcess.java new file mode 100644 index 0000000..69b600c --- /dev/null +++ b/org.tizen.common.sdblib/src/org/tizen/sdblib/SdbShellProcess.java @@ -0,0 +1,82 @@ +package org.tizen.sdblib; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; + +public class SdbShellProcess extends Process { + + public Process process = null; + public BufferedWriter bw = null; + + public SdbShellProcess(Process process, String command) { + this.process = process; + bw = new BufferedWriter(new OutputStreamWriter(this.process.getOutputStream())); + try { + bw.write(command + ";exit"); + bw.newLine(); + bw.flush(); + } catch (IOException e) { + // do nothing + } + } + + private void interrupt() { + if (bw == null) + return; + + char c = 0x03; //standard code representing "Ctrl-C" sequence + try { + bw.write(c); + bw.flush(); + Thread.sleep(250); // wait for terminate + } catch (IOException e) { + // do nothing + } catch (InterruptedException e) { + // do nothing + } finally { + try { + bw.close(); + } catch (IOException e) { + // do nothing + } + } + } + + @Override + public void destroy() { + interrupt(); + try { + process.exitValue(); + } catch (IllegalThreadStateException e) { + process.destroy(); + } + } + + @Override + public int exitValue() { + return process.exitValue(); + } + + @Override + public InputStream getErrorStream() { + return process.getErrorStream(); + } + + @Override + public InputStream getInputStream() { + return process.getInputStream(); + } + + @Override + public OutputStream getOutputStream() { + return process.getOutputStream(); + } + + @Override + public int waitFor() throws InterruptedException { + return process.waitFor(); + } +} diff --git a/org.tizen.common.sdblib/src/org/tizen/sdblib/ShellCommandUnresponsiveException.java b/org.tizen.common.sdblib/src/org/tizen/sdblib/ShellCommandUnresponsiveException.java new file mode 100644 index 0000000..bcd8bf4 --- /dev/null +++ b/org.tizen.common.sdblib/src/org/tizen/sdblib/ShellCommandUnresponsiveException.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2010 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 org.tizen.sdblib; + +import java.io.IOException; + +/** + * Exception thrown when a shell command executed on a device takes too long to send its output. + *

The command may not actually be unresponsive, it just has spent too much time not outputting + * any thing to the console. + */ +public class ShellCommandUnresponsiveException extends IOException { + private static final long serialVersionUID = 1L; +} diff --git a/org.tizen.common.sdblib/src/org/tizen/sdblib/SmartDevelopmentBridge.java b/org.tizen.common.sdblib/src/org/tizen/sdblib/SmartDevelopmentBridge.java new file mode 100644 index 0000000..1d30a74 --- /dev/null +++ b/org.tizen.common.sdblib/src/org/tizen/sdblib/SmartDevelopmentBridge.java @@ -0,0 +1,855 @@ +/* + * 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 org.tizen.sdblib; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.security.InvalidParameterException; +import java.util.ArrayList; + +/** + * A connection to the host-side smart development bridge (sdb) + *

This is the central point to communicate with any devices, emulators, or the applications + * running on them. + *

{@link #init(boolean)} must be called before anything is done. + */ +public final class SmartDevelopmentBridge { + private final static String SDB = "sdb"; //$NON-NLS-1$ + private final static String SDBLIB = "sdblib"; //$NON-NLS-1$ + private final static String SERVER_PORT_ENV_VAR = "SDB_SERVER_PORT"; //$NON-NLS-1$ + + // Where to find the SDB bridge. + final static String SDB_HOST = "127.0.0.1"; //$NON-NLS-1$ + final static int SDB_PORT = 26099; + + private static InetAddress sHostAddr; + private static InetSocketAddress sSocketAddr; + + private static SmartDevelopmentBridge sThis; + + /** Full path to sdb. */ + private String mSdbOsLocation = null; + + private boolean mStarted = false; + + private DeviceMonitor mDeviceMonitor; + + private final static ArrayList sBridgeListeners = + new ArrayList(); + private final static ArrayList sDeviceListeners = + new ArrayList(); + + // lock object for synchronization + private static final Object sLock = sBridgeListeners; + + /** + * Classes which implement this interface provide a method that deals + * with {@link SmartDevelopmentBridge} changes. + */ + public interface IDebugBridgeChangeListener { + /** + * Sent when a new {@link SmartDevelopmentBridge} is connected. + *

+ * This is sent from a non UI thread. + * @param bridge the new {@link SmartDevelopmentBridge} object. + */ + public void bridgeChanged(SmartDevelopmentBridge bridge); + } + + /** + * Classes which implement this interface provide methods that deal + * with {@link IDevice} addition, deletion, and changes. + */ + public interface IDeviceChangeListener { + /** + * Sent when the a device is connected to the {@link SmartDevelopmentBridge}. + *

+ * This is sent from a non UI thread. + * @param device the new device. + */ + public void deviceConnected(IDevice device); + + /** + * Sent when the a device is connected to the {@link SmartDevelopmentBridge}. + *

+ * This is sent from a non UI thread. + * @param device the new device. + */ + public void deviceDisconnected(IDevice device); + + /** + * Sent when a device data changed, or when clients are started/terminated on the device. + *

+ * This is sent from a non UI thread. + * @param device the device that was updated. + * @param changeMask the mask describing what changed. It can contain any of the following + * values: {@link IDevice#CHANGE_BUILD_INFO}, {@link IDevice#CHANGE_STATE}, + * {@link IDevice#CHANGE_CLIENT_LIST} + */ + public void deviceChanged(IDevice device, int changeMask); + } + + /** + * Initializes the sdb library. + *

This must be called once before any call to + * {@link #createBridge(String, boolean)}. + *

The library can be initialized in 2 ways: + *

    + *
  • Mode 1: clientSupport == true.
    The library monitors the + * devices and the applications running on them. It will connect to each application, as a + * debugger of sort, to be able to interact with them through JDWP packets.
  • + *
  • Mode 2: clientSupport == false.
    The library only monitors + * devices. The applications are left untouched, letting other tools built on + * sdblib to connect a debugger to them.
  • + *
+ *

Only one tool can run in mode 1 at the same time. + *

Note that mode 1 does not prevent debugging of applications running on devices. Mode 1 + * lets debuggers connect to sdblib which acts as a proxy between the debuggers and + * the applications to debug. See {@link Client#getDebuggerListenPort()}. + *

The preferences of sdblib should also be initialized with whatever default + * values were changed from the default values. + *

When the application quits, {@link #terminate()} should be called. + * @param clientSupport Indicates whether the library should enable the monitoring and + * interaction with applications running on the devices. + * @see SmartDevelopmentBridge#createBridge(String, boolean) + * @see SdbPreferences + */ + public static void init() { + // Determine port and instantiate socket address. + initsdbSocketAddr(); + } + + /** + * Terminates the sdb library. This must be called upon application termination. + */ + public static void terminate() { + // kill the monitoring services + if (sThis != null && sThis.mDeviceMonitor != null) { + sThis.mDeviceMonitor.stop(); + sThis.mDeviceMonitor = null; + } + } + + /** + * Returns the socket address of the SDB server on the host. + */ + public static InetSocketAddress getSocketAddress() { + return sSocketAddr; + } + + /** + * Creates a {@link SmartDevelopmentBridge} that is not linked to any particular executable. + *

This bridge will expect sdb to be running. It will not be able to start/stop + * sdb. + *

If a bridge has already been started, it is directly returned with no changes (similar + * to calling {@link #getBridge()}). + * @return a connected bridge. + */ + public static SmartDevelopmentBridge createBridge() { + synchronized (sLock) { + if (sThis != null) { + return sThis; + } + + try { + sThis = new SmartDevelopmentBridge(); + sThis.start(); + } catch (InvalidParameterException e) { + sThis = null; + } + + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IDebugBridgeChangeListener[] listenersCopy = sBridgeListeners.toArray( + new IDebugBridgeChangeListener[sBridgeListeners.size()]); + + // notify the listeners of the change + for (IDebugBridgeChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.bridgeChanged(sThis); + } catch (Exception e) { + Log.e(SDBLIB, e); + } + } + + return sThis; + } + } + + + /** + * Creates a new debug bridge from the location of the command line tool. + *

+ * Any existing server will be disconnected, unless the location is the same and + * forceNewBridge is set to false. + * @param osLocation the location of the command line tool 'sdb' + * @param forceNewBridge force creation of a new bridge even if one with the same location + * already exists. + * @return a connected bridge. + * @throws Exception + */ + public static SmartDevelopmentBridge createBridge(String osLocation, boolean forceNewBridge){ + synchronized (sLock) { + if (sThis != null) { + if (sThis.mSdbOsLocation != null && sThis.mSdbOsLocation.equals(osLocation) && + forceNewBridge == false) { + return sThis; + } else { + // stop the current server + sThis.stop(); + } + } + + sThis = new SmartDevelopmentBridge(osLocation); + sThis.start(); + + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IDebugBridgeChangeListener[] listenersCopy = sBridgeListeners.toArray( + new IDebugBridgeChangeListener[sBridgeListeners.size()]); + + // notify the listeners of the change + for (IDebugBridgeChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.bridgeChanged(sThis); + } catch (Exception e) { + Log.e(SDBLIB, e); + } + } + + return sThis; + } + } + + /** + * Returns the current debug bridge. Can be null if none were created. + */ + public static SmartDevelopmentBridge getBridge() { + return sThis; + } + + public String getSdbOsLocation() { + return mSdbOsLocation; + } + + /** + * Disconnects the current debug bridge, and destroy the object. + *

This also stops the current sdb host server. + *

+ * A new object will have to be created with {@link #createBridge(String, boolean)}. + */ + public static void disconnectBridge() { + synchronized (sLock) { + if (sThis != null) { + sThis.stop(); + sThis = null; + + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IDebugBridgeChangeListener[] listenersCopy = sBridgeListeners.toArray( + new IDebugBridgeChangeListener[sBridgeListeners.size()]); + + // notify the listeners. + for (IDebugBridgeChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.bridgeChanged(sThis); + } catch (Exception e) { + Log.e(SDBLIB, e); + } + } + } + } + } + + /** + * Adds the listener to the collection of listeners who will be notified when a new + * {@link SmartDevelopmentBridge} is connected, by sending it one of the messages defined + * in the {@link IDebugBridgeChangeListener} interface. + * @param listener The listener which should be notified. + */ + public static void addDebugBridgeChangeListener(IDebugBridgeChangeListener listener) { + synchronized (sLock) { + if (sBridgeListeners.contains(listener) == false) { + sBridgeListeners.add(listener); + if (sThis != null) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.bridgeChanged(sThis); + } catch (Exception e) { + Log.e(SDBLIB, e); + } + } + } + } + } + + /** + * Removes the listener from the collection of listeners who will be notified when a new + * {@link SmartDevelopmentBridge} is started. + * @param listener The listener which should no longer be notified. + */ + public static void removeDebugBridgeChangeListener(IDebugBridgeChangeListener listener) { + synchronized (sLock) { + sBridgeListeners.remove(listener); + } + } + + /** + * Adds the listener to the collection of listeners who will be notified when a {@link IDevice} + * is connected, disconnected, or when its properties or its {@link Client} list changed, + * by sending it one of the messages defined in the {@link IDeviceChangeListener} interface. + * @param listener The listener which should be notified. + */ + public static void addDeviceChangeListener(IDeviceChangeListener listener) { + synchronized (sLock) { + if (sDeviceListeners.contains(listener) == false) { + sDeviceListeners.add(listener); + } + } + } + + /** + * Removes the listener from the collection of listeners who will be notified when a + * {@link IDevice} is connected, disconnected, or when its properties or its {@link Client} + * list changed. + * @param listener The listener which should no longer be notified. + */ + public static void removeDeviceChangeListener(IDeviceChangeListener listener) { + synchronized (sLock) { + sDeviceListeners.remove(listener); + } + } + + /** + * Returns the devices. + * @see #hasInitialDeviceList() + */ + public IDevice[] getDevices() { + synchronized (sLock) { + if (mDeviceMonitor != null) { + return mDeviceMonitor.getDevices(); + } + } + + return new IDevice[0]; + } + + /** + * Returns whether the bridge has acquired the initial list from sdb after being created. + *

Calling {@link #getDevices()} right after {@link #createBridge(String, boolean)} will + * generally result in an empty list. This is due to the internal asynchronous communication + * mechanism with sdb that does not guarantee that the {@link IDevice} list has been + * built before the call to {@link #getDevices()}. + *

The recommended way to get the list of {@link IDevice} objects is to create a + * {@link IDeviceChangeListener} object. + */ + public boolean hasInitialDeviceList() { + if (mDeviceMonitor != null) { + return mDeviceMonitor.hasInitialDeviceList(); + } + + return false; + } + + /** + * Returns whether the {@link SmartDevelopmentBridge} object is still connected to the sdb daemon. + */ + public boolean isConnected() { + if (mDeviceMonitor != null) { + return mDeviceMonitor.isMonitoring(); + } + return false; + } + + /** + * Returns the number of times the {@link SmartDevelopmentBridge} object attempted to connect + * to the sdb daemon. + */ + public int getConnectionAttemptCount() { + if (mDeviceMonitor != null) { + return mDeviceMonitor.getConnectionAttemptCount(); + } + return -1; + } + + /** + * Returns the number of times the {@link SmartDevelopmentBridge} object attempted to restart + * the sdb daemon. + */ + public int getRestartAttemptCount() { + if (mDeviceMonitor != null) { + return mDeviceMonitor.getRestartAttemptCount(); + } + return -1; + } + + /** + * Creates a new bridge. + * @param osLocation the location of the command line tool + * @throws InvalidParameterException + */ + private SmartDevelopmentBridge(String osLocation) throws InvalidParameterException { + if (osLocation == null || osLocation.length() == 0) { + throw new InvalidParameterException(); + } + mSdbOsLocation = osLocation; + } + + /** + * Creates a new bridge not linked to any particular sdb executable. + */ + private SmartDevelopmentBridge() { + } + + /** + * Starts the debug bridge. + * @return true if success. + */ + boolean start() { + + mStarted = true; + // now that the bridge is connected, we start the underlying services. + mDeviceMonitor = new DeviceMonitor(this); + mDeviceMonitor.start(); + + return true; + } + + /** + * Kills the debug bridge, and the sdb host server. + * @return true if success + */ + boolean stop() { + // if we haven't started we return false; + if (mStarted == false) { + return false; + } + + // kill the monitoring services + mDeviceMonitor.stop(); + mDeviceMonitor = null; + + if (stopSdb() == false) { + return false; + } + + mStarted = false; + return true; + } + + /** + * Restarts sdb, but not the services around it. + * @return true if success. + */ + public boolean restart() { + if (mSdbOsLocation == null) { + Log.e(SDB, + "Cannot restart sdb when SmartDevelopmentBridge is created without the location of sdb."); //$NON-NLS-1$ + return false; + } + + synchronized (this) { + stopSdb(); + + boolean restart = startSdb(); + + if (restart && mDeviceMonitor == null) { + mDeviceMonitor = new DeviceMonitor(this); + mDeviceMonitor.start(); + } + + return restart; + } + } + + /** + * Notify the listener of a new {@link IDevice}. + *

+ * The notification of the listeners is done in a synchronized block. It is important to + * expect the listeners to potentially access various methods of {@link IDevice} as well as + * {@link #getDevices()} which use internal locks. + *

+ * For this reason, any call to this method from a method of {@link DeviceMonitor}, + * {@link IDevice} which is also inside a synchronized block, should first synchronize on + * the {@link SmartDevelopmentBridge} lock. Access to this lock is done through {@link #getLock()}. + * @param device the new IDevice. + * @see #getLock() + */ + void deviceConnected(IDevice device) { + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IDeviceChangeListener[] listenersCopy = null; + synchronized (sLock) { + listenersCopy = sDeviceListeners.toArray( + new IDeviceChangeListener[sDeviceListeners.size()]); + } + + // Notify the listeners + for (IDeviceChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.deviceConnected(device); + } catch (Exception e) { + Log.e(SDBLIB, e); + } + } + } + + /** + * Notify the listener of a disconnected {@link IDevice}. + *

+ * The notification of the listeners is done in a synchronized block. It is important to + * expect the listeners to potentially access various methods of {@link IDevice} as well as + * {@link #getDevices()} which use internal locks. + *

+ * For this reason, any call to this method from a method of {@link DeviceMonitor}, + * {@link IDevice} which is also inside a synchronized block, should first synchronize on + * the {@link SmartDevelopmentBridge} lock. Access to this lock is done through {@link #getLock()}. + * @param device the disconnected IDevice. + * @see #getLock() + */ + void deviceDisconnected(IDevice device) { + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IDeviceChangeListener[] listenersCopy = null; + synchronized (sLock) { + listenersCopy = sDeviceListeners.toArray( + new IDeviceChangeListener[sDeviceListeners.size()]); + } + + // Notify the listeners + for (IDeviceChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.deviceDisconnected(device); + } catch (Exception e) { + Log.e(SDBLIB, e); + } + } + } + + /** + * Notify the listener of a modified {@link IDevice}. + *

+ * The notification of the listeners is done in a synchronized block. It is important to + * expect the listeners to potentially access various methods of {@link IDevice} as well as + * {@link #getDevices()} which use internal locks. + *

+ * For this reason, any call to this method from a method of {@link DeviceMonitor}, + * {@link IDevice} which is also inside a synchronized block, should first synchronize on + * the {@link SmartDevelopmentBridge} lock. Access to this lock is done through {@link #getLock()}. + * @param device the modified IDevice. + * @see #getLock() + */ + void deviceChanged(IDevice device, int changeMask) { + // because the listeners could remove themselves from the list while processing + // their event callback, we make a copy of the list and iterate on it instead of + // the main list. + // This mostly happens when the application quits. + IDeviceChangeListener[] listenersCopy = null; + synchronized (sLock) { + listenersCopy = sDeviceListeners.toArray( + new IDeviceChangeListener[sDeviceListeners.size()]); + } + + // Notify the listeners + for (IDeviceChangeListener listener : listenersCopy) { + // we attempt to catch any exception so that a bad listener doesn't kill our + // thread + try { + listener.deviceChanged(device, changeMask); + } catch (Exception e) { + Log.e(SDBLIB, e); + } + } + } + + /** + * Returns the {@link DeviceMonitor} object. + */ + DeviceMonitor getDeviceMonitor() { + return mDeviceMonitor; + } + + /** + * Starts the sdb host side server. + * @return true if success + */ + synchronized boolean startSdb() { + if (mSdbOsLocation == null) { + Log.e(SDB, + "Cannot start sdb when SmartDevelopmentBridge is created without the location of sdb."); //$NON-NLS-1$ + return false; + } + + Process proc; + int status = -1; + + try { + String[] command = new String[2]; + command[0] = mSdbOsLocation; + command[1] = "start-server"; //$NON-NLS-1$ + Log.d(SDBLIB, + String.format("Launching '%1$s %2$s' to ensure SDB is running.", //$NON-NLS-1$ + mSdbOsLocation, command[1])); + proc = Runtime.getRuntime().exec(command); + + ArrayList errorOutput = new ArrayList(); + ArrayList stdOutput = new ArrayList(); + status = grabProcessOutput(proc, errorOutput, stdOutput, + false /* waitForReaders */); + + } catch (IOException ioe) { + Log.d(SDBLIB, "Unable to run 'sdb': " + ioe.getMessage()); //$NON-NLS-1$ + // we'll return false; + } catch (InterruptedException ie) { + Log.d(SDBLIB, "Unable to run 'sdb': " + ie.getMessage()); //$NON-NLS-1$ + // we'll return false; + } + + if (status != 0) { + Log.w(SDBLIB, + "'sdb start-server' failed -- run manually if necessary"); //$NON-NLS-1$ + return false; + } + + Log.d(SDBLIB, "'sdb start-server' succeeded"); //$NON-NLS-1$ + return true; + } + + /** + * Stops the sdb host side server. + * @return true if success + */ + private synchronized boolean stopSdb() { + if (mSdbOsLocation == null) { + Log.e(SDB, + "Cannot stop sdb when SmartDevelopmentBridge is created without the location of sdb."); //$NON-NLS-1$ + return false; + } + + Process proc; + int status = -1; + + try { + String[] command = new String[2]; + command[0] = mSdbOsLocation; + command[1] = "kill-server"; //$NON-NLS-1$ + proc = Runtime.getRuntime().exec(command); + status = proc.waitFor(); + } + catch (IOException ioe) { + // we'll return false; + } + catch (InterruptedException ie) { + // we'll return false; + } + + if (status != 0) { + Log.w(SDBLIB, + "'sdb kill-server' failed -- run manually if necessary"); //$NON-NLS-1$ + return false; + } + + Log.d(SDBLIB, "'sdb kill-server' succeeded"); //$NON-NLS-1$ + return true; + } + + /** + * Get the stderr/stdout outputs of a process and return when the process is done. + * Both must be read or the process will block on windows. + * @param process The process to get the ouput from + * @param errorOutput The array to store the stderr output. cannot be null. + * @param stdOutput The array to store the stdout output. cannot be null. + * @param displayStdOut If true this will display stdout as well + * @param waitforReaders if true, this will wait for the reader threads. + * @return the process return code. + * @throws InterruptedException + */ + private int grabProcessOutput(final Process process, final ArrayList errorOutput, + final ArrayList stdOutput, boolean waitforReaders) + throws InterruptedException { + assert errorOutput != null; + assert stdOutput != null; + // read the lines as they come. if null is returned, it's + // because the process finished + Thread t1 = new Thread("SdbReadThread1") { //$NON-NLS-1$ + @Override + public void run() { + // create a buffer to read the stderr output + InputStreamReader is = new InputStreamReader(process.getErrorStream()); + BufferedReader errReader = new BufferedReader(is); + + try { + while (true) { + String line = errReader.readLine(); + if (line != null) { + Log.e(SDB, line); + errorOutput.add(line); + } else { + break; + } + } + } catch (IOException e) { + // do nothing. + } + } + }; + + Thread t2 = new Thread("SdbReadThread2") { //$NON-NLS-1$ + @Override + public void run() { + InputStreamReader is = new InputStreamReader(process.getInputStream()); + BufferedReader outReader = new BufferedReader(is); + + try { + while (true) { + String line = outReader.readLine(); + if (line != null) { + Log.d(SDB, line); + stdOutput.add(line); + } else { + break; + } + } + } catch (IOException e) { + // do nothing. + } + } + }; + + t1.start(); + t2.start(); + + // it looks like on windows process#waitFor() can return + // before the thread have filled the arrays, so we wait for both threads and the + // process itself. + if (waitforReaders) { + try { + t1.join(); + } catch (InterruptedException e) { + } + try { + t2.join(); + } catch (InterruptedException e) { + } + } + + // get the return code from the process + return process.waitFor(); + } + + /** + * Returns the singleton lock used by this class to protect any access to the listener. + *

+ * This includes adding/removing listeners, but also notifying listeners of new bridges, + * devices, and clients. + */ + static Object getLock() { + return sLock; + } + + /** + * Instantiates sSocketAddr with the address of the host's sdb process. + */ + private static void initsdbSocketAddr() { + try { + int sdb_port = determineAndValidateSdbPort(); + sHostAddr = InetAddress.getByName(SDB_HOST); + sSocketAddr = new InetSocketAddress(sHostAddr, sdb_port); + } catch (UnknownHostException e) { + // localhost should always be known. + } + } + + /** + * Determines port where SDB is expected by looking at an env variable. + *

+ * The value for the environment variable SDB_SERVER_PORT is validated, + * IllegalArgumentException is thrown on illegal values. + *

+ * @return The port number where the host's sdb should be expected or started. + * @throws IllegalArgumentException if SDB_SERVER_PORT has a non-numeric value. + */ + private static int determineAndValidateSdbPort() { + String sdb_env_var; + int result = SDB_PORT; + try { + sdb_env_var = System.getenv(SERVER_PORT_ENV_VAR); + + if (sdb_env_var != null) { + sdb_env_var = sdb_env_var.trim(); + } + + if (sdb_env_var != null && sdb_env_var.length() > 0) { + // C tools (sdb, emulator) accept hex and octal port numbers, so need to accept + // them too. + result = Integer.decode(sdb_env_var); + + if (result <= 0) { + String errMsg = "env var " + SERVER_PORT_ENV_VAR //$NON-NLS-1$ + + ": must be >=0, got " //$NON-NLS-1$ + + System.getenv(SERVER_PORT_ENV_VAR); + throw new IllegalArgumentException(errMsg); + } + } + } catch (NumberFormatException nfEx) { + String errMsg = "env var " + SERVER_PORT_ENV_VAR //$NON-NLS-1$ + + ": illegal value '" //$NON-NLS-1$ + + System.getenv(SERVER_PORT_ENV_VAR) + "'"; //$NON-NLS-1$ + throw new IllegalArgumentException(errMsg); + } catch (SecurityException secEx) { + // A security manager has been installed that doesn't allow access to env vars. + // So an environment variable might have been set, but we can't tell. + // Let's log a warning and continue with SDB's default port. + // The issue is that sdb would be started (by the forked process having access + // to the env vars) on the desired port, but within this process, we can't figure out + // what that port is. However, a security manager not granting access to env vars + // but allowing to fork is a rare and interesting configuration, so the right + // thing seems to be to continue using the default port, as forking is likely to + // fail later on in the scenario of the security manager. + Log.w(SDBLIB, + "No access to env variables allowed by current security manager. " //$NON-NLS-1$ + + "If you've set SDB_SERVER_PORT: it's being ignored."); //$NON-NLS-1$ + } + return result; + } + +} diff --git a/org.tizen.common.sdblib/src/org/tizen/sdblib/SyncService.java b/org.tizen.common.sdblib/src/org/tizen/sdblib/SyncService.java new file mode 100644 index 0000000..9463cfa --- /dev/null +++ b/org.tizen.common.sdblib/src/org/tizen/sdblib/SyncService.java @@ -0,0 +1,1378 @@ +/* + * 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 org.tizen.sdblib; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +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 org.tizen.sdblib.FileListingService.FileEntry; +import org.tizen.sdblib.SdbHelper.SdbResponse; +import org.tizen.sdblib.SyncService.ISyncProgressMonitor; + + +/** + * Sync service class to push/pull to/from devices/emulators, through the debug bridge. + *

+ * To get a {@link SyncService} object, use {@link Device#getSyncService()}. + */ +public final class SyncService { + + private final static byte[] ID_OKAY = { 'O', 'K', 'A', 'Y' }; + private final static byte[] ID_FAIL = { 'F', 'A', 'I', 'L' }; + private final static byte[] ID_RECV = { 'R', 'E', 'C', 'V' }; + private final static byte[] ID_DATA = { 'D', 'A', 'T', 'A' }; + private final static byte[] ID_DONE = { 'D', 'O', 'N', 'E' }; + private final static byte[] ID_SEND = { 'S', 'E', 'N', 'D' }; + + private final static NullSyncProgressMonitor sNullSyncProgressMonitor = + new NullSyncProgressMonitor(); + + private final static int SYNC_DATA_MAX = 64*1024; + private final static int REMOTE_PATH_MAX_LENGTH = 1024; + + /** Result code for transfer success. */ + public static final int RESULT_OK = 0; + /** Result code for canceled transfer */ + public static final int RESULT_CANCELED = 1; + /** Result code for unknown error */ + public static final int RESULT_UNKNOWN_ERROR = 2; + /** Result code for network connection error */ + public static final int RESULT_CONNECTION_ERROR = 3; + /** Result code for unknown remote object during a pull */ + public static final int RESULT_NO_REMOTE_OBJECT = 4; + /** Result code when attempting to pull multiple files into a file */ + public static final int RESULT_TARGET_IS_FILE = 5; + /** Result code when attempting to pull multiple into a directory that does not exist. */ + public static final int RESULT_NO_DIR_TARGET = 6; + /** Result code for wrong encoding on the remote path. */ + public static final int RESULT_REMOTE_PATH_ENCODING = 7; + /** Result code for remote path that is too long. */ + public static final int RESULT_REMOTE_PATH_LENGTH = 8; + /** Result code for error while writing local file. */ + public static final int RESULT_FILE_WRITE_ERROR = 9; + /** Result code for error while reading local file. */ + public static final int RESULT_FILE_READ_ERROR = 10; + /** Result code for attempting to push a file that does not exist. */ + public static final int RESULT_NO_LOCAL_FILE = 11; + /** Result code for attempting to push a directory. */ + public static final int RESULT_LOCAL_IS_DIRECTORY = 12; + /** Result code for when the target path of a multi file push is a file. */ + public static final int RESULT_REMOTE_IS_FILE = 13; + /** Result code for receiving too much data from the remove device at once */ + public static final int RESULT_BUFFER_OVERRUN = 14; + /** Result code for network connection timeout */ + public static final int RESULT_CONNECTION_TIMEOUT = 15; + /** Result code for file exist */ + public static final int RESULT_FILE_EXIST = 16; + /** Result code for fail delete file */ + public static final int RESULT_FAIL_DELETE_FILE = 17; + /** Result code for fail delete file */ + public static final int RESULT_FAIL_CREATE_FILE = 18; + + /** + * A file transfer result. + *

+ * This contains a code, and an optional string + */ + public static class SyncResult { + private final int mCode; + private final String mMessage; + SyncResult(int code, String message) { + mCode = code; + mMessage = message; + } + + SyncResult(int code, Exception e) { + this(code, e.getMessage()); + } + + SyncResult(int code) { + this(code, errorCodeToString(code)); + } + + public int getCode() { + return mCode; + } + + public String getMessage() { + return mMessage; + } + } + + /** + * 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. + */ + public void start(int totalWork); + /** + * Sent when the transfer is finished or interrupted. + */ + public void stop(); + /** + * Sent to query for possible cancellation. + * @return true if the transfer should be stopped. + */ + public boolean isCanceled(); + /** + * Sent when a sub task is started. + * @param name the name of the sub task. + */ + public void startSubTask(String name); + /** + * Sent when some progress have been made. + * @param work the amount of work done. + */ + public void advance(int work); + } + + /** + * 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 final InetSocketAddress mAddress; + private final 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 sdb refuse the connection. This can happen + * if the {@link Device} is invalid. + * @throws TimeoutException in case of timeout on the connection. + * @throws sdbCommandRejectedException if sdb rejects the command + * @throws IOException If the connection to sdb failed. + */ + boolean openSync() throws TimeoutException, SdbCommandRejectedException, IOException { + try { + mChannel = SocketChannel.open(mAddress); + mChannel.configureBlocking(false); + + // target a specific device + SdbHelper.setDevice(mChannel, mDevice); + + byte[] request = SdbHelper.formSdbRequest("sync:"); // $NON-NLS-1$ + SdbHelper.write(mChannel, request, -1, SdbPreferences.getTimeOut()); + + SdbResponse resp = SdbHelper.readSdbResponse(mChannel, false /* readDiagString */); + + if (resp.okay == false) { + Log.w("sdb", "Got unhappy response from sdb 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}. + *

This object can be reused multiple times and can be used by concurrent threads. + */ + public static ISyncProgressMonitor getNullProgressMonitor() { + return sNullSyncProgressMonitor; + } + + /** + * Converts an error code into a non-localized string + * @param code the error code; + */ + private static String errorCodeToString(int code) { + switch (code) { + case RESULT_OK: + return "Success."; + case RESULT_CANCELED: + return "Transfer canceled by the user."; + case RESULT_UNKNOWN_ERROR: + return "Unknown Error."; + case RESULT_CONNECTION_ERROR: + return "sdb Connection Error."; + case RESULT_NO_REMOTE_OBJECT: + return "Remote object doesn't exist!"; + case RESULT_TARGET_IS_FILE: + return "Target object is a file."; + case RESULT_NO_DIR_TARGET: + return "Target directory doesn't exist."; + case RESULT_REMOTE_PATH_ENCODING: + return "Remote Path encoding is not supported."; + case RESULT_REMOTE_PATH_LENGTH: + return "Remove path is too long."; + case RESULT_FILE_WRITE_ERROR: + return "Writing local file failed!"; + case RESULT_FILE_READ_ERROR: + return "Reading local file failed!"; + case RESULT_NO_LOCAL_FILE: + return "Local file doesn't exist."; + case RESULT_LOCAL_IS_DIRECTORY: + return "Local path is a directory."; + case RESULT_REMOTE_IS_FILE: + return "Remote path is a file."; + case RESULT_BUFFER_OVERRUN: + return "Receiving too much data."; + case RESULT_CONNECTION_TIMEOUT: + return "timeout"; + case RESULT_FILE_EXIST: + return "File exists."; + case RESULT_FAIL_DELETE_FILE: + return "Fail delete file."; + case RESULT_FAIL_CREATE_FILE: + return "Fail create file."; + } + + throw new RuntimeException(); + } + + /** + * 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. + * @return a {@link SyncResult} object with a code and an optional message. + * + * @see FileListingService.FileEntry + * @see #getNullProgressMonitor() + */ + public SyncResult pull(FileEntry[] entries, String localPath, ISyncProgressMonitor monitor) { + + // first we check the destination is a directory and exists + File f = new File(localPath); + if (f.exists() == false) { + return new SyncResult(RESULT_NO_DIR_TARGET); + } + if (!f.isDirectory()) { + return new SyncResult(RESULT_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); + + SyncResult result = doPull(entries, localPath, fls, monitor); + + monitor.stop(); + + return result; + } + + /** + * 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. + * @param time interval while pulling the files + * @return a {@link SyncResult} object with a code and an optional message. + * + * @see FileListingService.FileEntry + * @see #getNullProgressMonitor() + */ + public SyncResult pull(FileEntry[] entries, String localPath, ISyncProgressMonitor monitor, int timeOut) { + + // first we check the destination is a directory and exists + File f = new File(localPath); + if (f.exists() == false) { + return new SyncResult(RESULT_NO_DIR_TARGET); + } + if (!f.isDirectory()) { + return new SyncResult(RESULT_TARGET_IS_FILE); + } + + // get a FileListingService object + FileListingService fls = new FileListingService(mDevice); + + // compute the number of file to move + long total = getTotalRemoteFileSizeLong(entries, fls); + TransferInfo transferInfo = new TransferInfo(); + + // start the monitor + monitor.start(transferInfo.downSizing(total)); + + SyncResult result = doPull(entries, localPath, fls, monitor, transferInfo, timeOut); + + monitor.stop(); + + return result; + } + + /** + * Pulls a single file. + * @param remote the remote file + * @param localFilename The local destination. + * @param monitor The progress monitor. Cannot be null. + * @return a {@link SyncResult} object with a code and an optional message. + * + * @see FileListingService.FileEntry + * @see #getNullProgressMonitor() + */ + public SyncResult pullFile(FileEntry remote, String localFilename, + ISyncProgressMonitor monitor) { + int total = remote.getSizeValue(); + monitor.start(total); + + SyncResult result = doPullFile(remote.getFullEscapedPath(), localFilename, monitor); + + monitor.stop(); + return result; + } + + /** + * Pulls a single file. + *

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. + * @return a {@link SyncResult} object with a code and an optional message. + * + * @see #getNullProgressMonitor() + */ + public SyncResult pullFile(String remoteFilepath, String localFilename, + ISyncProgressMonitor monitor) { + monitor.start(0); + //TODO: use the {@link FileListingService} to get the file size. + + SyncResult result = doPullFile(remoteFilepath, localFilename, monitor); + + monitor.stop(); + return result; + } + + /** + * Pulls a single file. + *

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 forceOverwrite this decides overwrite, if local file is already exist. + * @param monitor The progress monitor. Cannot be null. + * @return a {@link SyncResult} object with a code and an optional message. + * + * @see #getNullProgressMonitor() + */ + public SyncResult pullFile(String remoteFile, String localFile, boolean forceOverwrite, + ISyncProgressMonitor monitor) { + File f = new File(localFile); + if (f.exists() == true) { + if (f.isDirectory()) { + return new SyncResult(RESULT_LOCAL_IS_DIRECTORY); + } + + if (forceOverwrite == false) { + return new SyncResult(RESULT_FILE_EXIST); + } else { + boolean result = f.delete(); + if (result == false) { + return new SyncResult(RESULT_FAIL_DELETE_FILE); + } + } + } + + try { + String filePath = f.getAbsolutePath(); + String dirPath = filePath.substring(0, filePath.lastIndexOf(File.separator)); + File d = new File(dirPath); + if (d.exists() == false) { + d.mkdirs(); + } + boolean result = f.createNewFile(); + if (result == false) { + return new SyncResult(RESULT_FAIL_CREATE_FILE); + } + } catch (IOException e) { + return new SyncResult(RESULT_UNKNOWN_ERROR, e); + } + + monitor.start(0); + //TODO: use the {@link FileListingService} to get the file size. + + SyncResult result = doPullFile(remoteFile, localFile, monitor); + + monitor.stop(); + return result; + } + + + /** + * 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. + * @return a {@link SyncResult} object with a code and an optional message. + */ + public SyncResult push(String[] local, FileEntry remote, ISyncProgressMonitor monitor) { + if (!remote.isDirectory()) { + return new SyncResult(RESULT_REMOTE_IS_FILE); + } + + // make a list of File from the list of String + ArrayList files = new ArrayList(); + 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); + + SyncResult result = doPush(fileArray, remote.getFullPath(), monitor); + + monitor.stop(); + + return result; + } + + /** + * 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. + * @param time out value while push the files + * @return a {@link SyncResult} object with a code and an optional message. + */ + public SyncResult push(String[] local, FileEntry remote, ISyncProgressMonitor monitor, int timeOut) { + if (!remote.isDirectory()) { + return new SyncResult(RESULT_REMOTE_IS_FILE); + } + + // make a list of File from the list of String + ArrayList files = new ArrayList(); + 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()]); + long total = getTotalLocalFileSizeLong(fileArray); + TransferInfo sizeInfo = new TransferInfo(); + + monitor.start(sizeInfo.downSizing(total)); + + SyncResult result = doPush(fileArray, remote.getFullPath(), monitor, sizeInfo, timeOut); + + monitor.stop(); + + return result; + } + + /** + * Push a single file. + * @param local the local filepath. + * @param remote The remote filepath. + * @param monitor The progress monitor. Cannot be null. + * @return a {@link SyncResult} object with a code and an optional message. + */ + public SyncResult pushFile(String local, String remote, ISyncProgressMonitor monitor) { + File f = new File(local); + if (f.exists() == false) { + return new SyncResult(RESULT_NO_LOCAL_FILE); + } + + if (f.isDirectory()) { + return new SyncResult(RESULT_LOCAL_IS_DIRECTORY); + } + + monitor.start((int)f.length()); + + SyncResult result = doPushFile(local, remote, monitor); + + monitor.stop(); + + return result; + } + + /** + * 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 || type == FileListingService.TYPE_ROOT_DEVICE + || type == FileListingService.TYPE_ROOT_EMULATOR) { + // 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. + * @param entries + * @param fls + * @return long typed file size + */ + private long getTotalRemoteFileSizeLong(FileEntry[] entries, FileListingService fls) { + long count = 0; + for (FileEntry e : entries) { + int type = e.getType(); + if (type == FileListingService.TYPE_DIRECTORY || type == FileListingService.TYPE_ROOT_DEVICE + || type == FileListingService.TYPE_ROOT_EMULATOR) { + // get the children + FileEntry[] children = fls.getChildren(e, false, null); + count += getTotalRemoteFileSizeLong(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; + } + + /** + * 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 long typed file size + */ + private long getTotalLocalFileSizeLong(File[] files) { + long count = 0; + + for (File f : files) { + if (f.exists()) { + if (f.isDirectory()) { + return getTotalLocalFileSizeLong(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. + * @return a {@link SyncResult} object with a code and an optional message. + */ + private SyncResult doPull(FileEntry[] entries, String localPath, + FileListingService fileListingService, + ISyncProgressMonitor monitor) { + + for (FileEntry e : entries) { + // check if we're cancelled + if (monitor.isCanceled() == true) { + return new SyncResult(RESULT_CANCELED); + } + + // get type (we only pull directory and files for now) + monitor.startSubTask(e.getFullPath()); + String dest = localPath + File.separator + e.getName(); + SyncResult result; + if (e.isDirectory()) { + + // 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); + result = doPull(children, dest, fileListingService, monitor); + } else{ + result = doPullFile(e.getFullPath(), dest, monitor); + } + + if (result.mCode != RESULT_OK) { + return result; + }else if(e.isDirectory()) + { + monitor.advance(1); + } + } + + return new SyncResult(RESULT_OK); + } + + /** + * 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. + * @param size information + * @param time interval while pulling the files + * @return a {@link SyncResult} object with a code and an optional message. + */ + private SyncResult doPull(FileEntry[] entries, String localPath, + FileListingService fileListingService, + ISyncProgressMonitor monitor, + TransferInfo transferInfo, + int timeOut) { + + for (FileEntry e : entries) { + // check if we're cancelled + if (monitor.isCanceled() == true) { + return new SyncResult(RESULT_CANCELED); + } + + // get type (we only pull directory and files for now) +// monitor.startSubTask(e.getFullPath()); + transferInfo.setDest(e.getFullPath()); + String dest = localPath + File.separator + e.getName(); + SyncResult result; + if (e.isDirectory()) { + + // 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); + result = doPull(children, dest, fileListingService, monitor, transferInfo, timeOut); + } else{ + result = doPullFile(e.getFullPath(), dest, monitor, transferInfo, timeOut); + } + + if (result.mCode != RESULT_OK) { + return result; + }else if(e.isDirectory()) + { + monitor.advance(1); + } + } + + return new SyncResult(RESULT_OK); + } + + /** + * 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. + * @return a {@link SyncResult} object with a code and an optional message. + */ + private SyncResult doPullFile(String remotePath, String localPath, + ISyncProgressMonitor monitor) { + return doPullFile(remotePath, localPath, monitor, null, SdbPreferences.getTimeOut()); + } + + /** + * 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. + * @param size information + * @param time out value while pulling the file + * @return a {@link SyncResult} object with a code and an optional message. + */ + private SyncResult doPullFile(String remotePath, String localPath, + ISyncProgressMonitor monitor, + TransferInfo transferInfo, + int timeOut) { + + byte[] msg = null; + byte[] pullResult = new byte[8]; + if(transferInfo != null) + { + transferInfo.setSrc(localPath); + } + + try { + byte[] remotePathContent = remotePath.getBytes(SdbHelper.UTF_ENCODING); + + if (remotePathContent.length > REMOTE_PATH_MAX_LENGTH) { + return new SyncResult(RESULT_REMOTE_PATH_LENGTH); + } + + // create the full request message + msg = createFileReq(ID_RECV, remotePathContent); + + // and send it. + SdbHelper.write(mChannel, msg, -1, timeOut); + + // read the result, in a byte array containing 2 ints + // (id, size) + SdbHelper.read(mChannel, pullResult, -1, timeOut); + + // check we have the proper data back + if (checkResult(pullResult, ID_DATA) == false && + checkResult(pullResult, ID_DONE) == false) { + return new SyncResult(RESULT_CONNECTION_ERROR); + } + } catch (UnsupportedEncodingException e) { + return new SyncResult(RESULT_REMOTE_PATH_ENCODING, e); + } catch (TimeoutException e) { + return new SyncResult(RESULT_CONNECTION_TIMEOUT, e); + } catch (IOException e) { + return new SyncResult(RESULT_CONNECTION_ERROR, 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); + } catch (FileNotFoundException e) { + return new SyncResult(RESULT_FILE_WRITE_ERROR, e); + } + + // 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() == true) { + return new SyncResult(RESULT_CANCELED); + } + + // if we're done, we stop the loop + if (checkResult(pullResult, ID_DONE)) { + break; + } + if (checkResult(pullResult, ID_DATA) == false) { + // hmm there's an error + return new SyncResult(RESULT_CONNECTION_ERROR); + } + int length = ArrayHelper.swap32bitFromArray(pullResult, 4); + if (length > SYNC_DATA_MAX) { + // buffer overrun! + // error and exit + return new SyncResult(RESULT_BUFFER_OVERRUN); + } + + try { + // now read the length we received + SdbHelper.read(mChannel, data, length, timeOut); + + // get the header for the next packet. + SdbHelper.read(mChannel, pullResult, -1, timeOut); + } catch (TimeoutException e) { + return new SyncResult(RESULT_CONNECTION_TIMEOUT, e); + } catch (IOException e) { + return new SyncResult(RESULT_CONNECTION_ERROR, e); + } + + // write the content in the file + try { + fos.write(data, 0, length); + } catch (IOException e) { + return new SyncResult(RESULT_FILE_WRITE_ERROR, e); + } + + if(transferInfo != null) + { + transferInfo.monitorAdvance(length, monitor); + } + else + { + monitor.advance(length); + } + } + + try { + fos.flush(); + fos.close(); + } catch (IOException e) { + return new SyncResult(RESULT_FILE_WRITE_ERROR, e); + } + return new SyncResult(RESULT_OK); + } + + + /** + * Push multiple files + * @param fileArray + * @param remotePath + * @param monitor + * @return a {@link SyncResult} object with a code and an optional message. + */ + private SyncResult doPush(File[] fileArray, String remotePath, ISyncProgressMonitor monitor) { + for (File f : fileArray) { + // check if we're canceled + if (monitor.isCanceled() == true) { + return new SyncResult(RESULT_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); + SyncResult result = doPush(f.listFiles(), dest, monitor); + + if (result.mCode != RESULT_OK) { + return result; + } + + 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); + SyncResult result = doPushFile(f.getAbsolutePath(), remoteFile, monitor); + if (result.mCode != RESULT_OK) { + return result; + } + } + } + } + + return new SyncResult(RESULT_OK); + } + + /** + * Push multiple files + * @param fileArray + * @param remotePath + * @param monitor + * @param size information + * @param time out value while pushing the files + * @return a {@link SyncResult} object with a code and an optional message. + */ + private SyncResult doPush(File[] fileArray, String remotePath, ISyncProgressMonitor monitor, TransferInfo transferInfo, int timeOut) { + for (File f : fileArray) { + // check if we're canceled + if (monitor.isCanceled() == true) { + return new SyncResult(RESULT_CANCELED); + } + if (f.exists()) { + String remoteFile; + if("/".equals(remotePath)) + { + remoteFile = remotePath + f.getName(); + } + else + { + remoteFile = remotePath + "/" + f.getName(); + } + transferInfo.setDest(remoteFile); + + if (f.isDirectory()) { + // append the name of the directory to the remote path +// String dest = remotePath + "/" + f.getName(); // $NON-NLS-1S +// monitor.startSubTask(dest); + SyncResult result = doPush(f.listFiles(), remoteFile, monitor, transferInfo, timeOut); + + if (result.mCode != RESULT_OK) { + return result; + } + + 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); +// transferInfo.fileName = remoteFile; + SyncResult result = doPushFile(f.getAbsolutePath(), remoteFile, monitor, transferInfo, timeOut); + if (result.mCode != RESULT_OK) { + return result; + } + } + } + } + + return new SyncResult(RESULT_OK); + } + + /** + * 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. + * @return a {@link SyncResult} object with a code and an optional message. + */ + private SyncResult doPushFile(String localPath, String remotePath, + ISyncProgressMonitor monitor) { + return doPushFile( localPath, remotePath, monitor, null, SdbPreferences.getTimeOut()); + } + + /** + * Push a single file with permission 755 + * @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. + * @param size information + * @param time out value while pushing the file + * @return a {@link SyncResult} object with a code and an optional message. + */ + private SyncResult doPushFile(String localPath, String remotePath, + ISyncProgressMonitor monitor, + TransferInfo transferInfo, + int timeOut) { + remotePath = remotePath.replace("\\ ", " "); + if(transferInfo != null) + { + transferInfo.setSrc(localPath); + } + + FileInputStream fis = null; + byte[] msg; + + try { + byte[] remotePathContent = remotePath.getBytes(SdbHelper.UTF_ENCODING); + + if (remotePathContent.length > REMOTE_PATH_MAX_LENGTH) { + return new SyncResult(RESULT_REMOTE_PATH_LENGTH); + } + + File f = new File(localPath); + + // this shouldn't happen but still... + if (f.exists() == false) { + return new SyncResult(RESULT_NO_LOCAL_FILE); + } + + // create the stream to read the file + fis = new FileInputStream(f); + + // create the header for the action + msg = createSendFileReq(ID_SEND, remotePathContent, 0755); + } catch (UnsupportedEncodingException e) { + return new SyncResult(RESULT_REMOTE_PATH_ENCODING, e); + } catch (FileNotFoundException e) { + return new SyncResult(RESULT_FILE_READ_ERROR, e); + } + + // and send it. We use a custom try/catch block to make the difference between + // file and network IO exceptions. + try { + SdbHelper.write(mChannel, msg, -1, timeOut); + } catch (TimeoutException e) { + return new SyncResult(RESULT_CONNECTION_TIMEOUT, e); + } catch (IOException e) { + return new SyncResult(RESULT_CONNECTION_ERROR, e); + } + + // create the buffer used to read. + // we read max SYNC_DATA_MAX, but we need 2 4 bytes at the beginning. + if (mBuffer == null) { + mBuffer = new byte[SYNC_DATA_MAX + 8]; + } + System.arraycopy(ID_DATA, 0, mBuffer, 0, ID_DATA.length); + + // look while there is something to read + while (true) { + // check if we're canceled + if (monitor.isCanceled() == true) { + return new SyncResult(RESULT_CANCELED); + } + + // read up to SYNC_DATA_MAX + int readCount = 0; + try { + readCount = fis.read(mBuffer, 8, SYNC_DATA_MAX); + } catch (IOException e) { + return new SyncResult(RESULT_FILE_READ_ERROR, e); + } + + 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, mBuffer, 4); + + // now write it + try { + SdbHelper.write(mChannel, mBuffer, readCount+8, timeOut); + } catch (TimeoutException e) { + return new SyncResult(RESULT_CONNECTION_TIMEOUT, e); + } catch (IOException e) { + return new SyncResult(RESULT_CONNECTION_ERROR, e); + } + + if(transferInfo != null) + { + transferInfo.monitorAdvance(readCount, monitor); + } + else + { + monitor.advance(readCount); + } + } + // close the local file + try { + fis.close(); + } catch (IOException e) { + return new SyncResult(RESULT_FILE_READ_ERROR, e); + } + + try { + // create the DONE message + long time = System.currentTimeMillis() / 1000; + msg = createReq(ID_DONE, (int)time); + + // and send it. + SdbHelper.write(mChannel, msg, -1, timeOut); + + // read the result, in a byte array containing 2 ints + // (id, size) + byte[] result = new byte[8]; + SdbHelper.read(mChannel, result, -1 /* full length */, timeOut); + + if (checkResult(result, ID_OKAY) == false) { + if (checkResult(result, ID_FAIL)) { + // read some error message... + int len = ArrayHelper.swap32bitFromArray(result, 4); + + SdbHelper.read(mChannel, mBuffer, len, timeOut); + + // output the result? + String message = new String(mBuffer, 0, len); + Log.e("sdb", "transfer error: " + message); + return new SyncResult(RESULT_UNKNOWN_ERROR, message); + } + + return new SyncResult(RESULT_UNKNOWN_ERROR); + } + } catch (TimeoutException e) { + return new SyncResult(RESULT_CONNECTION_TIMEOUT, e); + } catch (IOException e) { + return new SyncResult(RESULT_CONNECTION_ERROR, e); + } + + return new SyncResult(RESULT_OK); + } + + /** + * 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 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 sdb + */ + 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(SdbHelper.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) { + if (result[0] != code[0] || + result[1] != code[1] || + result[2] != code[2] || + result[3] != code[3]) { + return false; + } + + return true; + + } + +} + +//to process size is bigger than integer maximum value +class TransferInfo{ + + private String from = null; + private String to = null; + //to show total file size and unit + private String total_sizeAndunit = null; + private int rate = 0; + private int remains = 0; + private long total_size = 0; + private long transfered_size = 0; + + TransferInfo(){} + + /** + * Getting size down until size is smaller than integer maximum size + * @param origin + * @return down size + */ + public int downSizing(long origin) + { + total_size = origin; + int times = 0; + while(origin > Integer.MAX_VALUE) + { + origin /= 10; + times++; + } + + rate = (int) Math.pow(10, times); + total_sizeAndunit = getSizeAndUnit(total_size); + return (int)origin; + } + + public void setDest(String dest) + { + to = dest; + } + + public void setSrc(String src) + { + from = src; + } + + public void monitorAdvance(int count, ISyncProgressMonitor monitor) + { + transfered_size += count; + + int size = remains + count ; + + if(size > rate) + { + monitor.advance(size/rate); + remains = size%rate; + } + else + { + remains = size; + } + + if(from != null) + { + monitor.startSubTask(String.format("From\t\"%s\"\tto\t\"%s\"\n( %s / %s ) ", from, to, getSizeAndUnit(transfered_size), total_sizeAndunit)); + } + } + + public String getSizeAndUnit(long size) + { + int index = 0; + int i = 0; + int short_size = 0; + double size_for_GB = 0; + + while(i < 3) + { + if(size >= 1000) + { + //case of b, kb, mb : xxx b/kb/mb + //case of gb : xxx.xx gb + if(index == 2) + { + size_for_GB = (double)size/1000.00; + }else + { + size/=1000; + } + + if(size > 0) + { + index++; + } + else + { + break; + } + i++; + } + else + { + break; + } + } + + short_size = (int)size % 1000; + + String unit = null; + + switch(index) + { + case 0: + unit = "B"; + break; + case 1: + unit = "KB"; + break; + case 2: + unit = "MB"; + break; + case 3: + unit = "GB"; + return String.format("%.2f %s", size_for_GB, unit); + } + + return String.format("%d %s", short_size, unit); + } +} \ No newline at end of file diff --git a/org.tizen.common.sdblib/src/org/tizen/sdblib/TimeoutException.java b/org.tizen.common.sdblib/src/org/tizen/sdblib/TimeoutException.java new file mode 100644 index 0000000..b0ae737 --- /dev/null +++ b/org.tizen.common.sdblib/src/org/tizen/sdblib/TimeoutException.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2010 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 org.tizen.sdblib; + +import java.io.IOException; + +/** + * Exception thrown when a connection to sdb failed with a timeout. + * + */ +public class TimeoutException extends IOException { + private static final long serialVersionUID = 1L; +} diff --git a/org.tizen.common.sdblib/test/lib/asm-4.0.jar b/org.tizen.common.sdblib/test/lib/asm-4.0.jar new file mode 100644 index 0000000..6d63075 Binary files /dev/null and b/org.tizen.common.sdblib/test/lib/asm-4.0.jar differ diff --git a/org.tizen.common.sdblib/test/lib/asm-tree-4.0.jar b/org.tizen.common.sdblib/test/lib/asm-tree-4.0.jar new file mode 100644 index 0000000..aa99d3a Binary files /dev/null and b/org.tizen.common.sdblib/test/lib/asm-tree-4.0.jar differ diff --git a/org.tizen.common.sdblib/test/lib/javassist-3.16.1-GA.jar b/org.tizen.common.sdblib/test/lib/javassist-3.16.1-GA.jar new file mode 100644 index 0000000..c29da0f Binary files /dev/null and b/org.tizen.common.sdblib/test/lib/javassist-3.16.1-GA.jar differ diff --git a/org.tizen.common.sdblib/test/lib/junit-4.10-src.jar b/org.tizen.common.sdblib/test/lib/junit-4.10-src.jar new file mode 100644 index 0000000..1449d28 Binary files /dev/null and b/org.tizen.common.sdblib/test/lib/junit-4.10-src.jar differ diff --git a/org.tizen.common.sdblib/test/lib/junit-4.10.jar b/org.tizen.common.sdblib/test/lib/junit-4.10.jar new file mode 100644 index 0000000..bf5c0b9 Binary files /dev/null and b/org.tizen.common.sdblib/test/lib/junit-4.10.jar differ diff --git a/org.tizen.common.sdblib/test/lib/mockito-all-1.9.0.jar b/org.tizen.common.sdblib/test/lib/mockito-all-1.9.0.jar new file mode 100644 index 0000000..273fd50 Binary files /dev/null and b/org.tizen.common.sdblib/test/lib/mockito-all-1.9.0.jar differ diff --git a/org.tizen.common.sdblib/test/lib/powermock-mockito-1.4.12-full.jar b/org.tizen.common.sdblib/test/lib/powermock-mockito-1.4.12-full.jar new file mode 100644 index 0000000..b70358d Binary files /dev/null and b/org.tizen.common.sdblib/test/lib/powermock-mockito-1.4.12-full.jar differ diff --git a/org.tizen.common.sdblib/test/src/org/tizen/sdblib/TransferInfoTest.java b/org.tizen.common.sdblib/test/src/org/tizen/sdblib/TransferInfoTest.java new file mode 100644 index 0000000..5182357 --- /dev/null +++ b/org.tizen.common.sdblib/test/src/org/tizen/sdblib/TransferInfoTest.java @@ -0,0 +1,30 @@ +package org.tizen.sdblib; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class TransferInfoTest +{ + @Test + public void test_getSizeAndUnit() + { + final Object[][] TEST_CASES = new Object[][] { + new Object[] { 100L, new String("100 B") }, + new Object[] { 1000L, new String("1 KB") }, + new Object[] { 10000L, new String("10 KB") }, + new Object[] { 100000L, new String("100 KB") }, + new Object[] { 1000000L, new String("1 MB") }, + new Object[] { 10000000L, new String("10 MB") }, + new Object[] { 100000000L, new String("100 MB") }, + new Object[] { 1000000000L, new String("1.00 GB") }, + }; + TransferInfo info = new TransferInfo(); + for ( final Object[] TEST_CASE : TEST_CASES ) + { + final long input = (Long) TEST_CASE[0]; + final String expected = (String) TEST_CASE[1]; + assertEquals( expected, info.getSizeAndUnit(input)); + } + } +} diff --git a/org.tizen.common/.classpath b/org.tizen.common/.classpath index 3657b0f..13f5c5b 100755 --- a/org.tizen.common/.classpath +++ b/org.tizen.common/.classpath @@ -1,25 +1,24 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.tizen.common/META-INF/MANIFEST.MF b/org.tizen.common/META-INF/MANIFEST.MF index 845a526..501d4ca 100755 --- a/org.tizen.common/META-INF/MANIFEST.MF +++ b/org.tizen.common/META-INF/MANIFEST.MF @@ -194,17 +194,16 @@ Export-Package: org.tizen.common.util.log, org.tizen.common.util.url.classpath, org.tizen.common.util.url.cp, - org.tizen.common.util.url.vf, - org.tizen.sdblib + org.tizen.common.util.url.vf Bundle-RequiredExecutionEnvironment: JavaSE-1.6 Import-Package: org.eclipse.jface.text, - org.eclipse.ui.console + org.eclipse.ui.console, + org.tizen.sdblib Bundle-Activator: org.tizen.common.CommonPlugin Bundle-ActivationPolicy: lazy Bundle-ClassPath: ., lib/log4j-1.2.17.jar, - lib/sdblib.jar, lib/slf4j-api-1.6.4.jar, lib/slf4j-log4j12-1.6.4.jar, lib/freemarker.jar, diff --git a/org.tizen.common/lib/sdblib.jar b/org.tizen.common/lib/sdblib.jar deleted file mode 100644 index d40c7c7..0000000 Binary files a/org.tizen.common/lib/sdblib.jar and /dev/null differ diff --git a/package/build.linux b/package/build.linux index 16f6596..2d1d9e7 100755 --- a/package/build.linux +++ b/package/build.linux @@ -58,11 +58,27 @@ install() if [ "x${TARGET_OS}" = "x${platform}" ] then - __set_parameter - INSTALL_DIR=${SRCDIR}/package/${package_name}.package.${TARGET_OS}/data - mkdir -p ${INSTALL_DIR} - __copy_necessary_binaries - ${build_script_path}/install.sh ${package_name} + + if [ "x${package_name}" = "xcommon-eplugin" ] + then + INSTALL_DIR=${SRCDIR}/package/${package_name}.package.${TARGET_OS}/data + mkdir -p ${INSTALL_DIR} + __set_parameter + __copy_necessary_binaries + ${build_script_path}/install.sh ${package_name} + sdblib_path="${INSTALL_DIR}/ide/plugins" + echo "sdblib path : ${sdblib_path}" + sdblib_name=`basename $(ls ${sdblib_path}/org.tizen.common.sdblib*.jar)` + echo "sdblib name : ${sdblib_name}" + elif [ "x${package_name}" = "xsdblib" ] + then + if [ -e ${sdblib_path}/${sdblib_name} ] + then + INSTALL_DIR=${SRCDIR}/package/${package_name}.package.${TARGET_OS}/data/tools/ide/lib + mkdir -p ${INSTALL_DIR} + cp ${sdblib_path}/${sdblib_name} ${INSTALL_DIR}/${sdblib_name} + fi + fi else echo "" fi diff --git a/package/pkginfo.manifest b/package/pkginfo.manifest index e631116..a9a1d70 100644 --- a/package/pkginfo.manifest +++ b/package/pkginfo.manifest @@ -37,3 +37,28 @@ Build-dependency:base-ide-product [macos-64], pde-build [macos-64] Install-dependency:base-ide-product [macos-64] Description:Common plugin +Package:sdblib +OS:ubuntu-32 +Build-host-os:ubuntu-32, ubuntu-64 +Description:sdblib + +Package:sdblib +OS:ubuntu-64 +Build-host-os:ubuntu-32, ubuntu-64 +Description:sdblib + +Package:sdblib +OS:windows-32 +Build-host-os:ubuntu-32, ubuntu-64 +Description:sdblib + +Package:sdblib +OS:windows-64 +Build-host-os:ubuntu-32, ubuntu-64 +Description:sdblib + +Package:sdblib +OS:macos-64 +Build-host-os:ubuntu-32, ubuntu-64 +Description:sdblib +