Merged in feature/abort_plugin_installation (pull request #326)

Feature/abort plugin installation
This commit is contained in:
Rene Pfeuffer
2019-10-02 05:04:10 +00:00
29 changed files with 656 additions and 167 deletions

View File

@@ -56,6 +56,21 @@ public class InstalledPluginResource {
return Response.ok(collectionMapper.mapInstalled(plugins, available)).build();
}
/**
* Updates all installed plugins.
*/
@POST
@Path("/update")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
@TypeHint(CollectionDto.class)
public Response updateAll() {
pluginManager.updateAll();
return Response.ok().build();
}
/**
* Returns the installed plugin with the given id.
*

View File

@@ -46,8 +46,6 @@ public class PendingPluginResource {
})
@Produces(VndMediaType.PLUGIN_COLLECTION)
public Response getPending() {
PluginPermissions.manage().check();
List<AvailablePlugin> pending = pluginManager
.getAvailable()
.stream()
@@ -71,8 +69,12 @@ public class PendingPluginResource {
List<PluginDto> updateDtos = updatePlugins.map(i -> mapper.mapInstalled(i, pending)).collect(toList());
List<PluginDto> uninstallDtos = uninstallPlugins.map(i -> mapper.mapInstalled(i, pending)).collect(toList());
if (!installDtos.isEmpty() || !updateDtos.isEmpty() || !uninstallDtos.isEmpty()) {
if (
PluginPermissions.manage().isPermitted() &&
(!installDtos.isEmpty() || !updateDtos.isEmpty() || !uninstallDtos.isEmpty())
) {
linksBuilder.single(link("execute", resourceLinks.pendingPluginCollection().executePending()));
linksBuilder.single(link("cancel", resourceLinks.pendingPluginCollection().cancelPending()));
}
Embedded.Builder embedded = Embedded.embeddedBuilder();
@@ -106,8 +108,18 @@ public class PendingPluginResource {
@ResponseCode(code = 500, condition = "internal server error")
})
public Response executePending() {
PluginPermissions.manage().check();
pluginManager.executePendingAndRestart();
return Response.ok().build();
}
@POST
@Path("/cancel")
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
public Response cancelPending() {
pluginManager.cancelPending();
return Response.ok().build();
}
}

View File

@@ -3,15 +3,15 @@ package sonia.scm.api.v2.resources;
import com.google.inject.Inject;
import de.otto.edison.hal.Embedded;
import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Link;
import de.otto.edison.hal.Links;
import sonia.scm.plugin.AvailablePlugin;
import sonia.scm.plugin.InstalledPlugin;
import sonia.scm.plugin.PluginPermissions;
import sonia.scm.plugin.PluginManager;
import java.util.List;
import static de.otto.edison.hal.Embedded.embeddedBuilder;
import static de.otto.edison.hal.Link.link;
import static de.otto.edison.hal.Links.linkingTo;
import static java.util.stream.Collectors.toList;
@@ -19,11 +19,13 @@ public class PluginDtoCollectionMapper {
private final ResourceLinks resourceLinks;
private final PluginDtoMapper mapper;
private final PluginManager manager;
@Inject
public PluginDtoCollectionMapper(ResourceLinks resourceLinks, PluginDtoMapper mapper) {
public PluginDtoCollectionMapper(ResourceLinks resourceLinks, PluginDtoMapper mapper, PluginManager manager) {
this.resourceLinks = resourceLinks;
this.mapper = mapper;
this.manager = manager;
}
public HalRepresentation mapInstalled(List<InstalledPlugin> plugins, List<AvailablePlugin> availablePlugins) {
@@ -44,6 +46,11 @@ public class PluginDtoCollectionMapper {
Links.Builder linksBuilder = linkingTo()
.with(Links.linkingTo().self(baseUrl).build());
if (!manager.getUpdatable().isEmpty()) {
linksBuilder.single(link("update", resourceLinks.installedPluginCollection().update()));
}
return linksBuilder.build();
}

View File

@@ -686,6 +686,10 @@ class ResourceLinks {
String self() {
return installedPluginCollectionLinkBuilder.method("installedPlugins").parameters().method("getInstalledPlugins").parameters().href();
}
String update() {
return installedPluginCollectionLinkBuilder.method("installedPlugins").parameters().method("updateAll").parameters().href();
}
}
public AvailablePluginLinks availablePlugin() {
@@ -739,6 +743,10 @@ class ResourceLinks {
return pendingPluginCollectionLinkBuilder.method("pendingPlugins").parameters().method("executePending").parameters().href();
}
String cancelPending() {
return pendingPluginCollectionLinkBuilder.method("pendingPlugins").parameters().method("cancelPending").parameters().href();
}
String self() {
return pendingPluginCollectionLinkBuilder.method("pendingPlugins").parameters().method("getPending").parameters().href();
}

View File

@@ -44,8 +44,8 @@ import sonia.scm.lifecycle.RestartEvent;
import sonia.scm.version.Version;
import javax.inject.Inject;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -72,7 +72,8 @@ public class DefaultPluginManager implements PluginManager {
private final PluginLoader loader;
private final PluginCenter center;
private final PluginInstaller installer;
private final Collection<PendingPluginInstallation> pendingQueue = new ArrayList<>();
private final Collection<PendingPluginInstallation> pendingInstallQueue = new ArrayList<>();
private final Collection<PendingPluginUninstallation> pendingUninstallQueue = new ArrayList<>();
private final PluginDependencyTracker dependencyTracker = new PluginDependencyTracker();
@Inject
@@ -106,7 +107,7 @@ public class DefaultPluginManager implements PluginManager {
}
private Optional<AvailablePlugin> getPending(String name) {
return pendingQueue
return pendingInstallQueue
.stream()
.map(PendingPluginInstallation::getPlugin)
.filter(filterByName(name))
@@ -138,6 +139,15 @@ public class DefaultPluginManager implements PluginManager {
.collect(Collectors.toList());
}
@Override
public List<InstalledPlugin> getUpdatable() {
return getInstalled()
.stream()
.filter(p -> isUpdatable(p.getDescriptor().getInformation().getName()))
.filter(p -> !p.isMarkedForUninstall())
.collect(Collectors.toList());
}
private <T extends Plugin> Predicate<T> filterByName(String name) {
return plugin -> name.equals(plugin.getDescriptor().getInformation().getName());
}
@@ -179,7 +189,7 @@ public class DefaultPluginManager implements PluginManager {
if (restartAfterInstallation) {
restart("plugin installation");
} else {
pendingQueue.addAll(pendingInstallations);
pendingInstallQueue.addAll(pendingInstallations);
updateMayUninstallFlag();
}
}
@@ -192,10 +202,7 @@ public class DefaultPluginManager implements PluginManager {
.orElseThrow(() -> NotFoundException.notFound(entity(InstalledPlugin.class, name)));
doThrow().violation("plugin is a core plugin and cannot be uninstalled").when(installed.isCore());
dependencyTracker.removeInstalled(installed.getDescriptor());
installed.setMarkedForUninstall(true);
createMarkerFile(installed, InstalledPlugin.UNINSTALL_MARKER_FILENAME);
markForUninstall(installed);
if (restartAfterInstallation) {
restart("plugin installation");
@@ -215,18 +222,22 @@ public class DefaultPluginManager implements PluginManager {
&& dependencyTracker.mayUninstall(p.getDescriptor().getInformation().getName());
}
private void createMarkerFile(InstalledPlugin plugin, String markerFile) {
private void markForUninstall(InstalledPlugin plugin) {
dependencyTracker.removeInstalled(plugin.getDescriptor());
try {
Files.createFile(plugin.getDirectory().resolve(markerFile));
} catch (IOException e) {
throw new PluginException("could not mark plugin " + plugin.getId() + " in path " + plugin.getDirectory() + "as " + markerFile, e);
Path file = Files.createFile(plugin.getDirectory().resolve(InstalledPlugin.UNINSTALL_MARKER_FILENAME));
pendingUninstallQueue.add(new PendingPluginUninstallation(plugin, file));
plugin.setMarkedForUninstall(true);
} catch (Exception e) {
dependencyTracker.addInstalled(plugin.getDescriptor());
throw new PluginException("could not mark plugin " + plugin.getId() + " in path " + plugin.getDirectory() + "as " + InstalledPlugin.UNINSTALL_MARKER_FILENAME, e);
}
}
@Override
public void executePendingAndRestart() {
PluginPermissions.manage().check();
if (!pendingQueue.isEmpty() || getInstalled().stream().anyMatch(InstalledPlugin::isMarkedForUninstall)) {
if (!pendingInstallQueue.isEmpty() || getInstalled().stream().anyMatch(InstalledPlugin::isMarkedForUninstall)) {
restart("execute pending plugin changes");
}
}
@@ -269,4 +280,25 @@ public class DefaultPluginManager implements PluginManager {
private boolean isUpdatable(String name) {
return getAvailable(name).isPresent() && !getPending(name).isPresent();
}
@Override
public void cancelPending() {
PluginPermissions.manage().check();
pendingUninstallQueue.forEach(PendingPluginUninstallation::cancel);
pendingInstallQueue.forEach(PendingPluginInstallation::cancel);
pendingUninstallQueue.clear();
pendingInstallQueue.clear();
updateMayUninstallFlag();
}
@Override
public void updateAll() {
PluginPermissions.manage().check();
for (InstalledPlugin installedPlugin : getInstalled()) {
String pluginName = installedPlugin.getDescriptor().getInformation().getName();
if (isUpdatable(pluginName)) {
install(pluginName, false);
}
}
}
}

View File

@@ -28,8 +28,9 @@ class PendingPluginInstallation {
LOG.info("cancel installation of plugin {}", name);
try {
Files.delete(file);
plugin.cancelInstallation();
} catch (IOException ex) {
throw new PluginFailedToCancelInstallationException("failed to cancel installation of plugin " + name, ex);
throw new PluginFailedToCancelInstallationException("failed to cancel plugin installation ", name, ex);
}
}
}

View File

@@ -0,0 +1,32 @@
package sonia.scm.plugin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
class PendingPluginUninstallation {
private static final Logger LOG = LoggerFactory.getLogger(PendingPluginUninstallation.class);
private final InstalledPlugin plugin;
private final Path uninstallFile;
PendingPluginUninstallation(InstalledPlugin plugin, Path uninstallFile) {
this.plugin = plugin;
this.uninstallFile = uninstallFile;
}
void cancel() {
String name = plugin.getDescriptor().getInformation().getName();
LOG.info("cancel uninstallation of plugin {}", name);
try {
Files.delete(uninstallFile);
plugin.setMarkedForUninstall(false);
} catch (IOException ex) {
throw new PluginFailedToCancelInstallationException("failed to cancel uninstallation", name, ex);
}
}
}

View File

@@ -33,6 +33,10 @@ class PluginDependencyTracker {
}
private void removeDependency(String from, String to) {
plugins.get(to).remove(from);
Collection<String> dependencies = plugins.get(to);
if (dependencies == null) {
throw new NullPointerException("inverse dependencies not found for " + to);
}
dependencies.remove(from);
}
}

View File

@@ -1,7 +1,16 @@
package sonia.scm.plugin;
public class PluginFailedToCancelInstallationException extends RuntimeException {
public PluginFailedToCancelInstallationException(String message, Throwable cause) {
super(message, cause);
import sonia.scm.ExceptionWithContext;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
public class PluginFailedToCancelInstallationException extends ExceptionWithContext {
public PluginFailedToCancelInstallationException(String message, String name, Exception cause) {
super(entity("plugin", name).build(), message, cause);
}
@Override
public String getCode() {
return "65RdZ5atX1";
}
}