Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/main'
Browse files Browse the repository at this point in the history
  • Loading branch information
ruibaby committed May 10, 2024
2 parents 95d759e + fe809c1 commit 5a447c2
Show file tree
Hide file tree
Showing 16 changed files with 297 additions and 120 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
## 快速开始

```bash
docker run -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 halohub/halo:2.14
docker run -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 halohub/halo:2.15
```

以上仅作为体验使用,详细部署文档请查阅:<https://docs.halo.run/getting-started/install/docker-compose>
Expand Down
8 changes: 4 additions & 4 deletions application/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -107,15 +107,15 @@ tasks.named('jacocoTestReport', JacocoReport) {
}

ext.presetPluginUrls = [
'https://github.com/halo-dev/plugin-comment-widget/releases/download/v1.9.0/plugin-comment-widget-1.9.0.jar' : 'plugin-comment-widget.jar',
'https://github.com/halo-dev/plugin-search-widget/releases/download/v1.3.1/plugin-search-widget-1.3.1.jar' : 'plugin-search-widget.jar',
'https://github.com/halo-dev/plugin-comment-widget/releases/download/v2.1.0/plugin-comment-widget-2.1.0.jar' : 'plugin-comment-widget.jar',
'https://github.com/halo-dev/plugin-search-widget/releases/download/v1.4.0/plugin-search-widget-1.4.0.jar' : 'plugin-search-widget.jar',
'https://github.com/halo-dev/plugin-sitemap/releases/download/v1.1.1/plugin-sitemap-1.1.1.jar' : 'plugin-sitemap.jar',
'https://github.com/halo-dev/plugin-feed/releases/download/v1.2.1/plugin-feed-1.2.1.jar' : 'plugin-feed.jar',
'https://github.com/halo-dev/plugin-feed/releases/download/v1.2.2/plugin-feed-1.2.2.jar' : 'plugin-feed.jar',

// Currently, plugin-app-store is not open source, so we need to download it from the official website.
// Please see https://github.com/halo-dev/plugin-app-store/issues/55
// https://www.halo.run/store/apps/app-VYJbF
'https://www.halo.run/store/apps/app-VYJbF/releases/download/app-release-cWbLS/assets/app-release-cWbLS-fZYSx': 'appstore.jar',
'https://www.halo.run/store/apps/app-VYJbF/releases/download/app-release-yfjXe/assets/app-release-yfjXe-kOzZZ': 'appstore.jar',
]

tasks.register('downloadPluginPresets', Download) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import static run.halo.app.plugin.PluginUtils.generateFileName;
import static run.halo.app.plugin.PluginUtils.isDevelopmentMode;

import com.fasterxml.jackson.core.type.TypeReference;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
Expand All @@ -21,17 +22,24 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Clock;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.pf4j.PluginManager;
import org.pf4j.PluginRuntimeException;
import org.pf4j.PluginState;
import org.pf4j.PluginStateEvent;
import org.pf4j.PluginStateListener;
import org.pf4j.PluginWrapper;
import org.pf4j.RuntimeMode;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ResourceUtils;
import org.springframework.web.util.UriComponentsBuilder;
import run.halo.app.core.extension.Plugin;
Expand All @@ -50,10 +58,13 @@
import run.halo.app.extension.controller.RequeueException;
import run.halo.app.infra.Condition;
import run.halo.app.infra.ConditionStatus;
import run.halo.app.infra.utils.JsonParseException;
import run.halo.app.infra.utils.JsonUtils;
import run.halo.app.infra.utils.PathUtils;
import run.halo.app.infra.utils.YamlUnstructuredLoader;
import run.halo.app.plugin.PluginConst;
import run.halo.app.plugin.PluginProperties;
import run.halo.app.plugin.SpringPluginManager;

