blob: e4556334638e035b128544b09e9cd824bf1b60f5 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.idea.sdk.remote.internal.updater;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.sdklib.SdkManager;
import com.android.tools.idea.sdk.remote.internal.*;
import com.android.tools.idea.sdk.remote.internal.sources.SdkRepoConstants;
import com.android.utils.ILogger;
import com.android.utils.IReaderLogger;
import com.android.utils.Pair;
import java.io.IOException;
import java.util.ArrayList;
/**
* Performs an update using only a non-interactive console output with no GUI.
*/
public class SdkUpdaterNoWindow {
/**
* The {@link UpdaterData} to use.
*/
private final UpdaterData mUpdaterData;
/**
* The {@link ILogger} logger to use.
*/
private final ILogger mSdkLog;
/**
* The reply to any question asked by the update process. Currently this will
* be yes/no for ability to replace modified samples or restart ADB.
*/
private final boolean mForce;
/**
* Creates an UpdateNoWindow object that will update using the given SDK root
* and outputs to the given SDK logger.
*
* @param osSdkRoot The OS path of the SDK folder to update.
* @param sdkManager An existing SDK manager to list current platforms and addons.
* @param sdkLog A logger object, that should ideally output to a write-only console.
* @param force The reply to any question asked by the update process. Currently this will
* be yes/no for ability to replace modified samples or restart ADB.
* @param proxyPort An optional HTTP/HTTPS proxy port. Can be null.
* @param proxyHost An optional HTTP/HTTPS proxy host. Can be null.
*/
public SdkUpdaterNoWindow(String osSdkRoot,
SdkManager sdkManager,
ILogger sdkLog,
boolean force,
String proxyHost,
String proxyPort) {
mSdkLog = sdkLog;
mForce = force;
mUpdaterData = new UpdaterData(osSdkRoot, sdkLog);
// Use a factory that only outputs to the given ILogger.
mUpdaterData.setTaskFactory(new ConsoleTaskFactory());
// Setup the default sources including the getenv overrides.
mUpdaterData.setupDefaultSources();
}
/**
* Performs the actual update.
*
* @param pkgFilter A list of {@link SdkRepoConstants#NODES} to limit the type of packages
* we can update. A null or empty list means to update everything possible.
* @param includeAll True to list and install all packages, including obsolete ones.
* @param dryMode True to check what would be updated/installed but do not actually
* download or install anything.
* @param acceptLicense SDK licenses to automatically accept.
* @param includeDependencies If true, also include any required dependencies
*/
public void updateAll(ArrayList<String> pkgFilter,
boolean includeAll,
boolean dryMode,
String acceptLicense,
boolean includeDependencies) {
mUpdaterData.updateOrInstallAll_NoGUI(pkgFilter, includeAll, dryMode, acceptLicense, includeDependencies);
}
// -----
/**
* A custom implementation of {@link ITaskFactory} that
* provides {@link ConsoleTaskMonitor} objects.
*/
private class ConsoleTaskFactory implements ITaskFactory {
@Override
public void start(String title, ITask task) {
start(title, null /*parentMonitor*/, task);
}
@Override
public void start(String title, ITaskMonitor parentMonitor, ITask task) {
if (parentMonitor == null) {
task.run(new ConsoleTaskMonitor(title, task));
}
else {
// Use all the reminder of the parent monitor.
if (parentMonitor.getProgressMax() == 0) {
parentMonitor.setProgressMax(1);
}
ITaskMonitor sub = parentMonitor.createSubMonitor(parentMonitor.getProgressMax() - parentMonitor.getProgress());
try {
task.run(sub);
}
finally {
int delta = sub.getProgressMax() - sub.getProgress();
if (delta > 0) {
sub.incProgress(delta);
}
}
}
}
}
/**
* A custom implementation of {@link ITaskMonitor} that defers all output to the
* super {@link SdkUpdaterNoWindow#mSdkLog}.
*/
private class ConsoleTaskMonitor implements ITaskMonitor {
private static final double MAX_COUNT = 10000.0;
private double mIncCoef = 0;
private double mValue = 0;
private String mLastDesc = null;
private String mLastProgressBase = null;
/**
* Creates a new {@link ConsoleTaskMonitor} with the given title.
*/
public ConsoleTaskMonitor(String title, ITask task) {
mSdkLog.info("%s:\n", title);
}
/**
* Sets the description in the current task dialog.
*/
@Override
public void setDescription(String format, Object... args) {
String last = mLastDesc;
String line = String.format(" " + format, args); //$NON-NLS-1$
// If the description contains a %, it generally indicates a recurring
// progress so we want a \r at the end.
int pos = line.indexOf('%');
if (pos > -1) {
String base = line.trim();
if (mLastProgressBase != null && base.startsWith(mLastProgressBase)) {
line = " " + base.substring(mLastProgressBase.length()); //$NON-NLS-1$
}
line += '\r';
}
else {
mLastProgressBase = line.trim();
line += '\n';
}
// Skip line if it's the same as the last one.
if (last != null && last.equals(line.trim())) {
return;
}
mLastDesc = line.trim();
// If the last line terminated with a \r but the new one doesn't, we need to
// insert a \n to avoid erasing the previous line.
if (last != null &&
last.endsWith("\r") && //$NON-NLS-1$
!line.endsWith("\r")) { //$NON-NLS-1$
line = '\n' + line;
}
mSdkLog.info("%s", line); //$NON-NLS-1$
}
@Override
public void log(String format, Object... args) {
setDescription(" " + format, args); //$NON-NLS-1$
}
@Override
public void logError(String format, Object... args) {
setDescription(format, args);
}
@Override
public void logVerbose(String format, Object... args) {
// The ConsoleTask does not display verbose log messages.
}
// --- ILogger ---
@Override
public void error(@Nullable Throwable t, @Nullable String errorFormat, Object... args) {
mSdkLog.error(t, errorFormat, args);
}
@Override
public void warning(@NonNull String warningFormat, Object... args) {
mSdkLog.warning(warningFormat, args);
}
@Override
public void info(@NonNull String msgFormat, Object... args) {
mSdkLog.info(msgFormat, args);
}
@Override
public void verbose(@NonNull String msgFormat, Object... args) {
mSdkLog.verbose(msgFormat, args);
}
/**
* Sets the max value of the progress bar.
* <p/>
* Weird things will happen if setProgressMax is called multiple times
* *after* {@link #incProgress(int)}: we don't try to adjust it on the
* fly.
*/
@Override
public void setProgressMax(int max) {
assert max > 0;
// Always set the dialog's progress max to 10k since it only handles
// integers and we want to have a better inner granularity. Instead
// we use the max to compute a coefficient for inc deltas.
mIncCoef = max > 0 ? MAX_COUNT / max : 0;
assert mIncCoef > 0;
}
@Override
public int getProgressMax() {
return mIncCoef > 0 ? (int)(MAX_COUNT / mIncCoef) : 0;
}
/**
* Increments the current value of the progress bar.
*/
@Override
public void incProgress(int delta) {
if (delta > 0 && mIncCoef > 0) {
internalIncProgress(delta * mIncCoef);
}
}
private void internalIncProgress(double realDelta) {
mValue += realDelta;
// max value is 10k, so 10k/100 == 100%.
// Experimentation shows that it is not really useful to display this
// progression since during download the description line will change.
// mSdkLog.printf(" [%3d%%]\r", ((int)mValue) / 100);
}
/**
* Returns the current value of the progress bar,
* between 0 and up to {@link #setProgressMax(int)} - 1.
*/
@Override
public int getProgress() {
assert mIncCoef > 0;
return mIncCoef > 0 ? (int)(mValue / mIncCoef) : 0;
}
/**
* Returns true if the "Cancel" button was selected.
*/
@Override
public boolean isCancelRequested() {
return false;
}
/**
* Display a yes/no question dialog box.
* <p/>
* This implementation allow this to be called from any thread, it
* makes sure the dialog is opened synchronously in the ui thread.
*
* @param title The title of the dialog box
* @param message The error message
* @return true if YES was clicked.
*/
@Override
public boolean displayPrompt(final String title, final String message) {
// TODO Make it interactive if mForce==false
mSdkLog.info("\n%1$s\n%2$s\n%3$s", //$NON-NLS-1$
title, message, mForce ? "--force used, will reply yes\n" : "Note: you can use --force to override to yes.\n");
if (mForce) {
return true;
}
while (true) {
mSdkLog.info("%1$s", "[y/n] =>"); //$NON-NLS-1$
try {
byte[] readBuffer = new byte[2048];
String reply = readLine(readBuffer).trim();
mSdkLog.info("\n"); //$NON-NLS-1$
if (reply.length() > 0 && reply.length() <= 3) {
char c = reply.charAt(0);
if (c == 'y' || c == 'Y') {
return true;
}
else if (c == 'n' || c == 'N') {
return false;
}
}
mSdkLog.info("Unknown reply '%s'. Please use y[es]/n[o].\n"); //$NON-NLS-1$
}
catch (IOException e) {
// Exception. Be conservative and say no.
mSdkLog.info("\n"); //$NON-NLS-1$
return false;
}
}
}
/**
* Displays a prompt message to the user and read two values,
* login/password.
* <p/>
* <i>Asks user for login/password information.</i>
* <p/>
* This method shows a question in the standard output, asking for login
* and password.</br>
* <b>Method Output:</b></br>
* Title</br>
* Message</br>
* Login: (Wait for user input)</br>
* Password: (Wait for user input)</br>
* <p/>
*
* @param title The title of the iteration.
* @param message The message to be displayed.
* @return A {@link Pair} holding the entered login and password. The
* <b>first element</b> is always the <b>Login</b>, and the
* <b>second element</b> is always the <b>Password</b>. This
* method will never return null, in case of error the pair will
* be filled with empty strings.
* @see ITaskMonitor#displayLoginCredentialsPrompt(String, String)
*/
@Override
public UserCredentials displayLoginCredentialsPrompt(String title, String message) {
String login = ""; //$NON-NLS-1$
String password = ""; //$NON-NLS-1$
String workstation = ""; //$NON-NLS-1$
String domain = ""; //$NON-NLS-1$
mSdkLog.info("\n%1$s\n%2$s", title, message);
byte[] readBuffer = new byte[2048];
try {
mSdkLog.info("\nLogin: ");
login = readLine(readBuffer);
mSdkLog.info("\nPassword: ");
password = readLine(readBuffer);
mSdkLog.info("\nIf your proxy uses NTLM authentication, provide the following information. Leave blank otherwise.");
mSdkLog.info("\nWorkstation: ");
workstation = readLine(readBuffer);
mSdkLog.info("\nDomain: ");
domain = readLine(readBuffer);
/*
* TODO: Implement a way to don't echo the typed password On
* Java 5 there's no simple way to do this. There's just a
* workaround which is output backspaces on each keystroke.
* A good alternative is to use Java 6 java.io.Console
*/
}
catch (IOException e) {
// Reset login/pass to empty Strings.
login = ""; //$NON-NLS-1$
password = ""; //$NON-NLS-1$
workstation = ""; //$NON-NLS-1$
domain = ""; //$NON-NLS-1$
//Just print the error to console.
mSdkLog.info("\nError occurred during login/pass query: %s\n", e.getMessage());
}
return new UserCredentials(login, password, workstation, domain);
}
/**
* Reads current console input in the given buffer.
*
* @param buffer Buffer to hold the user input. Must be larger than the largest
* expected input. Cannot be null.
* @return A new string. May be empty but not null.
* @throws IOException in case the buffer isn't long enough.
*/
private String readLine(byte[] buffer) throws IOException {
int count;
if (mSdkLog instanceof IReaderLogger) {
count = ((IReaderLogger)mSdkLog).readLine(buffer);
}
else {
count = System.in.read(buffer);
}
// is the input longer than the buffer?
if (count == buffer.length && buffer[count - 1] != 10) {
throw new IOException(String.format("Input is longer than the buffer size, (%1$s) bytes", buffer.length));
}
// ignore end whitespace
while (count > 0 && (buffer[count - 1] == '\r' || buffer[count - 1] == '\n')) {
count--;
}
return new String(buffer, 0, count);
}
/**
* Creates a sub-monitor that will use up to tickCount on the progress bar.
* tickCount must be 1 or more.
*/
@Override
public ITaskMonitor createSubMonitor(int tickCount) {
assert mIncCoef > 0;
assert tickCount > 0;
return new ConsoleSubTaskMonitor(this, null, mValue, tickCount * mIncCoef);
}
}
private interface IConsoleSubTaskMonitor extends ITaskMonitor {
void subIncProgress(double realDelta);
}
private static class ConsoleSubTaskMonitor implements IConsoleSubTaskMonitor {
private final ConsoleTaskMonitor mRoot;
private final IConsoleSubTaskMonitor mParent;
private final double mStart;
private final double mSpan;
private double mSubValue;
private double mSubCoef;
/**
* Creates a new sub task monitor which will work for the given range [start, start+span]
* in its parent.
*
* @param root The ProgressTask root
* @param parent The immediate parent. Can be the null or another sub task monitor.
* @param start The start value in the root's coordinates
* @param span The span value in the root's coordinates
*/
public ConsoleSubTaskMonitor(ConsoleTaskMonitor root, IConsoleSubTaskMonitor parent, double start, double span) {
mRoot = root;
mParent = parent;
mStart = start;
mSpan = span;
mSubValue = start;
}
@Override
public boolean isCancelRequested() {
return mRoot.isCancelRequested();
}
@Override
public void setDescription(String format, Object... args) {
mRoot.setDescription(format, args);
}
@Override
public void log(String format, Object... args) {
mRoot.log(format, args);
}
@Override
public void logError(String format, Object... args) {
mRoot.logError(format, args);
}
@Override
public void logVerbose(String format, Object... args) {
mRoot.logVerbose(format, args);
}
@Override
public void setProgressMax(int max) {
assert max > 0;
mSubCoef = max > 0 ? mSpan / max : 0;
assert mSubCoef > 0;
}
@Override
public int getProgressMax() {
return mSubCoef > 0 ? (int)(mSpan / mSubCoef) : 0;
}
@Override
public int getProgress() {
assert mSubCoef > 0;
return mSubCoef > 0 ? (int)((mSubValue - mStart) / mSubCoef) : 0;
}
@Override
public void incProgress(int delta) {
if (delta > 0 && mSubCoef > 0) {
subIncProgress(delta * mSubCoef);
}
}
@Override
public void subIncProgress(double realDelta) {
mSubValue += realDelta;
if (mParent != null) {
mParent.subIncProgress(realDelta);
}
else {
mRoot.internalIncProgress(realDelta);
}
}
@Override
public boolean displayPrompt(String title, String message) {
return mRoot.displayPrompt(title, message);
}
@Override
public UserCredentials displayLoginCredentialsPrompt(String title, String message) {
return mRoot.displayLoginCredentialsPrompt(title, message);
}
@Override
public ITaskMonitor createSubMonitor(int tickCount) {
assert mSubCoef > 0;
assert tickCount > 0;
return new ConsoleSubTaskMonitor(mRoot, this, mSubValue, tickCount * mSubCoef);
}
// --- ILogger ---
@Override
public void error(@Nullable Throwable t, @Nullable String errorFormat, Object... args) {
mRoot.error(t, errorFormat, args);
}
@Override
public void warning(@NonNull String warningFormat, Object... args) {
mRoot.warning(warningFormat, args);
}
@Override
public void info(@NonNull String msgFormat, Object... args) {
mRoot.info(msgFormat, args);
}
@Override
public void verbose(@NonNull String msgFormat, Object... args) {
mRoot.verbose(msgFormat, args);
}
}
}