Plugins: Add status bar on download (#18695)

As some plugins are becoming big now, it is hard for the user to know, if the plugin
is being downloaded or just nothing happens.

This commit adds a progress bar during download, which can be disabled by using the `-q`
parameter.

In addition this updates to jimfs 1.1, which allows us to test the batch mode, as adding
security policies are now supported due to having jimfs:// protocol support in URL stream
handlers.
This commit is contained in:
Alexander Reelsen 2016-06-29 16:44:12 +02:00 committed by GitHub
parent 6d5666553c
commit 56fa751928
8 changed files with 315 additions and 31 deletions

View File

@ -19,6 +19,8 @@
package org.elasticsearch.cli; package org.elasticsearch.cli;
import org.elasticsearch.common.SuppressForbidden;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.Console; import java.io.Console;
import java.io.IOException; import java.io.IOException;
@ -26,8 +28,6 @@ import java.io.InputStreamReader;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import org.elasticsearch.common.SuppressForbidden;
/** /**
* A Terminal wraps access to reading input and writing output for a cli. * A Terminal wraps access to reading input and writing output for a cli.
* *
@ -81,8 +81,13 @@ public abstract class Terminal {
/** Prints a line to the terminal at {@code verbosity} level. */ /** Prints a line to the terminal at {@code verbosity} level. */
public final void println(Verbosity verbosity, String msg) { public final void println(Verbosity verbosity, String msg) {
print(verbosity, msg + lineSeparator);
}
/** Prints message to the terminal at {@code verbosity} level, without a newline. */
public final void print(Verbosity verbosity, String msg) {
if (this.verbosity.ordinal() >= verbosity.ordinal()) { if (this.verbosity.ordinal() >= verbosity.ordinal()) {
getWriter().print(msg + lineSeparator); getWriter().print(msg);
getWriter().flush(); getWriter().flush();
} }
} }

View File

@ -19,12 +19,31 @@
package org.elasticsearch.plugins; package org.elasticsearch.plugins;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import org.apache.lucene.search.spell.LevensteinDistance;
import org.apache.lucene.util.CollectionUtil;
import org.apache.lucene.util.IOUtils;
import org.elasticsearch.Version;
import org.elasticsearch.bootstrap.JarHell;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.SettingCommand;
import org.elasticsearch.cli.Terminal;
import org.elasticsearch.cli.UserError;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.hash.MessageDigests;
import org.elasticsearch.common.io.FileSystemUtils;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.node.internal.InternalSettingsPreparer;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.URL; import java.net.URL;
import java.net.URLConnection;
import java.net.URLDecoder; import java.net.URLDecoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream; import java.nio.file.DirectoryStream;
@ -49,24 +68,6 @@ import java.util.stream.Collectors;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream; import java.util.zip.ZipInputStream;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import org.apache.lucene.search.spell.LevensteinDistance;
import org.apache.lucene.util.CollectionUtil;
import org.apache.lucene.util.IOUtils;
import org.elasticsearch.Version;
import org.elasticsearch.bootstrap.JarHell;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.SettingCommand;
import org.elasticsearch.cli.Terminal;
import org.elasticsearch.cli.UserError;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.hash.MessageDigests;
import org.elasticsearch.common.io.FileSystemUtils;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.node.internal.InternalSettingsPreparer;
import static org.elasticsearch.cli.Terminal.Verbosity.VERBOSE; import static org.elasticsearch.cli.Terminal.Verbosity.VERBOSE;
/** /**
@ -141,6 +142,7 @@ class InstallPluginCommand extends SettingCommand {
private final OptionSpec<Void> batchOption; private final OptionSpec<Void> batchOption;
private final OptionSpec<String> arguments; private final OptionSpec<String> arguments;
public static final Set<PosixFilePermission> DIR_AND_EXECUTABLE_PERMS; public static final Set<PosixFilePermission> DIR_AND_EXECUTABLE_PERMS;
public static final Set<PosixFilePermission> FILE_PERMS; public static final Set<PosixFilePermission> FILE_PERMS;
@ -273,13 +275,49 @@ class InstallPluginCommand extends SettingCommand {
terminal.println(VERBOSE, "Retrieving zip from " + urlString); terminal.println(VERBOSE, "Retrieving zip from " + urlString);
URL url = new URL(urlString); URL url = new URL(urlString);
Path zip = Files.createTempFile(tmpDir, null, ".zip"); Path zip = Files.createTempFile(tmpDir, null, ".zip");
try (InputStream in = url.openStream()) { URLConnection urlConnection = url.openConnection();
int contentLength = urlConnection.getContentLength();
try (InputStream in = new TerminalProgressInputStream(urlConnection.getInputStream(), contentLength, terminal)) {
// must overwrite since creating the temp file above actually created the file // must overwrite since creating the temp file above actually created the file
Files.copy(in, zip, StandardCopyOption.REPLACE_EXISTING); Files.copy(in, zip, StandardCopyOption.REPLACE_EXISTING);
} }
return zip; return zip;
} }
/**
* content length might be -1 for unknown and progress only makes sense if the content length is greater than 0
*/
private class TerminalProgressInputStream extends ProgressInputStream {
private final Terminal terminal;
private int width = 50;
private final boolean enabled;
public TerminalProgressInputStream(InputStream is, int expectedTotalSize, Terminal terminal) {
super(is, expectedTotalSize);
this.terminal = terminal;
this.enabled = expectedTotalSize > 0;
}
@Override
public void onProgress(int percent) {
if (enabled) {
int currentPosition = percent * width / 100;
StringBuilder sb = new StringBuilder("\r[");
sb.append(String.join("=", Collections.nCopies(currentPosition, "")));
if (currentPosition > 0 && percent < 100) {
sb.append(">");
}
sb.append(String.join(" ", Collections.nCopies(width - currentPosition, "")));
sb.append("] %s   ");
if (percent == 100) {
sb.append("\n");
}
terminal.print(Terminal.Verbosity.NORMAL, String.format(Locale.ROOT, sb.toString(), percent + "%"));
}
}
}
/** Downloads a zip from the url, as well as a SHA1 checksum, and checks the checksum. */ /** Downloads a zip from the url, as well as a SHA1 checksum, and checks the checksum. */
private Path downloadZipAndChecksum(Terminal terminal, String urlString, Path tmpDir) throws Exception { private Path downloadZipAndChecksum(Terminal terminal, String urlString, Path tmpDir) throws Exception {
Path zip = downloadZip(terminal, urlString, tmpDir); Path zip = downloadZip(terminal, urlString, tmpDir);

View File

@ -26,8 +26,6 @@ import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment; import org.elasticsearch.env.Environment;
import org.elasticsearch.node.internal.InternalSettingsPreparer; import org.elasticsearch.node.internal.InternalSettingsPreparer;
import java.util.Collections;
/** /**
* A cli tool for adding, removing and listing plugins for elasticsearch. * A cli tool for adding, removing and listing plugins for elasticsearch.
*/ */

View File

@ -0,0 +1,83 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you 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.elasticsearch.plugins;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* An input stream that allows to add a listener to monitor progress
* The listener is triggered whenever a full percent is increased
* The listener is never triggered twice on the same percentage
* The listener will always return 99 percent, if the expectedTotalSize is exceeded, until it is finished
*
* Only used by the InstallPluginCommand, thus package private here
*/
abstract class ProgressInputStream extends FilterInputStream {
private final int expectedTotalSize;
private int currentPercent;
private int count = 0;
public ProgressInputStream(InputStream is, int expectedTotalSize) {
super(is);
this.expectedTotalSize = expectedTotalSize;
this.currentPercent = 0;
}
@Override
public int read() throws IOException {
int read = in.read();
checkProgress(read == -1 ? -1 : 1);
return read;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int byteCount = super.read(b, off, len);
checkProgress(byteCount);
return byteCount;
}
@Override
public int read(byte b[]) throws IOException {
return read(b, 0, b.length);
}
void checkProgress(int byteCount) {
// are we done?
if (byteCount == -1) {
currentPercent = 100;
onProgress(currentPercent);
} else {
count += byteCount;
// rounding up to 100% would mean we say we are done, before we are...
// this also catches issues, when expectedTotalSize was guessed wrong
int percent = Math.min(99, (int) Math.floor(100.0*count/expectedTotalSize));
if (percent > currentPercent) {
currentPercent = percent;
onProgress(percent);
}
}
}
public void onProgress(int percent) {}
}

View File

@ -0,0 +1,116 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you 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.elasticsearch.plugins;
import org.elasticsearch.test.ESTestCase;
import java.util.ArrayList;
import java.util.List;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.hasSize;
public class ProgressInputStreamTests extends ESTestCase {
private List<Integer> progresses = new ArrayList<>();
public void testThatProgressListenerIsCalled() throws Exception {
ProgressInputStream is = newProgressInputStream(0);
is.checkProgress(-1);
assertThat(progresses, hasSize(1));
assertThat(progresses, hasItems(100));
}
public void testThatProgressListenerIsCalledOnUnexpectedCompletion() throws Exception {
ProgressInputStream is = newProgressInputStream(2);
is.checkProgress(-1);
assertThat(progresses, hasItems(100));
}
public void testThatProgressListenerReturnsMaxValueOnWrongExpectedSize() throws Exception {
ProgressInputStream is = newProgressInputStream(2);
is.checkProgress(1);
assertThat(progresses, hasItems(50));
is.checkProgress(3);
assertThat(progresses, hasItems(50, 99));
is.checkProgress(-1);
assertThat(progresses, hasItems(50, 99, 100));
}
public void testOneByte() throws Exception {
ProgressInputStream is = newProgressInputStream(1);
is.checkProgress(1);
is.checkProgress(-1);
assertThat(progresses, hasItems(99, 100));
}
public void testOddBytes() throws Exception {
int odd = (randomIntBetween(100, 200) / 2) + 1;
ProgressInputStream is = newProgressInputStream(odd);
for (int i = 0; i < odd; i++) {
is.checkProgress(1);
}
is.checkProgress(-1);
assertThat(progresses, hasSize(odd+1));
assertThat(progresses, hasItem(100));
}
public void testEvenBytes() throws Exception {
int even = (randomIntBetween(100, 200) / 2);
ProgressInputStream is = newProgressInputStream(even);
for (int i = 0; i < even; i++) {
is.checkProgress(1);
}
is.checkProgress(-1);
assertThat(progresses, hasSize(even+1));
assertThat(progresses, hasItem(100));
}
public void testOnProgressCannotBeCalledMoreThanOncePerPercent() throws Exception {
int count = randomIntBetween(150, 300);
ProgressInputStream is = newProgressInputStream(count);
for (int i = 0; i < count; i++) {
is.checkProgress(1);
}
is.checkProgress(-1);
assertThat(progresses, hasSize(100));
}
private ProgressInputStream newProgressInputStream(int expectedSize) {
return new ProgressInputStream(null, expectedSize) {
@Override
public void onProgress(int percent) {
progresses.add(percent);
}
};
}
}

View File

@ -51,7 +51,7 @@ sudo bin/elasticsearch-plugin install analysis-icu
----------------------------------- -----------------------------------
This command will install the version of the plugin that matches your This command will install the version of the plugin that matches your
Elasticsearch version. Elasticsearch version and also show a progress bar while downloading.
[float] [float]
=== Custom URL or file system === Custom URL or file system
@ -117,8 +117,8 @@ The `plugin` scripts supports a number of other command line parameters:
=== Silent/Verbose mode === Silent/Verbose mode
The `--verbose` parameter outputs more debug information, while the `--silent` The `--verbose` parameter outputs more debug information, while the `--silent`
parameter turns off all output. The script may return the following exit parameter turns off all output including the progress bar. The script may
codes: return the following exit codes:
[horizontal] [horizontal]
`0`:: everything was OK `0`:: everything was OK

View File

@ -26,7 +26,7 @@
apply plugin: 'elasticsearch.standalone-test' apply plugin: 'elasticsearch.standalone-test'
dependencies { dependencies {
testCompile 'com.google.jimfs:jimfs:1.0' testCompile 'com.google.jimfs:jimfs:1.1'
} }
// TODO: give each evil test its own fresh JVM for more isolation. // TODO: give each evil test its own fresh JVM for more isolation.

View File

@ -25,6 +25,7 @@ import com.google.common.jimfs.Jimfs;
import org.apache.lucene.util.LuceneTestCase; import org.apache.lucene.util.LuceneTestCase;
import org.elasticsearch.Version; import org.elasticsearch.Version;
import org.elasticsearch.cli.MockTerminal; import org.elasticsearch.cli.MockTerminal;
import org.elasticsearch.cli.Terminal;
import org.elasticsearch.cli.UserError; import org.elasticsearch.cli.UserError;
import org.elasticsearch.common.SuppressForbidden; import org.elasticsearch.common.SuppressForbidden;
import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.collect.Tuple;
@ -70,6 +71,7 @@ import java.util.zip.ZipOutputStream;
import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.not;
@LuceneTestCase.SuppressFileSystems("*") @LuceneTestCase.SuppressFileSystems("*")
public class InstallPluginCommandTests extends ESTestCase { public class InstallPluginCommandTests extends ESTestCase {
@ -179,6 +181,10 @@ public class InstallPluginCommandTests extends ESTestCase {
/** creates a plugin .zip and returns the url for testing */ /** creates a plugin .zip and returns the url for testing */
static String createPlugin(String name, Path structure) throws IOException { static String createPlugin(String name, Path structure) throws IOException {
return createPlugin(name, structure, false);
}
static String createPlugin(String name, Path structure, boolean createSecurityPolicyFile) throws IOException {
PluginTestUtil.writeProperties(structure, PluginTestUtil.writeProperties(structure,
"description", "fake desc", "description", "fake desc",
"name", name, "name", name,
@ -186,6 +192,10 @@ public class InstallPluginCommandTests extends ESTestCase {
"elasticsearch.version", Version.CURRENT.toString(), "elasticsearch.version", Version.CURRENT.toString(),
"java.version", System.getProperty("java.specification.version"), "java.version", System.getProperty("java.specification.version"),
"classname", "FakePlugin"); "classname", "FakePlugin");
if (createSecurityPolicyFile) {
String securityPolicyContent = "grant {\n permission java.lang.RuntimePermission \"setFactory\";\n};\n";
Files.write(structure.resolve("plugin-security.policy"), securityPolicyContent.getBytes(StandardCharsets.UTF_8));
}
writeJar(structure.resolve("plugin.jar"), "FakePlugin"); writeJar(structure.resolve("plugin.jar"), "FakePlugin");
return writeZip(structure, "elasticsearch"); return writeZip(structure, "elasticsearch");
} }
@ -583,7 +593,41 @@ public class InstallPluginCommandTests extends ESTestCase {
assertThat(e.getMessage(), containsString("Unknown plugin unknown_plugin")); assertThat(e.getMessage(), containsString("Unknown plugin unknown_plugin"));
} }
// TODO: test batch flag? public void testBatchFlag() throws Exception {
MockTerminal terminal = new MockTerminal();
installPlugin(terminal, true);
assertThat(terminal.getOutput(), containsString("WARNING: plugin requires additional permissions"));
}
public void testQuietFlagDisabled() throws Exception {
MockTerminal terminal = new MockTerminal();
terminal.setVerbosity(randomFrom(Terminal.Verbosity.NORMAL, Terminal.Verbosity.VERBOSE));
installPlugin(terminal, false);
assertThat(terminal.getOutput(), containsString("100%"));
}
public void testQuietFlagEnabled() throws Exception {
MockTerminal terminal = new MockTerminal();
terminal.setVerbosity(Terminal.Verbosity.SILENT);
installPlugin(terminal, false);
assertThat(terminal.getOutput(), not(containsString("100%")));
}
private void installPlugin(MockTerminal terminal, boolean isBatch) throws Exception {
Tuple<Path, Environment> env = createEnv(fs, temp);
Path pluginDir = createPluginDir(temp);
// if batch is enabled, we also want to add a security policy
String pluginZip = createPlugin("fake", pluginDir, isBatch);
Map<String, String> settings = new HashMap<>();
settings.put("path.home", env.v1().toString());
new InstallPluginCommand() {
@Override
void jarHellCheck(Path candidate, Path pluginsDir) throws Exception {
}
}.execute(terminal, pluginZip, isBatch, settings);
}
// TODO: test checksum (need maven/official below) // TODO: test checksum (need maven/official below)
// TODO: test maven, official, and staging install...need tests with fixtures... // TODO: test maven, official, and staging install...need tests with fixtures...
} }