/**
* Plugin reconciler.
Expand All @@ -66,19 +77,23 @@
@Component
public class PluginReconciler implements Reconciler<Request> {
private static final String FINALIZER_NAME = "plugin-protection";
private static final String DEPENDENTS_ANNO_KEY = "plugin.halo.run/dependents-snapshot";
private final ExtensionClient client;
private final PluginManager pluginManager;
private final SpringPluginManager pluginManager;

private final PluginProperties pluginProperties;

private Clock clock;

public PluginReconciler(ExtensionClient client, PluginManager pluginManager,
public PluginReconciler(ExtensionClient client, SpringPluginManager pluginManager,
PluginProperties pluginProperties) {
this.client = client;
this.pluginManager = pluginManager;
this.pluginProperties = pluginProperties;
this.clock = Clock.systemUTC();

this.pluginManager.addPluginStateListener(new PluginStartedListener());
this.pluginManager.addPluginStateListener(new PluginStoppedListener());
}

/**
Expand All @@ -95,6 +110,11 @@ public Result reconcile(Request request) {
return client.fetch(Plugin.class, request.name())
.map(plugin -> {
if (ExtensionUtil.isDeleted(plugin)) {
if (!checkDependents(plugin)) {
client.update(plugin);
// Check dependents every 10 seconds
return Result.requeue(Duration.ofSeconds(10));
}
// CleanUp resources and remove finalizer.
if (removeFinalizers(plugin.getMetadata(), Set.of(FINALIZER_NAME))) {
cleanupResources(plugin);
Expand All @@ -117,10 +137,10 @@ public Result reconcile(Request request) {

if (requestToEnable(plugin)) {
// Start
startPlugin(plugin);
enablePlugin(plugin);
} else {
// stop the plugin and disable it
stopAndDisablePlugin(plugin);
disablePlugin(plugin);
}
} catch (Throwable t) {
// populate condition
Expand All @@ -145,6 +165,28 @@ public Result reconcile(Request request) {
.orElseGet(Result::doNotRetry);
}

private boolean checkDependents(Plugin plugin) {
var pluginId = plugin.getMetadata().getName();
var dependents = pluginManager.getDependents(pluginId);
if (CollectionUtils.isEmpty(dependents)) {
return true;
}
var status = plugin.statusNonNull();
var condition = Condition.builder()
.type(PluginState.FAILED.toString())
.reason("DependentsExist")
.message(
"The plugin has dependents %s, please delete them first."
.formatted(dependents.stream().map(PluginWrapper::getPluginId).toList())
)
.status(ConditionStatus.FALSE)
.lastTransitionTime(clock.instant())
.build();
nullSafeConditions(status).addAndEvictFIFO(condition);
status.setPhase(Plugin.Phase.FAILED);
return false;
}

private void syncPluginState(Plugin plugin) {
var pluginName = plugin.getMetadata().getName();
var p = pluginManager.getPlugin(pluginName);
Expand Down Expand Up @@ -191,32 +233,87 @@ private void cleanupResources(Plugin plugin) {
}
}

private void startPlugin(Plugin plugin) {
private void enablePlugin(Plugin plugin) {
// start the plugin
var pluginName = plugin.getMetadata().getName();
var wrapper = pluginManager.getPlugin(pluginName);
log.info("Starting plugin {}", pluginName);
plugin.statusNonNull().setPhase(Plugin.Phase.STARTING);
if (!PluginState.STARTED.equals(wrapper.getPluginState())) {
var pluginState = pluginManager.startPlugin(pluginName);
if (!PluginState.STARTED.equals(pluginState)) {
throw new IllegalStateException("Failed to start plugin " + pluginName);
}
plugin.statusNonNull().setLastStartTime(clock.instant());
var condition = Condition.builder()
.type(PluginState.STARTED.toString())
.reason(PluginState.STARTED.toString())
.message("Started successfully")
.lastTransitionTime(clock.instant())
.status(ConditionStatus.TRUE)
.build();
nullSafeConditions(plugin.statusNonNull()).addAndEvictFIFO(condition);
var pluginState = pluginManager.startPlugin(pluginName);
if (!PluginState.STARTED.equals(pluginState)) {
throw new IllegalStateException("Failed to start plugin " + pluginName);
}

var dependents = getAndRemoveDependents(plugin);
log.info("Starting dependents {} for plugin {}", dependents, pluginName);
dependents.stream()
.sorted(Comparator.reverseOrder())
.forEach(dependent -> {
if (pluginManager.getPlugin(dependent) != null) {
pluginManager.startPlugin(dependent);
}
});
log.info("Started dependents {} for plugin {}", dependents, pluginName);

plugin.statusNonNull().setLastStartTime(clock.instant());
var condition = Condition.builder()
.type(PluginState.STARTED.toString())
.reason(PluginState.STARTED.toString())
.message("Started successfully")
.lastTransitionTime(clock.instant())
.status(ConditionStatus.TRUE)
.build();
nullSafeConditions(plugin.statusNonNull()).addAndEvictFIFO(condition);
plugin.statusNonNull().setPhase(Plugin.Phase.STARTED);

log.info("Started plugin {}", pluginName);
}

private void stopAndDisablePlugin(Plugin plugin) {
private List<String> getAndRemoveDependents(Plugin plugin) {
var pluginName = plugin.getMetadata().getName();
var annotations = plugin.getMetadata().getAnnotations();
if (annotations == null) {
return List.of();
}
var dependentsAnno = annotations.remove(DEPENDENTS_ANNO_KEY);
List<String> dependents = List.of();
if (StringUtils.isNotBlank(dependentsAnno)) {
try {
dependents = JsonUtils.jsonToObject(dependentsAnno, new TypeReference<>() {
});
} catch (JsonParseException ignored) {
log.error("Failed to parse dependents annotation {} for plugin {}",
dependentsAnno, pluginName);
// Keep going to start the plugin.
}
}
return dependents;
}

private void setDependents(Plugin plugin) {
var pluginName = plugin.getMetadata().getName();
var annotations = plugin.getMetadata().getAnnotations();
if (annotations == null) {
annotations = new HashMap<>();
plugin.getMetadata().setAnnotations(annotations);
}
if (!annotations.containsKey(DEPENDENTS_ANNO_KEY)) {
// get dependents
var dependents = pluginManager.getDependents(pluginName)
.stream()
.filter(pluginWrapper ->
Objects.equals(PluginState.STARTED, pluginWrapper.getPluginState())
)
.map(PluginWrapper::getPluginId)
.toList();

annotations.put(DEPENDENTS_ANNO_KEY, JsonUtils.objectToJson(dependents));
}
}

private void disablePlugin(Plugin plugin) {
var pluginName = plugin.getMetadata().getName();
if (pluginManager.getPlugin(pluginName) != null) {
setDependents(plugin);
pluginManager.disablePlugin(pluginName);
}
plugin.statusNonNull().setPhase(Plugin.Phase.DISABLED);
Expand Down Expand Up @@ -284,29 +381,29 @@ private void resolveStaticResources(Plugin plugin) {
}

private void loadOrReload(Plugin plugin) {
// TODO Try to check dependencies before.
var pluginName = plugin.getMetadata().getName();
try {
var p = pluginManager.getPlugin(pluginName);
var requestToReload = requestToReload(plugin);
if (requestToReload) {
log.info("Unloading plugin {}", pluginName);
if (p != null) {
pluginManager.unloadPlugin(pluginName);
}
}
if (p == null || requestToReload) {
log.info("Loading plugin {}", pluginName);
var p = pluginManager.getPlugin(pluginName);
var requestToReload = requestToReload(plugin);
if (requestToReload) {
if (p != null) {
var loadLocation = plugin.getStatus().getLoadLocation();
pluginManager.loadPlugin(Paths.get(loadLocation));
log.info("Loaded plugin {}", pluginName);
}
} catch (Throwable t) {
// unload the plugin
if (pluginManager.getPlugin(pluginName) != null) {
pluginManager.unloadPlugin(pluginName);
setDependents(plugin);
var unloaded = pluginManager.reloadPlugin(pluginName, Paths.get(loadLocation));
if (!unloaded) {
throw new PluginRuntimeException("Failed to reload plugin " + pluginName);
}
p = pluginManager.getPlugin(pluginName);
}
throw t;
}
if (p != null && pluginManager.getUnresolvedPlugins().contains(p)) {
pluginManager.unloadPlugin(pluginName);
p = null;
}
if (p == null) {
var loadLocation = plugin.getStatus().getLoadLocation();
log.info("Loading plugin {} from {}", pluginName, loadLocation);
pluginManager.loadPlugin(Paths.get(loadLocation));
log.info("Loaded plugin {} from {}", pluginName, loadLocation);
}
}

Expand Down Expand Up @@ -480,4 +577,38 @@ static String buildReverseProxyName(String pluginName) {
return pluginName + "-system-generated-reverse-proxy";
}

public class PluginStartedListener implements PluginStateListener {

@Override
public void pluginStateChanged(PluginStateEvent event) {
if (PluginState.STARTED.equals(event.getPluginState())) {
var pluginId = event.getPlugin().getPluginId();
client.fetch(Plugin.class, pluginId)
.ifPresent(plugin -> {
if (!Objects.equals(true, plugin.getSpec().getEnabled())) {
plugin.getSpec().setEnabled(true);
client.update(plugin);
}
});
}
}
}

public class PluginStoppedListener implements PluginStateListener {

@Override
public void pluginStateChanged(PluginStateEvent event) {
if (PluginState.STOPPED.equals(event.getPluginState())) {
var pluginId = event.getPlugin().getPluginId();
client.fetch(Plugin.class, pluginId)
.ifPresent(plugin -> {
if (!requestToReload(plugin)
&& Objects.equals(true, plugin.getSpec().getEnabled())) {
plugin.getSpec().setEnabled(false);
client.update(plugin);
}
});
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.springframework.util.CollectionUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.extension.ExtensionUtil;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.Unstructured;
import run.halo.app.infra.properties.HaloProperties;
Expand Down Expand Up @@ -96,7 +97,13 @@ private Mono<Unstructured> createOrUpdate(Unstructured extension) {
extension.getMetadata().setVersion(existingExt.getMetadata().getVersion());
return extensionClient.update(extension);
})
.switchIfEmpty(Mono.defer(() -> extensionClient.create(extension)));
.switchIfEmpty(Mono.defer(() -> {
if (ExtensionUtil.isDeleted(extension)) {
// skip deleted extension
return Mono.empty();
}
return extensionClient.create(extension);
}));
}

private List<Resource> listResources(String location) {
Expand Down
Loading

0 comments on commit 5a447c2

Please sign in to comment.