mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-12 16:35:45 +01:00
Merged in feature/abort_plugin_installation (pull request #326)
Feature/abort plugin installation
This commit is contained in:
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user