mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-12 16:35:45 +01:00
Merge pull request #1079 from scm-manager/feature/real_jvm_restart
Real jvm restart
This commit is contained in:
@@ -7,14 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
## Unreleased
|
## Unreleased
|
||||||
### Added
|
### Added
|
||||||
- Extension point to add links to the repository cards from plug ins ([#1041](https://github.com/scm-manager/scm-manager/pull/1041))
|
- Extension point to add links to the repository cards from plug ins ([#1041](https://github.com/scm-manager/scm-manager/pull/1041))
|
||||||
|
- Libc based restart strategy for posix operating systems ([#1079](https://github.com/scm-manager/scm-manager/pull/1079))
|
||||||
|
- Simple restart strategy with System.exit ([#1079](https://github.com/scm-manager/scm-manager/pull/1079))
|
||||||
|
- Notification if restart is not supported on the underlying platform ([#1079](https://github.com/scm-manager/scm-manager/pull/1079))
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Update resteasy to version 4.5.2.Final
|
- Update resteasy to version 4.5.2.Final
|
||||||
|
- Update shiro to version 1.5.2
|
||||||
- Use browser built-in EventSource for apiClient subscriptions
|
- Use browser built-in EventSource for apiClient subscriptions
|
||||||
- Changeover to MIT license ([#1066](https://github.com/scm-manager/scm-manager/pull/1066))
|
- Changeover to MIT license ([#1066](https://github.com/scm-manager/scm-manager/pull/1066))
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
- EventSource Polyfill
|
- EventSource Polyfill
|
||||||
|
- ClassLoader based restart logic ([#1079](https://github.com/scm-manager/scm-manager/pull/1079))
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Build on windows ([#1048](https://github.com/scm-manager/scm-manager/issues/1048), [#1049](https://github.com/scm-manager/scm-manager/issues/1049), [#1056](https://github.com/scm-manager/scm-manager/pull/1056))
|
- Build on windows ([#1048](https://github.com/scm-manager/scm-manager/issues/1048), [#1049](https://github.com/scm-manager/scm-manager/issues/1049), [#1056](https://github.com/scm-manager/scm-manager/pull/1056))
|
||||||
|
|||||||
9
pom.xml
9
pom.xml
@@ -363,6 +363,13 @@
|
|||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<version>4.13</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.hamcrest</groupId>
|
<groupId>org.hamcrest</groupId>
|
||||||
<artifactId>hamcrest-core</artifactId>
|
<artifactId>hamcrest-core</artifactId>
|
||||||
@@ -919,7 +926,7 @@
|
|||||||
|
|
||||||
<!-- security libraries -->
|
<!-- security libraries -->
|
||||||
<ssp.version>1.2.0</ssp.version>
|
<ssp.version>1.2.0</ssp.version>
|
||||||
<shiro.version>1.5.1</shiro.version>
|
<shiro.version>1.5.2</shiro.version>
|
||||||
|
|
||||||
<!-- repository libraries -->
|
<!-- repository libraries -->
|
||||||
<jgit.version>5.6.1.202002131546-r-scm1</jgit.version>
|
<jgit.version>5.6.1.202002131546-r-scm1</jgit.version>
|
||||||
|
|||||||
@@ -29,63 +29,37 @@ package sonia.scm.lifecycle;
|
|||||||
import sonia.scm.event.Event;
|
import sonia.scm.event.Event;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This event can be used to force a restart of the webapp context. The restart
|
* This event indicates a forced restart of scm-manager.
|
||||||
* event is useful during plugin development, because we don't have to restart
|
|
||||||
* the whole server, to see our changes. The restart event could also be used
|
|
||||||
* to install or upgrade plugins.
|
|
||||||
*
|
|
||||||
* But the restart event should be used carefully, because the whole context
|
|
||||||
* will be restarted and that process could take some time.
|
|
||||||
*
|
|
||||||
* @author Sebastian Sdorra
|
* @author Sebastian Sdorra
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
@Event
|
@Event
|
||||||
public class RestartEvent
|
public class RestartEvent {
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
private final Class<?> cause;
|
||||||
* Constructs ...
|
private final String reason;
|
||||||
*
|
|
||||||
*
|
RestartEvent(Class<?> cause, String reason) {
|
||||||
* @param cause
|
|
||||||
* @param reason
|
|
||||||
*/
|
|
||||||
public RestartEvent(Class<?> cause, String reason)
|
|
||||||
{
|
|
||||||
this.cause = cause;
|
this.cause = cause;
|
||||||
this.reason = reason;
|
this.reason = reason;
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- get methods ----------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The class which has fired the restart event.
|
* The class which has fired the restart event.
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* @return class which has fired the restart event
|
* @return class which has fired the restart event
|
||||||
*/
|
*/
|
||||||
public Class<?> getCause()
|
public Class<?> getCause() {
|
||||||
{
|
|
||||||
return cause;
|
return cause;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the reason for the restart.
|
* Returns the reason for the restart.
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* @return reason for restart
|
* @return reason for restart
|
||||||
*/
|
*/
|
||||||
public String getReason()
|
public String getReason() {
|
||||||
{
|
|
||||||
return reason;
|
return reason;
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- fields ---------------------------------------------------------------
|
|
||||||
|
|
||||||
/** cause of restart */
|
|
||||||
private final Class<?> cause;
|
|
||||||
|
|
||||||
/** reason for restart */
|
|
||||||
private final String reason;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
package sonia.scm.lifecycle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception is thrown if a restart is not supported or a restart strategy is misconfigured.
|
||||||
|
*/
|
||||||
|
public class RestartNotSupportedException extends RuntimeException {
|
||||||
|
RestartNotSupportedException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
RestartNotSupportedException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
49
scm-core/src/main/java/sonia/scm/lifecycle/Restarter.java
Normal file
49
scm-core/src/main/java/sonia/scm/lifecycle/Restarter.java
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
package sonia.scm.lifecycle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Restarter} is able to restart scm-manager.
|
||||||
|
*
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
public interface Restarter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return {@code true} if restarting scm-manager is supported.
|
||||||
|
*
|
||||||
|
* @return {@code true} if restart is supported
|
||||||
|
*/
|
||||||
|
boolean isSupported();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Issues a restart. The method will fire a {@link RestartEvent} to notify the system about the upcoming restart.
|
||||||
|
* If restarting is not supported by the underlying platform a {@link RestartNotSupportedException} is thrown.
|
||||||
|
*
|
||||||
|
* @param cause cause of the restart. This should be the class which calls this method.
|
||||||
|
* @param reason reason for the required restart.
|
||||||
|
* @throws RestartNotSupportedException if restarting is not supported by the underlying platform.
|
||||||
|
*/
|
||||||
|
void restart(Class<?> cause, String reason);
|
||||||
|
}
|
||||||
@@ -53,6 +53,12 @@
|
|||||||
<version>2.0.0-SNAPSHOT</version>
|
<version>2.0.0-SNAPSHOT</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.github.sdorra</groupId>
|
<groupId>com.github.sdorra</groupId>
|
||||||
<artifactId>shiro-unit</artifactId>
|
<artifactId>shiro-unit</artifactId>
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
package sonia.scm.lifecycle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates restart events for testing.
|
||||||
|
* This is required, because the constructor of {@link RestartEvent} is package private.
|
||||||
|
*/
|
||||||
|
public final class RestartEventFactory {
|
||||||
|
|
||||||
|
private RestartEventFactory(){}
|
||||||
|
|
||||||
|
public static RestartEvent create(Class<?> cause, String reason) {
|
||||||
|
return new RestartEvent(cause, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -30,6 +30,7 @@
|
|||||||
"installedNavLink": "Installiert",
|
"installedNavLink": "Installiert",
|
||||||
"availableNavLink": "Verfügbar"
|
"availableNavLink": "Verfügbar"
|
||||||
},
|
},
|
||||||
|
"showPending": "Änderungen anzeigen",
|
||||||
"executePending": "Änderungen ausführen",
|
"executePending": "Änderungen ausführen",
|
||||||
"outdatedPlugins": "{{count}} veraltetes Plugin",
|
"outdatedPlugins": "{{count}} veraltetes Plugin",
|
||||||
"outdatedPlugins_plural": "{{count}} veraltete Plugins",
|
"outdatedPlugins_plural": "{{count}} veraltete Plugins",
|
||||||
@@ -55,6 +56,7 @@
|
|||||||
"executeAndRestart": "Ausführen und Neustarten",
|
"executeAndRestart": "Ausführen und Neustarten",
|
||||||
"updateAll": "Alle Plugins aktualisieren",
|
"updateAll": "Alle Plugins aktualisieren",
|
||||||
"abort": "Abbrechen",
|
"abort": "Abbrechen",
|
||||||
|
"close": "Schließen",
|
||||||
"author": "Autor",
|
"author": "Autor",
|
||||||
"version": "Version",
|
"version": "Version",
|
||||||
"currentVersion": "Installierte Version",
|
"currentVersion": "Installierte Version",
|
||||||
@@ -66,10 +68,12 @@
|
|||||||
"uninstalledNotification": "Das Plugin wurde erfolgreich installiert. Um Änderungen an der UI zu sehen, muss die Seite neu geladen werden:",
|
"uninstalledNotification": "Das Plugin wurde erfolgreich installiert. Um Änderungen an der UI zu sehen, muss die Seite neu geladen werden:",
|
||||||
"executedChangesNotification": "Die Plugin Änderungen wurden erfolgreich durchgeführt. Um Änderungen an der UI zu sehen, muss die Seite neu geladen werden:",
|
"executedChangesNotification": "Die Plugin Änderungen wurden erfolgreich durchgeführt. Um Änderungen an der UI zu sehen, muss die Seite neu geladen werden:",
|
||||||
"reload": "jetzt neu laden",
|
"reload": "jetzt neu laden",
|
||||||
"restartNotification": "Der SCM-Manager Kontext sollte nur neu gestartet werden, wenn aktuell niemand damit arbeitet.",
|
"restartNotification": "Der SCM-Manager sollte nur neu gestartet werden, wenn aktuell niemand damit arbeitet.",
|
||||||
"executePending": "Die folgenden Plugin-Änderungen werden ausgeführt. Anschließend wird der SCM-Manager Kontext neu gestartet.",
|
"executePending": "Die folgenden Plugin-Änderungen werden ausgeführt. Anschließend wird SCM-Manager neu gestartet.",
|
||||||
"cancelPending": "Die folgenden Plugin-Änderungen werden abgebrochen und zurückgesetzt.",
|
"cancelPending": "Die folgenden Plugin-Änderungen werden abgebrochen und zurückgesetzt.",
|
||||||
"updateAllInfo": "Die folgenden Plugins werden aktualisiert. Die Änderungen werden nach dem nächsten Neustart wirksam."
|
"updateAllInfo": "Die folgenden Plugins werden aktualisiert. Die Änderungen werden nach dem nächsten Neustart wirksam.",
|
||||||
|
"manualRestartRequired": "Nachdem die Plugin-Änderung durchgeführt wurde, muss SCM-Manager neu gestartet werden.",
|
||||||
|
"showPending": "Um die folgenden Plugin-Änderungen auszuführen, muss SCM-Manager neu gestartet werden."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repositoryRole": {
|
"repositoryRole": {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"installedNavLink": "Installed",
|
"installedNavLink": "Installed",
|
||||||
"availableNavLink": "Available"
|
"availableNavLink": "Available"
|
||||||
},
|
},
|
||||||
|
"showPending": "Show changes",
|
||||||
"executePending": "Execute changes",
|
"executePending": "Execute changes",
|
||||||
"outdatedPlugins": "{{count}} outdated plugin",
|
"outdatedPlugins": "{{count}} outdated plugin",
|
||||||
"outdatedPlugins_plural": "{{count}} outdated plugins",
|
"outdatedPlugins_plural": "{{count}} outdated plugins",
|
||||||
@@ -55,6 +56,7 @@
|
|||||||
"executeAndRestart": "Execute and Restart",
|
"executeAndRestart": "Execute and Restart",
|
||||||
"updateAll": "Update all plugins",
|
"updateAll": "Update all plugins",
|
||||||
"abort": "Abort",
|
"abort": "Abort",
|
||||||
|
"close": "Close",
|
||||||
"author": "Author",
|
"author": "Author",
|
||||||
"version": "Version",
|
"version": "Version",
|
||||||
"currentVersion": "Installed version",
|
"currentVersion": "Installed version",
|
||||||
@@ -66,10 +68,12 @@
|
|||||||
"uninstalledNotification": "Successfully uninstalled plugin. You have to reload the page, to see ui changes:",
|
"uninstalledNotification": "Successfully uninstalled plugin. You have to reload the page, to see ui changes:",
|
||||||
"executedChangesNotification": "Successfully executed plugin changes. You have to reload the page, to see ui changes:",
|
"executedChangesNotification": "Successfully executed plugin changes. You have to reload the page, to see ui changes:",
|
||||||
"reload": "reload now",
|
"reload": "reload now",
|
||||||
"restartNotification": "You should only restart the scm-manager context if no one else is currently working with it.",
|
"restartNotification": "You should only restart SCM-Manager if no one else is currently working with it.",
|
||||||
"executePending": "The following plugin changes will be executed and after that the scm-manager context will be restarted.",
|
"executePending": "The following plugin changes will be executed and after that the SCM-Manager will be restarted.",
|
||||||
"cancelPending": "The following plugin changes will be canceled.",
|
"cancelPending": "The following plugin changes will be canceled.",
|
||||||
"updateAllInfo": "The following plugin changes will be executed. You need to restart the scm-manager to make these changes effective."
|
"updateAllInfo": "The following plugin changes will be executed. You need to restart the SCM-Manager to make these changes effective.",
|
||||||
|
"manualRestartRequired": "After the plugin change has been made, SCM-Manager must be restarted.",
|
||||||
|
"showPending": "To execute the following plugin changes, SCM-Manager must be restarted."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repositoryRole": {
|
"repositoryRole": {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { PendingPlugins } from "@scm-manager/ui-types";
|
|||||||
import { WithTranslation, withTranslation } from "react-i18next";
|
import { WithTranslation, withTranslation } from "react-i18next";
|
||||||
import waitForRestart from "./waitForRestart";
|
import waitForRestart from "./waitForRestart";
|
||||||
import SuccessNotification from "./SuccessNotification";
|
import SuccessNotification from "./SuccessNotification";
|
||||||
|
import PendingPluginsQueue from "./PendingPluginsQueue";
|
||||||
|
|
||||||
type Props = WithTranslation & {
|
type Props = WithTranslation & {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -85,70 +86,14 @@ class ExecutePendingModal extends React.Component<Props, State> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
renderInstallQueue = () => {
|
|
||||||
const { pendingPlugins, t } = this.props;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{pendingPlugins._embedded && pendingPlugins._embedded.new.length > 0 && (
|
|
||||||
<>
|
|
||||||
<strong>{t("plugins.modal.installQueue")}</strong>
|
|
||||||
<ul>
|
|
||||||
{pendingPlugins._embedded.new.map(plugin => (
|
|
||||||
<li key={plugin.name}>{plugin.name}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
renderUpdateQueue = () => {
|
|
||||||
const { pendingPlugins, t } = this.props;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{pendingPlugins._embedded && pendingPlugins._embedded.update.length > 0 && (
|
|
||||||
<>
|
|
||||||
<strong>{t("plugins.modal.updateQueue")}</strong>
|
|
||||||
<ul>
|
|
||||||
{pendingPlugins._embedded.update.map(plugin => (
|
|
||||||
<li key={plugin.name}>{plugin.name}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
renderUninstallQueue = () => {
|
|
||||||
const { pendingPlugins, t } = this.props;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{pendingPlugins._embedded && pendingPlugins._embedded.uninstall.length > 0 && (
|
|
||||||
<>
|
|
||||||
<strong>{t("plugins.modal.uninstallQueue")}</strong>
|
|
||||||
<ul>
|
|
||||||
{pendingPlugins._embedded.uninstall.map(plugin => (
|
|
||||||
<li key={plugin.name}>{plugin.name}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
renderBody = () => {
|
renderBody = () => {
|
||||||
const { t } = this.props;
|
const { pendingPlugins, t } = this.props;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="media">
|
<div className="media">
|
||||||
<div className="content">
|
<div className="content">
|
||||||
<p>{t("plugins.modal.executePending")}</p>
|
<p>{t("plugins.modal.executePending")}</p>
|
||||||
{this.renderInstallQueue()}
|
<PendingPluginsQueue pendingPlugins={pendingPlugins} />
|
||||||
{this.renderUpdateQueue()}
|
|
||||||
{this.renderUninstallQueue()}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="media">{this.renderNotifications()}</div>
|
<div className="media">{this.renderNotifications()}</div>
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { FC } from "react";
|
||||||
|
import { PendingPlugins } from "@scm-manager/ui-types";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
pendingPlugins: PendingPlugins;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SectionProps = Props & {
|
||||||
|
type: string;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Section: FC<SectionProps> = ({ pendingPlugins, type, label }) => {
|
||||||
|
const plugins = pendingPlugins?._embedded[type];
|
||||||
|
if (!plugins || plugins.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<strong>{label}</strong>
|
||||||
|
<ul>
|
||||||
|
{plugins.map(plugin => (
|
||||||
|
<li key={plugin.name}>{plugin.name}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PendingPluginsQueue: FC<Props> = ({ pendingPlugins }) => {
|
||||||
|
const [t] = useTranslation("admin");
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Section pendingPlugins={pendingPlugins} type="new" label={t("plugins.modal.installQueue")} />
|
||||||
|
<Section pendingPlugins={pendingPlugins} type="update" label={t("plugins.modal.updateQueue")} />
|
||||||
|
<Section pendingPlugins={pendingPlugins} type="uninstall" label={t("plugins.modal.uninstallQueue")} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PendingPluginsQueue;
|
||||||
@@ -214,8 +214,25 @@ class PluginModal extends React.Component<Props, State> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
createRestartSectionContent = () => {
|
||||||
const { restart } = this.state;
|
const { restart } = this.state;
|
||||||
|
const { plugin, pluginAction, t } = this.props;
|
||||||
|
|
||||||
|
if (plugin._links[pluginAction + "WithRestart"]) {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
checked={restart}
|
||||||
|
label={t("plugins.modal.restart")}
|
||||||
|
onChange={this.handleRestartChange}
|
||||||
|
disabled={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return <Notification type="warning">{t("plugins.modal.manualRestartRequired")}</Notification>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
const { plugin, pluginAction, onClose, t } = this.props;
|
const { plugin, pluginAction, onClose, t } = this.props;
|
||||||
|
|
||||||
const body = (
|
const body = (
|
||||||
@@ -262,12 +279,7 @@ class PluginModal extends React.Component<Props, State> {
|
|||||||
</div>
|
</div>
|
||||||
<div className="media">
|
<div className="media">
|
||||||
<div className="media-content">
|
<div className="media-content">
|
||||||
<Checkbox
|
{this.createRestartSectionContent()}
|
||||||
checked={restart}
|
|
||||||
label={t("plugins.modal.restart")}
|
|
||||||
onChange={this.handleRestartChange}
|
|
||||||
disabled={false}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{this.renderNotifications()}
|
{this.renderNotifications()}
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
import React, { FC } from "react";
|
||||||
|
import { Button, Modal, Notification } from "@scm-manager/ui-components";
|
||||||
|
import { PendingPlugins } from "@scm-manager/ui-types";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import PendingPluginsQueue from "./PendingPluginsQueue";
|
||||||
|
|
||||||
|
|
||||||
|
type ModalBodyProps = {
|
||||||
|
pendingPlugins: PendingPlugins;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ModalBody: FC<ModalBodyProps> = ({ pendingPlugins }) => {
|
||||||
|
const [t] = useTranslation("admin");
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="media">
|
||||||
|
<div className="content">
|
||||||
|
<p>{t("plugins.modal.showPending")}</p>
|
||||||
|
<PendingPluginsQueue pendingPlugins={pendingPlugins} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="media">
|
||||||
|
<Notification type="warning">{t("plugins.modal.restartNotification")}</Notification>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: () => void;
|
||||||
|
pendingPlugins: PendingPlugins;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ShowPendingModal: FC<Props> = ({ pendingPlugins, onClose }) => {
|
||||||
|
const [t] = useTranslation("admin");
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={t("plugins.showPending")}
|
||||||
|
closeFunction={onClose}
|
||||||
|
body={<ModalBody pendingPlugins={pendingPlugins} />}
|
||||||
|
footer={<Button label={t("plugins.modal.close")} action={onClose} />}
|
||||||
|
active={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShowPendingModal;
|
||||||
@@ -55,6 +55,7 @@ import ExecutePendingActionModal from "../components/ExecutePendingActionModal";
|
|||||||
import CancelPendingActionModal from "../components/CancelPendingActionModal";
|
import CancelPendingActionModal from "../components/CancelPendingActionModal";
|
||||||
import UpdateAllActionModal from "../components/UpdateAllActionModal";
|
import UpdateAllActionModal from "../components/UpdateAllActionModal";
|
||||||
import { Plugin } from "@scm-manager/ui-types/src";
|
import { Plugin } from "@scm-manager/ui-types/src";
|
||||||
|
import ShowPendingModal from "../components/ShowPendingModal";
|
||||||
|
|
||||||
type Props = WithTranslation & {
|
type Props = WithTranslation & {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
@@ -74,6 +75,7 @@ type Props = WithTranslation & {
|
|||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
showPendingModal: boolean;
|
showPendingModal: boolean;
|
||||||
|
showExecutePendingModal: boolean;
|
||||||
showUpdateAllModal: boolean;
|
showUpdateAllModal: boolean;
|
||||||
showCancelModal: boolean;
|
showCancelModal: boolean;
|
||||||
};
|
};
|
||||||
@@ -83,6 +85,7 @@ class PluginsOverview extends React.Component<Props, State> {
|
|||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
showPendingModal: false,
|
showPendingModal: false,
|
||||||
|
showExecutePendingModal: false,
|
||||||
showUpdateAllModal: false,
|
showUpdateAllModal: false,
|
||||||
showCancelModal: false
|
showCancelModal: false
|
||||||
};
|
};
|
||||||
@@ -138,7 +141,8 @@ class PluginsOverview extends React.Component<Props, State> {
|
|||||||
const { pendingPlugins, collection, t } = this.props;
|
const { pendingPlugins, collection, t } = this.props;
|
||||||
const buttons = [];
|
const buttons = [];
|
||||||
|
|
||||||
if (pendingPlugins && pendingPlugins._links && pendingPlugins._links.execute) {
|
if (pendingPlugins && pendingPlugins._links) {
|
||||||
|
if (pendingPlugins._links.execute) {
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
@@ -146,6 +150,24 @@ class PluginsOverview extends React.Component<Props, State> {
|
|||||||
key={"executePending"}
|
key={"executePending"}
|
||||||
icon={"arrow-circle-right"}
|
icon={"arrow-circle-right"}
|
||||||
label={t("plugins.executePending")}
|
label={t("plugins.executePending")}
|
||||||
|
action={() =>
|
||||||
|
this.setState({
|
||||||
|
showExecutePendingModal: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingPlugins._links.cancel) {
|
||||||
|
if (!pendingPlugins._links.execute) {
|
||||||
|
buttons.push(
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
reducedMobile={true}
|
||||||
|
key={"showPending"}
|
||||||
|
icon={"info"}
|
||||||
|
label={t("plugins.showPending")}
|
||||||
action={() =>
|
action={() =>
|
||||||
this.setState({
|
this.setState({
|
||||||
showPendingModal: true
|
showPendingModal: true
|
||||||
@@ -155,7 +177,6 @@ class PluginsOverview extends React.Component<Props, State> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pendingPlugins && pendingPlugins._links && pendingPlugins._links.cancel) {
|
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
@@ -171,6 +192,7 @@ class PluginsOverview extends React.Component<Props, State> {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (collection && collection._links && collection._links.update) {
|
if (collection && collection._links && collection._links.update) {
|
||||||
buttons.push(
|
buttons.push(
|
||||||
@@ -228,14 +250,27 @@ class PluginsOverview extends React.Component<Props, State> {
|
|||||||
|
|
||||||
renderModals = () => {
|
renderModals = () => {
|
||||||
const { collection, pendingPlugins } = this.props;
|
const { collection, pendingPlugins } = this.props;
|
||||||
const { showPendingModal, showCancelModal, showUpdateAllModal } = this.state;
|
const { showPendingModal, showExecutePendingModal, showCancelModal, showUpdateAllModal } = this.state;
|
||||||
|
|
||||||
if (showPendingModal) {
|
if (showPendingModal) {
|
||||||
|
return (
|
||||||
|
<ShowPendingModal
|
||||||
|
onClose={() =>
|
||||||
|
this.setState({
|
||||||
|
showPendingModal: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pendingPlugins={pendingPlugins}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showExecutePendingModal) {
|
||||||
return (
|
return (
|
||||||
<ExecutePendingActionModal
|
<ExecutePendingActionModal
|
||||||
onClose={() =>
|
onClose={() =>
|
||||||
this.setState({
|
this.setState({
|
||||||
showPendingModal: false
|
showExecutePendingModal: false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
pendingPlugins={pendingPlugins}
|
pendingPlugins={pendingPlugins}
|
||||||
|
|||||||
@@ -312,12 +312,18 @@
|
|||||||
<version>1.23</version>
|
<version>1.23</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- class loader leak prevention -->
|
<!-- unix restart -->
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>se.jiderhamn.classloader-leak-prevention</groupId>
|
<groupId>org.kohsuke</groupId>
|
||||||
<artifactId>classloader-leak-prevention-core</artifactId>
|
<artifactId>akuma</artifactId>
|
||||||
<version>2.7.0</version>
|
<version>1.10</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.java.dev.jna</groupId>
|
||||||
|
<artifactId>jna</artifactId>
|
||||||
|
<version>5.5.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- test scope -->
|
<!-- test scope -->
|
||||||
@@ -800,10 +806,6 @@
|
|||||||
<name>scm.stage</name>
|
<name>scm.stage</name>
|
||||||
<value>${scm.stage}</value>
|
<value>${scm.stage}</value>
|
||||||
</systemProperty>
|
</systemProperty>
|
||||||
<systemProperty>
|
|
||||||
<name>sonia.scm.classloading.lifecycle</name>
|
|
||||||
<value>simple</value>
|
|
||||||
</systemProperty>
|
|
||||||
</systemProperties>
|
</systemProperties>
|
||||||
<jettyXml>${project.basedir}/src/main/conf/jetty.xml</jettyXml>
|
<jettyXml>${project.basedir}/src/main/conf/jetty.xml</jettyXml>
|
||||||
<scanIntervalSeconds>0</scanIntervalSeconds>
|
<scanIntervalSeconds>0</scanIntervalSeconds>
|
||||||
@@ -889,10 +891,6 @@
|
|||||||
<name>scm.home</name>
|
<name>scm.home</name>
|
||||||
<value>target/scm-it</value>
|
<value>target/scm-it</value>
|
||||||
</systemProperty>
|
</systemProperty>
|
||||||
<systemProperty>
|
|
||||||
<name>sonia.scm.classloading.lifecycle</name>
|
|
||||||
<value>simple</value>
|
|
||||||
</systemProperty>
|
|
||||||
</systemProperties>
|
</systemProperties>
|
||||||
<jettyXml>${project.basedir}/src/main/conf/jetty.xml</jettyXml>
|
<jettyXml>${project.basedir}/src/main/conf/jetty.xml</jettyXml>
|
||||||
<scanIntervalSeconds>0</scanIntervalSeconds>
|
<scanIntervalSeconds>0</scanIntervalSeconds>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
|
|
||||||
package sonia.scm.api.v2.resources;
|
package sonia.scm.api.v2.resources;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
|
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
@@ -45,6 +46,7 @@ import javax.ws.rs.PUT;
|
|||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
import javax.ws.rs.Produces;
|
import javax.ws.rs.Produces;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RESTful Web Service Resource to manage the configuration.
|
* RESTful Web Service Resource to manage the configuration.
|
||||||
@@ -61,6 +63,8 @@ public class ConfigResource {
|
|||||||
private final ScmConfiguration configuration;
|
private final ScmConfiguration configuration;
|
||||||
private final NamespaceStrategyValidator namespaceStrategyValidator;
|
private final NamespaceStrategyValidator namespaceStrategyValidator;
|
||||||
|
|
||||||
|
private Consumer<ScmConfiguration> store = (config) -> ScmConfigurationUtil.getInstance().store(config);
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public ConfigResource(ConfigDtoToScmConfigurationMapper dtoToConfigMapper,
|
public ConfigResource(ConfigDtoToScmConfigurationMapper dtoToConfigMapper,
|
||||||
ScmConfigurationToConfigDtoMapper configToDtoMapper,
|
ScmConfigurationToConfigDtoMapper configToDtoMapper,
|
||||||
@@ -71,6 +75,11 @@ public class ConfigResource {
|
|||||||
this.namespaceStrategyValidator = namespaceStrategyValidator;
|
this.namespaceStrategyValidator = namespaceStrategyValidator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
void setStore(Consumer<ScmConfiguration> store) {
|
||||||
|
this.store = store;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the global scm config.
|
* Returns the global scm config.
|
||||||
*/
|
*/
|
||||||
@@ -137,7 +146,7 @@ public class ConfigResource {
|
|||||||
ScmConfiguration config = dtoToConfigMapper.map(configDto);
|
ScmConfiguration config = dtoToConfigMapper.map(configDto);
|
||||||
synchronized (ScmConfiguration.class) {
|
synchronized (ScmConfiguration.class) {
|
||||||
configuration.load(config);
|
configuration.load(config);
|
||||||
ScmConfigurationUtil.getInstance().store(configuration);
|
store.accept(configuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response.noContent().build();
|
return Response.noContent().build();
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import sonia.scm.lifecycle.Restarter;
|
||||||
import sonia.scm.plugin.AvailablePlugin;
|
import sonia.scm.plugin.AvailablePlugin;
|
||||||
import sonia.scm.plugin.InstalledPlugin;
|
import sonia.scm.plugin.InstalledPlugin;
|
||||||
import sonia.scm.plugin.PluginManager;
|
import sonia.scm.plugin.PluginManager;
|
||||||
@@ -56,12 +57,14 @@ public class PendingPluginResource {
|
|||||||
private final PluginManager pluginManager;
|
private final PluginManager pluginManager;
|
||||||
private final ResourceLinks resourceLinks;
|
private final ResourceLinks resourceLinks;
|
||||||
private final PluginDtoMapper mapper;
|
private final PluginDtoMapper mapper;
|
||||||
|
private final Restarter restarter;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public PendingPluginResource(PluginManager pluginManager, ResourceLinks resourceLinks, PluginDtoMapper mapper) {
|
public PendingPluginResource(PluginManager pluginManager, ResourceLinks resourceLinks, PluginDtoMapper mapper, Restarter restarter) {
|
||||||
this.pluginManager = pluginManager;
|
this.pluginManager = pluginManager;
|
||||||
this.resourceLinks = resourceLinks;
|
this.resourceLinks = resourceLinks;
|
||||||
this.mapper = mapper;
|
this.mapper = mapper;
|
||||||
|
this.restarter = restarter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@@ -118,7 +121,9 @@ public class PendingPluginResource {
|
|||||||
PluginPermissions.manage().isPermitted() &&
|
PluginPermissions.manage().isPermitted() &&
|
||||||
(!installDtos.isEmpty() || !updateDtos.isEmpty() || !uninstallDtos.isEmpty())
|
(!installDtos.isEmpty() || !updateDtos.isEmpty() || !uninstallDtos.isEmpty())
|
||||||
) {
|
) {
|
||||||
|
if (restarter.isSupported()) {
|
||||||
linksBuilder.single(link("execute", resourceLinks.pendingPluginCollection().executePending()));
|
linksBuilder.single(link("execute", resourceLinks.pendingPluginCollection().executePending()));
|
||||||
|
}
|
||||||
linksBuilder.single(link("cancel", resourceLinks.pendingPluginCollection().cancelPending()));
|
linksBuilder.single(link("cancel", resourceLinks.pendingPluginCollection().cancelPending()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ package sonia.scm.api.v2.resources;
|
|||||||
import de.otto.edison.hal.Links;
|
import de.otto.edison.hal.Links;
|
||||||
import org.mapstruct.Mapper;
|
import org.mapstruct.Mapper;
|
||||||
import org.mapstruct.MappingTarget;
|
import org.mapstruct.MappingTarget;
|
||||||
|
import sonia.scm.lifecycle.Restarter;
|
||||||
import sonia.scm.plugin.AvailablePlugin;
|
import sonia.scm.plugin.AvailablePlugin;
|
||||||
import sonia.scm.plugin.InstalledPlugin;
|
import sonia.scm.plugin.InstalledPlugin;
|
||||||
import sonia.scm.plugin.Plugin;
|
import sonia.scm.plugin.Plugin;
|
||||||
@@ -47,6 +48,9 @@ public abstract class PluginDtoMapper {
|
|||||||
@Inject
|
@Inject
|
||||||
private ResourceLinks resourceLinks;
|
private ResourceLinks resourceLinks;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private Restarter restarter;
|
||||||
|
|
||||||
public abstract void map(PluginInformation plugin, @MappingTarget PluginDto dto);
|
public abstract void map(PluginInformation plugin, @MappingTarget PluginDto dto);
|
||||||
|
|
||||||
public PluginDto mapInstalled(InstalledPlugin plugin, List<AvailablePlugin> availablePlugins) {
|
public PluginDto mapInstalled(InstalledPlugin plugin, List<AvailablePlugin> availablePlugins) {
|
||||||
@@ -78,12 +82,20 @@ public abstract class PluginDtoMapper {
|
|||||||
.self(information.getName()));
|
.self(information.getName()));
|
||||||
|
|
||||||
if (!plugin.isPending() && PluginPermissions.manage().isPermitted()) {
|
if (!plugin.isPending() && PluginPermissions.manage().isPermitted()) {
|
||||||
links.single(link("install", resourceLinks.availablePlugin().install(information.getName())));
|
String href = resourceLinks.availablePlugin().install(information.getName());
|
||||||
|
appendLink(links, "install", href);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new PluginDto(links.build());
|
return new PluginDto(links.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void appendLink(Links.Builder links, String name, String href) {
|
||||||
|
links.single(link(name, href));
|
||||||
|
if (restarter.isSupported()) {
|
||||||
|
links.single(link(name + "WithRestart", href + "?restart=true"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private PluginDto createDtoForInstalled(InstalledPlugin plugin, List<AvailablePlugin> availablePlugins) {
|
private PluginDto createDtoForInstalled(InstalledPlugin plugin, List<AvailablePlugin> availablePlugins) {
|
||||||
PluginInformation information = plugin.getDescriptor().getInformation();
|
PluginInformation information = plugin.getDescriptor().getInformation();
|
||||||
Optional<AvailablePlugin> availablePlugin = checkForUpdates(plugin, availablePlugins);
|
Optional<AvailablePlugin> availablePlugin = checkForUpdates(plugin, availablePlugins);
|
||||||
@@ -96,13 +108,16 @@ public abstract class PluginDtoMapper {
|
|||||||
&& !availablePlugin.get().isPending()
|
&& !availablePlugin.get().isPending()
|
||||||
&& PluginPermissions.manage().isPermitted()
|
&& PluginPermissions.manage().isPermitted()
|
||||||
) {
|
) {
|
||||||
links.single(link("update", resourceLinks.availablePlugin().install(information.getName())));
|
String href = resourceLinks.availablePlugin().install(information.getName());
|
||||||
|
appendLink(links, "update", href);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (plugin.isUninstallable()
|
if (plugin.isUninstallable()
|
||||||
&& (!availablePlugin.isPresent() || !availablePlugin.get().isPending())
|
&& (!availablePlugin.isPresent() || !availablePlugin.get().isPending())
|
||||||
&& PluginPermissions.manage().isPermitted()
|
&& PluginPermissions.manage().isPermitted()
|
||||||
) {
|
) {
|
||||||
links.single(link("uninstall", resourceLinks.installedPlugin().uninstall(information.getName())));
|
String href = resourceLinks.installedPlugin().uninstall(information.getName());
|
||||||
|
appendLink(links, "uninstall", href);
|
||||||
}
|
}
|
||||||
|
|
||||||
PluginDto dto = new PluginDto(links.build());
|
PluginDto dto = new PluginDto(links.build());
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import sonia.scm.event.ScmEventBus;
|
|||||||
import javax.servlet.FilterConfig;
|
import javax.servlet.FilterConfig;
|
||||||
import javax.servlet.ServletContextEvent;
|
import javax.servlet.ServletContextEvent;
|
||||||
import javax.servlet.ServletException;
|
import javax.servlet.ServletException;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
//~--- JDK imports ------------------------------------------------------------
|
||||||
|
|
||||||
@@ -93,12 +94,16 @@ public class BootstrapContextFilter extends GuiceFilter {
|
|||||||
if (filterConfig == null) {
|
if (filterConfig == null) {
|
||||||
LOG.error("filter config is null, scm-manager is not initialized");
|
LOG.error("filter config is null, scm-manager is not initialized");
|
||||||
} else {
|
} else {
|
||||||
RestartStrategy restartStrategy = RestartStrategy.get(webAppClassLoader);
|
Optional<RestartStrategy> restartStrategy = RestartStrategy.get(webAppClassLoader);
|
||||||
restartStrategy.restart(new GuiceInjectionContext());
|
if (restartStrategy.isPresent()) {
|
||||||
|
restartStrategy.get().restart(new GuiceInjectionContext());
|
||||||
|
} else {
|
||||||
|
LOG.warn("restarting is not supported by the underlying platform");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class GuiceInjectionContext implements RestartStrategy.InjectionContext {
|
private class GuiceInjectionContext implements RestartStrategy.InternalInjectionContext {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
|
|||||||
45
scm-webapp/src/main/java/sonia/scm/lifecycle/CLibrary.java
Normal file
45
scm-webapp/src/main/java/sonia/scm/lifecycle/CLibrary.java
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
package sonia.scm.lifecycle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for native c library.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({
|
||||||
|
"squid:S1214", // usage as constant is common practice for jna
|
||||||
|
"squid:S1191" // use of sun.* classes is required for jna
|
||||||
|
})
|
||||||
|
interface CLibrary extends com.sun.jna.Library {
|
||||||
|
CLibrary LIBC = com.sun.jna.Native.load("c", CLibrary.class);
|
||||||
|
|
||||||
|
int F_GETFD = 1;
|
||||||
|
int F_SETFD = 2;
|
||||||
|
int FD_CLOEXEC = 1;
|
||||||
|
|
||||||
|
int getdtablesize();
|
||||||
|
int fcntl(int fd, int command);
|
||||||
|
int fcntl(int fd, int command, int flags);
|
||||||
|
int execvp(String file, com.sun.jna.StringArray args);
|
||||||
|
String strerror(int errno);
|
||||||
|
}
|
||||||
@@ -21,48 +21,44 @@
|
|||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
package sonia.scm.lifecycle;
|
||||||
|
|
||||||
package sonia.scm.lifecycle.classloading;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import sonia.scm.event.ScmEventBus;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import javax.inject.Inject;
|
||||||
import org.slf4j.LoggerFactory;
|
import javax.inject.Singleton;
|
||||||
import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventor;
|
|
||||||
|
|
||||||
/**
|
@Singleton
|
||||||
* Logging adapter for {@link ClassLoaderLeakPreventor}.
|
public class DefaultRestarter implements Restarter {
|
||||||
*/
|
|
||||||
public class LoggingAdapter implements se.jiderhamn.classloader.leak.prevention.Logger {
|
|
||||||
|
|
||||||
@SuppressWarnings("squid:S3416") // suppress "loggers should be named for their enclosing classes" rule
|
private ScmEventBus eventBus;
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(ClassLoaderLeakPreventor.class);
|
private RestartStrategy strategy;
|
||||||
|
|
||||||
@Override
|
@Inject
|
||||||
public void debug(String msg) {
|
public DefaultRestarter() {
|
||||||
LOG.debug(msg);
|
this(
|
||||||
|
ScmEventBus.getInstance(),
|
||||||
|
RestartStrategy.get(Thread.currentThread().getContextClassLoader()).orElse(null)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
DefaultRestarter(ScmEventBus eventBus, RestartStrategy strategy) {
|
||||||
|
this.eventBus = eventBus;
|
||||||
|
this.strategy = strategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void info(String msg) {
|
public boolean isSupported() {
|
||||||
LOG.info(msg);
|
return strategy != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void warn(String msg) {
|
public void restart(Class<?> cause, String reason) {
|
||||||
LOG.warn(msg);
|
if (!isSupported()) {
|
||||||
|
throw new RestartNotSupportedException("restarting is not supported");
|
||||||
}
|
}
|
||||||
|
eventBus.post(new RestartEvent(cause, reason));
|
||||||
@Override
|
|
||||||
public void warn(Throwable t) {
|
|
||||||
LOG.warn(t.getMessage(), t);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void error(String msg) {
|
|
||||||
LOG.error(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void error(Throwable t) {
|
|
||||||
LOG.error(t.getMessage(), t);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
package sonia.scm.lifecycle;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.function.IntConsumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link RestartStrategy} which tears down the scm-manager context and
|
||||||
|
* then exists the java process with {@link System#exit(int)}.
|
||||||
|
* <p>
|
||||||
|
* This is useful if an external mechanism is able to restart the process after it has exited.
|
||||||
|
*/
|
||||||
|
class ExitRestartStrategy extends RestartStrategy {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(ExitRestartStrategy.class);
|
||||||
|
|
||||||
|
static final String NAME = "exit";
|
||||||
|
|
||||||
|
static final String PROPERTY_EXIT_CODE = "sonia.scm.restart.exit-code";
|
||||||
|
|
||||||
|
private IntConsumer exiter = System::exit;
|
||||||
|
|
||||||
|
private int exitCode;
|
||||||
|
|
||||||
|
ExitRestartStrategy() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
void setExiter(IntConsumer exiter) {
|
||||||
|
this.exiter = exiter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void prepareRestart(InjectionContext context) {
|
||||||
|
exitCode = determineExitCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void executeRestart(InjectionContext context) {
|
||||||
|
LOG.warn("exit scm-manager with exit code {}", exitCode);
|
||||||
|
exiter.accept(exitCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int determineExitCode() {
|
||||||
|
String exitCodeAsString = System.getProperty(PROPERTY_EXIT_CODE, "0");
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(exitCodeAsString);
|
||||||
|
} catch (NumberFormatException ex) {
|
||||||
|
throw new RestartNotSupportedException("invalid exit code " + exitCodeAsString, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
/*
|
|
||||||
* MIT License
|
|
||||||
*
|
|
||||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package sonia.scm.lifecycle;
|
|
||||||
|
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import sonia.scm.event.RecreateEventBusEvent;
|
|
||||||
import sonia.scm.event.ScmEventBus;
|
|
||||||
import sonia.scm.event.ShutdownEventBusEvent;
|
|
||||||
|
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restart strategy implementation which destroy the injection context and re initialize it.
|
|
||||||
*/
|
|
||||||
public class InjectionContextRestartStrategy implements RestartStrategy {
|
|
||||||
|
|
||||||
private static final String DISABLE_RESTART_PROPERTY = "sonia.scm.restart.disable";
|
|
||||||
private static final String WAIT_PROPERTY = "sonia.scm.restart.wait";
|
|
||||||
private static final String DISABLE_GC_PROPERTY = "sonia.scm.restart.disable-gc";
|
|
||||||
|
|
||||||
private static final AtomicLong INSTANCE_COUNTER = new AtomicLong();
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(InjectionContextRestartStrategy.class);
|
|
||||||
|
|
||||||
private boolean restartEnabled = !Boolean.getBoolean(DISABLE_RESTART_PROPERTY);
|
|
||||||
private long waitInMs = Integer.getInteger(WAIT_PROPERTY, 250);
|
|
||||||
private boolean gcEnabled = !Boolean.getBoolean(DISABLE_GC_PROPERTY);
|
|
||||||
|
|
||||||
private final ClassLoader webAppClassLoader;
|
|
||||||
|
|
||||||
InjectionContextRestartStrategy(ClassLoader webAppClassLoader) {
|
|
||||||
this.webAppClassLoader = webAppClassLoader;
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
void setWaitInMs(long waitInMs) {
|
|
||||||
this.waitInMs = waitInMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
void setGcEnabled(boolean gcEnabled) {
|
|
||||||
this.gcEnabled = gcEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void restart(InjectionContext context) {
|
|
||||||
stop(context);
|
|
||||||
if (restartEnabled) {
|
|
||||||
start(context);
|
|
||||||
} else {
|
|
||||||
LOG.warn("restarting context is disabled");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("squid:S1215") // suppress explicit gc call warning
|
|
||||||
private void start(InjectionContext context) {
|
|
||||||
LOG.debug("use WebAppClassLoader as ContextClassLoader, to avoid ClassLoader leaks");
|
|
||||||
Thread.currentThread().setContextClassLoader(webAppClassLoader);
|
|
||||||
|
|
||||||
LOG.warn("send recreate eventbus event");
|
|
||||||
ScmEventBus.getInstance().post(new RecreateEventBusEvent());
|
|
||||||
|
|
||||||
// restart context delayed, to avoid timing problems
|
|
||||||
new Thread(() -> {
|
|
||||||
try {
|
|
||||||
if (gcEnabled){
|
|
||||||
LOG.info("call gc to clean up memory from old instances");
|
|
||||||
System.gc();
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG.info("wait {}ms before re starting the context", waitInMs);
|
|
||||||
Thread.sleep(waitInMs);
|
|
||||||
|
|
||||||
LOG.warn("reinitialize injection context");
|
|
||||||
context.initialize();
|
|
||||||
|
|
||||||
LOG.debug("register injection context on new eventbus");
|
|
||||||
ScmEventBus.getInstance().register(context);
|
|
||||||
} catch ( Exception ex) {
|
|
||||||
LOG.error("failed to restart", ex);
|
|
||||||
}
|
|
||||||
}, "Delayed-Restart-" + INSTANCE_COUNTER.incrementAndGet()).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void stop(InjectionContext context) {
|
|
||||||
LOG.warn("destroy injection context");
|
|
||||||
context.destroy();
|
|
||||||
|
|
||||||
if (!restartEnabled) {
|
|
||||||
// shutdown eventbus, but do this only if restart is disabled
|
|
||||||
ScmEventBus.getInstance().post(new ShutdownEventBusEvent());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
package sonia.scm.lifecycle;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import static sonia.scm.lifecycle.CLibrary.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restart strategy which uses execvp from libc. This strategy is only supported on posix base operating systems.
|
||||||
|
*/
|
||||||
|
class PosixRestartStrategy extends RestartStrategy {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(PosixRestartStrategy.class);
|
||||||
|
|
||||||
|
PosixRestartStrategy() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void executeRestart(InjectionContext context) {
|
||||||
|
LOG.warn("restart scm-manager jvm process");
|
||||||
|
try {
|
||||||
|
restart();
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.error("failed to collect java vm arguments", e);
|
||||||
|
LOG.error("we will now exit the java process");
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("squid:S1191") // use of sun.* classes is required for jna)
|
||||||
|
private static void restart() throws IOException {
|
||||||
|
com.sun.akuma.JavaVMArguments args = com.sun.akuma.JavaVMArguments.current();
|
||||||
|
args.remove("--daemon");
|
||||||
|
|
||||||
|
int sz = LIBC.getdtablesize();
|
||||||
|
for(int i=3; i<sz; i++) {
|
||||||
|
int flags = LIBC.fcntl(i, F_GETFD);
|
||||||
|
if(flags<0) continue;
|
||||||
|
LIBC.fcntl(i, F_SETFD,flags| FD_CLOEXEC);
|
||||||
|
}
|
||||||
|
|
||||||
|
// exec to self
|
||||||
|
String exe = args.get(0);
|
||||||
|
LIBC.execvp(exe, new com.sun.jna.StringArray(args.toArray(new String[0])));
|
||||||
|
throw new IOException("Failed to exec '"+exe+"' "+LIBC.strerror(com.sun.jna.Native.getLastError()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,19 +24,20 @@
|
|||||||
|
|
||||||
package sonia.scm.lifecycle;
|
package sonia.scm.lifecycle;
|
||||||
|
|
||||||
/**
|
import org.slf4j.Logger;
|
||||||
* Strategy for restarting SCM-Manager.
|
import org.slf4j.LoggerFactory;
|
||||||
*/
|
|
||||||
public interface RestartStrategy {
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Context for Injection in SCM-Manager.
|
* Strategy for restarting SCM-Manager. Implementations must either have a default constructor or one taking the
|
||||||
|
* class loader for the current context as a single argument.
|
||||||
*/
|
*/
|
||||||
interface InjectionContext {
|
public abstract class RestartStrategy {
|
||||||
/**
|
|
||||||
* Initialize the injection context.
|
private static final Logger LOG = LoggerFactory.getLogger(RestartStrategy.class);
|
||||||
*/
|
|
||||||
void initialize();
|
interface InternalInjectionContext extends InjectionContext {
|
||||||
/**
|
/**
|
||||||
* Destroys the injection context.
|
* Destroys the injection context.
|
||||||
*/
|
*/
|
||||||
@@ -44,18 +45,52 @@ public interface RestartStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restart SCM-Manager.
|
* Context for Injection in SCM-Manager.
|
||||||
* @param context injection context
|
|
||||||
*/
|
*/
|
||||||
void restart(InjectionContext context);
|
public interface InjectionContext {
|
||||||
|
/**
|
||||||
|
* Initialize the injection context.
|
||||||
|
*/
|
||||||
|
void initialize();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the configured strategy.
|
* Restart SCM-Manager by first calling {@link #prepareRestart(InjectionContext)}, destroying the
|
||||||
|
* current context, and finally calling {@link #executeRestart(InjectionContext)}.
|
||||||
*
|
*
|
||||||
* @return configured strategy
|
* @param context injection context
|
||||||
*/
|
*/
|
||||||
static RestartStrategy get(ClassLoader webAppClassLoader) {
|
public final void restart(InternalInjectionContext context) {
|
||||||
return new InjectionContextRestartStrategy(webAppClassLoader);
|
prepareRestart(context);
|
||||||
|
LOG.warn("destroy injection context");
|
||||||
|
context.destroy();
|
||||||
|
executeRestart(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare the restart of SCM-Manager. Here you can check whether restart is possible and,
|
||||||
|
* if necessary, throw a {@link RestartNotSupportedException} to abort the restart.
|
||||||
|
*
|
||||||
|
* @param context injection context
|
||||||
|
*/
|
||||||
|
protected void prepareRestart(InjectionContext context) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actually restart SCM-Manager.
|
||||||
|
*
|
||||||
|
* @param context injection context
|
||||||
|
*/
|
||||||
|
protected abstract void executeRestart(InjectionContext context);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the configured strategy or empty if restart is not supported by the underlying platform.
|
||||||
|
*
|
||||||
|
* @param webAppClassLoader root webapp classloader
|
||||||
|
* @return configured strategy or empty optional
|
||||||
|
*/
|
||||||
|
static Optional<RestartStrategy> get(ClassLoader webAppClassLoader) {
|
||||||
|
return Optional.ofNullable(RestartStrategyFactory.create(webAppClassLoader));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
package sonia.scm.lifecycle;
|
||||||
|
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import sonia.scm.PlatformType;
|
||||||
|
import sonia.scm.util.SystemUtil;
|
||||||
|
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
|
||||||
|
final class RestartStrategyFactory {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* System property to load a specific restart strategy.
|
||||||
|
*/
|
||||||
|
static final String PROPERTY_STRATEGY = "sonia.scm.lifecycle.restart-strategy";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No restart supported.
|
||||||
|
*/
|
||||||
|
static final String STRATEGY_NONE = "none";
|
||||||
|
|
||||||
|
private RestartStrategyFactory() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the configured strategy or {@code null} if restart is not supported.
|
||||||
|
*
|
||||||
|
* @param webAppClassLoader root webapp classloader
|
||||||
|
* @return configured strategy or {@code null}
|
||||||
|
*/
|
||||||
|
static RestartStrategy create(ClassLoader webAppClassLoader) {
|
||||||
|
String property = System.getProperty(PROPERTY_STRATEGY);
|
||||||
|
if (Strings.isNullOrEmpty(property)) {
|
||||||
|
return forPlatform();
|
||||||
|
}
|
||||||
|
return fromProperty(webAppClassLoader, property);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RestartStrategy fromProperty(ClassLoader webAppClassLoader, String property) {
|
||||||
|
if (STRATEGY_NONE.equalsIgnoreCase(property)) {
|
||||||
|
return null;
|
||||||
|
} else if (ExitRestartStrategy.NAME.equalsIgnoreCase(property)) {
|
||||||
|
return new ExitRestartStrategy();
|
||||||
|
} else {
|
||||||
|
return fromClassName(property, webAppClassLoader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RestartStrategy fromClassName(String className, ClassLoader classLoader) {
|
||||||
|
try {
|
||||||
|
Class<? extends RestartStrategy> rsClass = Class.forName(className).asSubclass(RestartStrategy.class);
|
||||||
|
return createInstance(rsClass, classLoader);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RestartNotSupportedException("failed to create restart strategy from property", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RestartStrategy createInstance(Class<? extends RestartStrategy> rsClass, ClassLoader classLoader) throws InstantiationException, IllegalAccessException, java.lang.reflect.InvocationTargetException, NoSuchMethodException {
|
||||||
|
try {
|
||||||
|
Constructor<? extends RestartStrategy> constructor = rsClass.getConstructor(ClassLoader.class);
|
||||||
|
return constructor.newInstance(classLoader);
|
||||||
|
} catch (NoSuchMethodException ex) {
|
||||||
|
return rsClass.getConstructor().newInstance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RestartStrategy forPlatform() {
|
||||||
|
// we do not use SystemUtil here, to allow testing
|
||||||
|
String osName = System.getProperty(SystemUtil.PROPERTY_OSNAME);
|
||||||
|
PlatformType platform = PlatformType.createPlatformType(osName);
|
||||||
|
if (platform.isPosix()) {
|
||||||
|
return new PosixRestartStrategy();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,6 @@
|
|||||||
|
|
||||||
package sonia.scm.lifecycle.classloading;
|
package sonia.scm.lifecycle.classloading;
|
||||||
|
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import sonia.scm.lifecycle.LifeCycle;
|
import sonia.scm.lifecycle.LifeCycle;
|
||||||
@@ -42,19 +41,11 @@ public abstract class ClassLoaderLifeCycle implements LifeCycle {
|
|||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(ClassLoaderLifeCycle.class);
|
private static final Logger LOG = LoggerFactory.getLogger(ClassLoaderLifeCycle.class);
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
static final String PROPERTY = "sonia.scm.classloading.lifecycle";
|
|
||||||
|
|
||||||
public static ClassLoaderLifeCycle create() {
|
public static ClassLoaderLifeCycle create() {
|
||||||
ClassLoader webappClassLoader = Thread.currentThread().getContextClassLoader();
|
ClassLoader webappClassLoader = Thread.currentThread().getContextClassLoader();
|
||||||
String implementation = System.getProperty(PROPERTY);
|
|
||||||
if (SimpleClassLoaderLifeCycle.NAME.equalsIgnoreCase(implementation)) {
|
|
||||||
LOG.info("create new simple ClassLoaderLifeCycle");
|
LOG.info("create new simple ClassLoaderLifeCycle");
|
||||||
return new SimpleClassLoaderLifeCycle(webappClassLoader);
|
return new SimpleClassLoaderLifeCycle(webappClassLoader);
|
||||||
}
|
}
|
||||||
LOG.info("create new ClassLoaderLifeCycle with leak prevention");
|
|
||||||
return new ClassLoaderLifeCycleWithLeakPrevention(webappClassLoader);
|
|
||||||
}
|
|
||||||
|
|
||||||
private final ClassLoader webappClassLoader;
|
private final ClassLoader webappClassLoader;
|
||||||
|
|
||||||
|
|||||||
@@ -1,187 +0,0 @@
|
|||||||
/*
|
|
||||||
* MIT License
|
|
||||||
*
|
|
||||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package sonia.scm.lifecycle.classloading;
|
|
||||||
|
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventor;
|
|
||||||
import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventorFactory;
|
|
||||||
import se.jiderhamn.classloader.leak.prevention.cleanup.IIOServiceProviderCleanUp;
|
|
||||||
import se.jiderhamn.classloader.leak.prevention.cleanup.MBeanCleanUp;
|
|
||||||
import se.jiderhamn.classloader.leak.prevention.cleanup.ShutdownHookCleanUp;
|
|
||||||
import se.jiderhamn.classloader.leak.prevention.cleanup.StopThreadsCleanUp;
|
|
||||||
import se.jiderhamn.classloader.leak.prevention.preinit.AwtToolkitInitiator;
|
|
||||||
import se.jiderhamn.classloader.leak.prevention.preinit.Java2dDisposerInitiator;
|
|
||||||
import se.jiderhamn.classloader.leak.prevention.preinit.Java2dRenderQueueInitiator;
|
|
||||||
import se.jiderhamn.classloader.leak.prevention.preinit.SunAwtAppContextInitiator;
|
|
||||||
|
|
||||||
import java.io.Closeable;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayDeque;
|
|
||||||
import java.util.Deque;
|
|
||||||
|
|
||||||
import static se.jiderhamn.classloader.leak.prevention.cleanup.ShutdownHookCleanUp.SHUTDOWN_HOOK_WAIT_MS_DEFAULT;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates and shutdown SCM-Manager ClassLoaders with ClassLoader leak detection.
|
|
||||||
*/
|
|
||||||
final class ClassLoaderLifeCycleWithLeakPrevention extends ClassLoaderLifeCycle {
|
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(ClassLoaderLifeCycleWithLeakPrevention.class);
|
|
||||||
|
|
||||||
private Deque<ClassLoaderAndPreventor> classLoaders = new ArrayDeque<>();
|
|
||||||
|
|
||||||
private final ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory;
|
|
||||||
|
|
||||||
private ClassLoaderAppendListener classLoaderAppendListener = new ClassLoaderAppendListener() {
|
|
||||||
@Override
|
|
||||||
public <C extends ClassLoader> C apply(C classLoader) {
|
|
||||||
return classLoader;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ClassLoaderLifeCycleWithLeakPrevention(ClassLoader webappClassLoader) {
|
|
||||||
this(webappClassLoader, createClassLoaderLeakPreventorFactory(webappClassLoader));
|
|
||||||
}
|
|
||||||
|
|
||||||
ClassLoaderLifeCycleWithLeakPrevention(ClassLoader webappClassLoader, ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory) {
|
|
||||||
super(webappClassLoader);
|
|
||||||
this.classLoaderLeakPreventorFactory = classLoaderLeakPreventorFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ClassLoaderLeakPreventorFactory createClassLoaderLeakPreventorFactory(ClassLoader webappClassLoader) {
|
|
||||||
// Should threads tied to the web app classloader be forced to stop at application shutdown?
|
|
||||||
boolean stopThreads = Boolean.getBoolean("ClassLoaderLeakPreventor.stopThreads");
|
|
||||||
|
|
||||||
// Should Timer threads tied to the web app classloader be forced to stop at application shutdown?
|
|
||||||
boolean stopTimerThreads = Boolean.getBoolean("ClassLoaderLeakPreventor.stopTimerThreads");
|
|
||||||
|
|
||||||
// Should shutdown hooks registered from the application be executed at application shutdown?
|
|
||||||
boolean executeShutdownHooks = Boolean.getBoolean("ClassLoaderLeakPreventor.executeShutdownHooks");
|
|
||||||
|
|
||||||
// No of milliseconds to wait for threads to finish execution, before stopping them.
|
|
||||||
int threadWaitMs = Integer.getInteger("ClassLoaderLeakPreventor.threadWaitMs", ClassLoaderLeakPreventor.THREAD_WAIT_MS_DEFAULT);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* No of milliseconds to wait for shutdown hooks to finish execution, before stopping them.
|
|
||||||
* If set to -1 there will be no waiting at all, but Thread is allowed to run until finished.
|
|
||||||
*/
|
|
||||||
int shutdownHookWaitMs = Integer.getInteger("ClassLoaderLeakPreventor.shutdownHookWaitMs", SHUTDOWN_HOOK_WAIT_MS_DEFAULT);
|
|
||||||
|
|
||||||
LOG.info("Settings for {} (CL: 0x{}):", ClassLoaderLifeCycleWithLeakPrevention.class.getName(), Integer.toHexString(System.identityHashCode(webappClassLoader)) );
|
|
||||||
LOG.info(" stopThreads = {}", stopThreads);
|
|
||||||
LOG.info(" stopTimerThreads = {}", stopTimerThreads);
|
|
||||||
LOG.info(" executeShutdownHooks = {}", executeShutdownHooks);
|
|
||||||
LOG.info(" threadWaitMs = {} ms", threadWaitMs);
|
|
||||||
LOG.info(" shutdownHookWaitMs = {} ms", shutdownHookWaitMs);
|
|
||||||
|
|
||||||
// use webapp classloader as safe base? or system?
|
|
||||||
ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory = new ClassLoaderLeakPreventorFactory(webappClassLoader);
|
|
||||||
classLoaderLeakPreventorFactory.setLogger(new LoggingAdapter());
|
|
||||||
|
|
||||||
final ShutdownHookCleanUp shutdownHookCleanUp = classLoaderLeakPreventorFactory.getCleanUp(ShutdownHookCleanUp.class);
|
|
||||||
shutdownHookCleanUp.setExecuteShutdownHooks(executeShutdownHooks);
|
|
||||||
shutdownHookCleanUp.setShutdownHookWaitMs(shutdownHookWaitMs);
|
|
||||||
|
|
||||||
final StopThreadsCleanUp stopThreadsCleanUp = classLoaderLeakPreventorFactory.getCleanUp(StopThreadsCleanUp.class);
|
|
||||||
stopThreadsCleanUp.setStopThreads(stopThreads);
|
|
||||||
stopThreadsCleanUp.setStopTimerThreads(stopTimerThreads);
|
|
||||||
stopThreadsCleanUp.setThreadWaitMs(threadWaitMs);
|
|
||||||
|
|
||||||
// remove awt and imageio cleanup
|
|
||||||
classLoaderLeakPreventorFactory.removePreInitiator(AwtToolkitInitiator.class);
|
|
||||||
classLoaderLeakPreventorFactory.removePreInitiator(SunAwtAppContextInitiator.class);
|
|
||||||
classLoaderLeakPreventorFactory.removeCleanUp(IIOServiceProviderCleanUp.class);
|
|
||||||
classLoaderLeakPreventorFactory.removePreInitiator(Java2dRenderQueueInitiator.class);
|
|
||||||
classLoaderLeakPreventorFactory.removePreInitiator(Java2dDisposerInitiator.class);
|
|
||||||
|
|
||||||
// the MBeanCleanUp causes a Exception and we use no mbeans
|
|
||||||
classLoaderLeakPreventorFactory.removeCleanUp(MBeanCleanUp.class);
|
|
||||||
|
|
||||||
return classLoaderLeakPreventorFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
void setClassLoaderAppendListener(ClassLoaderAppendListener classLoaderAppendListener) {
|
|
||||||
this.classLoaderAppendListener = classLoaderAppendListener;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void shutdownClassLoaders() {
|
|
||||||
ClassLoaderAndPreventor clap = classLoaders.poll();
|
|
||||||
while (clap != null) {
|
|
||||||
clap.shutdown();
|
|
||||||
clap = classLoaders.poll();
|
|
||||||
}
|
|
||||||
// be sure it is realy empty
|
|
||||||
classLoaders.clear();
|
|
||||||
classLoaders = new ArrayDeque<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected <T extends ClassLoader> T initAndAppend(T originalClassLoader) {
|
|
||||||
LOG.debug("init classloader {}", originalClassLoader);
|
|
||||||
T classLoader = classLoaderAppendListener.apply(originalClassLoader);
|
|
||||||
|
|
||||||
ClassLoaderLeakPreventor preventor = classLoaderLeakPreventorFactory.newLeakPreventor(classLoader);
|
|
||||||
preventor.runPreClassLoaderInitiators();
|
|
||||||
classLoaders.push(new ClassLoaderAndPreventor(classLoader, preventor));
|
|
||||||
|
|
||||||
return classLoader;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ClassLoaderAppendListener {
|
|
||||||
<C extends ClassLoader> C apply(C classLoader);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class ClassLoaderAndPreventor {
|
|
||||||
|
|
||||||
private final ClassLoader classLoader;
|
|
||||||
private final ClassLoaderLeakPreventor preventor;
|
|
||||||
|
|
||||||
private ClassLoaderAndPreventor(ClassLoader classLoader, ClassLoaderLeakPreventor preventor) {
|
|
||||||
this.classLoader = classLoader;
|
|
||||||
this.preventor = preventor;
|
|
||||||
}
|
|
||||||
|
|
||||||
void shutdown() {
|
|
||||||
LOG.debug("shutdown classloader {}", classLoader);
|
|
||||||
preventor.runCleanUps();
|
|
||||||
close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void close() {
|
|
||||||
if (classLoader instanceof Closeable) {
|
|
||||||
LOG.trace("close classloader {}", classLoader);
|
|
||||||
try {
|
|
||||||
((Closeable) classLoader).close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOG.warn("failed to close classloader", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -33,6 +33,8 @@ import sonia.scm.SCMContext;
|
|||||||
import sonia.scm.SCMContextProvider;
|
import sonia.scm.SCMContextProvider;
|
||||||
import sonia.scm.io.DefaultFileSystem;
|
import sonia.scm.io.DefaultFileSystem;
|
||||||
import sonia.scm.io.FileSystem;
|
import sonia.scm.io.FileSystem;
|
||||||
|
import sonia.scm.lifecycle.DefaultRestarter;
|
||||||
|
import sonia.scm.lifecycle.Restarter;
|
||||||
import sonia.scm.plugin.PluginLoader;
|
import sonia.scm.plugin.PluginLoader;
|
||||||
import sonia.scm.repository.RepositoryLocationResolver;
|
import sonia.scm.repository.RepositoryLocationResolver;
|
||||||
import sonia.scm.repository.xml.MetadataStore;
|
import sonia.scm.repository.xml.MetadataStore;
|
||||||
@@ -85,6 +87,8 @@ public class BootstrapModule extends AbstractModule {
|
|||||||
|
|
||||||
bind(FileSystem.class, DefaultFileSystem.class);
|
bind(FileSystem.class, DefaultFileSystem.class);
|
||||||
|
|
||||||
|
bind(Restarter.class, DefaultRestarter.class);
|
||||||
|
|
||||||
// note CipherUtil uses an other generator
|
// note CipherUtil uses an other generator
|
||||||
bind(CipherHandler.class).toInstance(CipherUtil.getInstance().getCipherHandler());
|
bind(CipherHandler.class).toInstance(CipherUtil.getInstance().getCipherHandler());
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
import sonia.scm.NotFoundException;
|
import sonia.scm.NotFoundException;
|
||||||
import sonia.scm.event.ScmEventBus;
|
import sonia.scm.event.ScmEventBus;
|
||||||
import sonia.scm.lifecycle.RestartEvent;
|
import sonia.scm.lifecycle.RestartEvent;
|
||||||
|
import sonia.scm.lifecycle.Restarter;
|
||||||
import sonia.scm.version.Version;
|
import sonia.scm.version.Version;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
@@ -59,20 +60,21 @@ public class DefaultPluginManager implements PluginManager {
|
|||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(DefaultPluginManager.class);
|
private static final Logger LOG = LoggerFactory.getLogger(DefaultPluginManager.class);
|
||||||
|
|
||||||
private final ScmEventBus eventBus;
|
|
||||||
private final PluginLoader loader;
|
private final PluginLoader loader;
|
||||||
private final PluginCenter center;
|
private final PluginCenter center;
|
||||||
private final PluginInstaller installer;
|
private final PluginInstaller installer;
|
||||||
|
private final Restarter restarter;
|
||||||
|
|
||||||
private final Collection<PendingPluginInstallation> pendingInstallQueue = new ArrayList<>();
|
private final Collection<PendingPluginInstallation> pendingInstallQueue = new ArrayList<>();
|
||||||
private final Collection<PendingPluginUninstallation> pendingUninstallQueue = new ArrayList<>();
|
private final Collection<PendingPluginUninstallation> pendingUninstallQueue = new ArrayList<>();
|
||||||
private final PluginDependencyTracker dependencyTracker = new PluginDependencyTracker();
|
private final PluginDependencyTracker dependencyTracker = new PluginDependencyTracker();
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public DefaultPluginManager(ScmEventBus eventBus, PluginLoader loader, PluginCenter center, PluginInstaller installer) {
|
public DefaultPluginManager(PluginLoader loader, PluginCenter center, PluginInstaller installer, Restarter restarter) {
|
||||||
this.eventBus = eventBus;
|
|
||||||
this.loader = loader;
|
this.loader = loader;
|
||||||
this.center = center;
|
this.center = center;
|
||||||
this.installer = installer;
|
this.installer = installer;
|
||||||
|
this.restarter = restarter;
|
||||||
|
|
||||||
this.computeInstallationDependencies();
|
this.computeInstallationDependencies();
|
||||||
}
|
}
|
||||||
@@ -233,16 +235,8 @@ public class DefaultPluginManager implements PluginManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
private void triggerRestart(String cause) {
|
||||||
void triggerRestart(String cause) {
|
restarter.restart(PluginManager.class, cause);
|
||||||
new Thread(() -> {
|
|
||||||
try {
|
|
||||||
Thread.sleep(200);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
}
|
|
||||||
eventBus.post(new RestartEvent(PluginManager.class, cause));
|
|
||||||
}).start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void cancelPending(List<PendingPluginInstallation> pendingInstallations) {
|
private void cancelPending(List<PendingPluginInstallation> pendingInstallations) {
|
||||||
|
|||||||
@@ -24,8 +24,18 @@
|
|||||||
|
|
||||||
package sonia.scm.plugin;
|
package sonia.scm.plugin;
|
||||||
|
|
||||||
|
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||||
|
|
||||||
public class PluginChecksumMismatchException extends PluginInstallException {
|
public class PluginChecksumMismatchException extends PluginInstallException {
|
||||||
public PluginChecksumMismatchException(String message) {
|
public PluginChecksumMismatchException(AvailablePlugin plugin, String calculatedChecksum, String expectedChecksum) {
|
||||||
super(message);
|
super(
|
||||||
|
entity("Plugin", plugin.getDescriptor().getInformation().getName()).build(),
|
||||||
|
String.format("downloaded plugin checksum %s does not match expected %s", calculatedChecksum, expectedChecksum)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCode() {
|
||||||
|
return "6mRuFxaWM1";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sonia.scm.plugin;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||||
|
|
||||||
|
public class PluginCleanupException extends PluginInstallException {
|
||||||
|
public PluginCleanupException(Path file) {
|
||||||
|
super(entity("File", file.toString()).build(), "failed to cleanup, after broken installation");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCode() {
|
||||||
|
return "8nRuFzjss1";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,8 +24,15 @@
|
|||||||
|
|
||||||
package sonia.scm.plugin;
|
package sonia.scm.plugin;
|
||||||
|
|
||||||
|
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||||
|
|
||||||
public class PluginDownloadException extends PluginInstallException {
|
public class PluginDownloadException extends PluginInstallException {
|
||||||
public PluginDownloadException(String message, Throwable cause) {
|
public PluginDownloadException(AvailablePlugin plugin, Exception cause) {
|
||||||
super(message, cause);
|
super(entity("Plugin", plugin.getDescriptor().getInformation().getName()).build(), "failed to download plugin", cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCode() {
|
||||||
|
return "9iRuFz1UB1";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,13 +24,18 @@
|
|||||||
|
|
||||||
package sonia.scm.plugin;
|
package sonia.scm.plugin;
|
||||||
|
|
||||||
public class PluginInstallException extends RuntimeException {
|
import sonia.scm.ContextEntry;
|
||||||
|
import sonia.scm.ExceptionWithContext;
|
||||||
|
|
||||||
public PluginInstallException(String message) {
|
import java.util.List;
|
||||||
super(message);
|
|
||||||
|
abstract class PluginInstallException extends ExceptionWithContext {
|
||||||
|
|
||||||
|
public PluginInstallException(List<ContextEntry> context, String message) {
|
||||||
|
super(context, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PluginInstallException(String message, Throwable cause) {
|
public PluginInstallException(List<ContextEntry> context, String message, Exception cause) {
|
||||||
super(message, cause);
|
super(context, message, cause);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class PluginInstaller {
|
|||||||
return new PendingPluginInstallation(plugin.install(), file);
|
return new PendingPluginInstallation(plugin.install(), file);
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
cleanup(file);
|
cleanup(file);
|
||||||
throw new PluginDownloadException("failed to download plugin", ex);
|
throw new PluginDownloadException(plugin, ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ class PluginInstaller {
|
|||||||
Files.deleteIfExists(file);
|
Files.deleteIfExists(file);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new PluginInstallException("failed to cleanup, after broken installation");
|
throw new PluginCleanupException(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,9 +84,7 @@ class PluginInstaller {
|
|||||||
String calculatedChecksum = hash.toString();
|
String calculatedChecksum = hash.toString();
|
||||||
if (!checksum.get().equalsIgnoreCase(calculatedChecksum)) {
|
if (!checksum.get().equalsIgnoreCase(calculatedChecksum)) {
|
||||||
cleanup(file);
|
cleanup(file);
|
||||||
throw new PluginChecksumMismatchException(
|
throw new PluginChecksumMismatchException(plugin, calculatedChecksum, checksum.get());
|
||||||
String.format("downloaded plugin checksum %s does not match expected %s", calculatedChecksum, checksum.get())
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import sonia.scm.event.ScmEventBus;
|
import sonia.scm.event.ScmEventBus;
|
||||||
import sonia.scm.lifecycle.RestartEvent;
|
import sonia.scm.lifecycle.RestartEvent;
|
||||||
|
import sonia.scm.lifecycle.Restarter;
|
||||||
import sonia.scm.update.repository.DefaultMigrationStrategyDAO;
|
import sonia.scm.update.repository.DefaultMigrationStrategyDAO;
|
||||||
import sonia.scm.update.repository.MigrationStrategy;
|
import sonia.scm.update.repository.MigrationStrategy;
|
||||||
import sonia.scm.update.repository.V1Repository;
|
import sonia.scm.update.repository.V1Repository;
|
||||||
@@ -65,11 +66,13 @@ class MigrationWizardServlet extends HttpServlet {
|
|||||||
|
|
||||||
private final XmlRepositoryV1UpdateStep repositoryV1UpdateStep;
|
private final XmlRepositoryV1UpdateStep repositoryV1UpdateStep;
|
||||||
private final DefaultMigrationStrategyDAO migrationStrategyDao;
|
private final DefaultMigrationStrategyDAO migrationStrategyDao;
|
||||||
|
private final Restarter restarter;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
MigrationWizardServlet(XmlRepositoryV1UpdateStep repositoryV1UpdateStep, DefaultMigrationStrategyDAO migrationStrategyDao) {
|
MigrationWizardServlet(XmlRepositoryV1UpdateStep repositoryV1UpdateStep, DefaultMigrationStrategyDAO migrationStrategyDao, Restarter restarter) {
|
||||||
this.repositoryV1UpdateStep = repositoryV1UpdateStep;
|
this.repositoryV1UpdateStep = repositoryV1UpdateStep;
|
||||||
this.migrationStrategyDao = migrationStrategyDao;
|
this.migrationStrategyDao = migrationStrategyDao;
|
||||||
|
this.restarter = restarter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -140,12 +143,16 @@ class MigrationWizardServlet extends HttpServlet {
|
|||||||
);
|
);
|
||||||
|
|
||||||
Map<String, Object> model = Collections.singletonMap("contextPath", req.getContextPath());
|
Map<String, Object> model = Collections.singletonMap("contextPath", req.getContextPath());
|
||||||
|
|
||||||
respondWithTemplate(resp, model, "templates/repository-migration-restart.mustache");
|
|
||||||
|
|
||||||
ThreadContext.bind(new Subject.Builder(new DefaultSecurityManager()).authenticated(false).buildSubject());
|
ThreadContext.bind(new Subject.Builder(new DefaultSecurityManager()).authenticated(false).buildSubject());
|
||||||
|
|
||||||
ScmEventBus.getInstance().post(new RestartEvent(MigrationWizardServlet.class, "wrote migration data"));
|
if (restarter.isSupported()) {
|
||||||
|
respondWithTemplate(resp, model, "templates/repository-migration-restart.mustache");
|
||||||
|
restarter.restart(MigrationWizardServlet.class, "wrote migration data");
|
||||||
|
} else {
|
||||||
|
respondWithTemplate(resp, model, "templates/repository-migration-manual-restart.mustache");
|
||||||
|
LOG.error("Restarting is not supported on this platform.");
|
||||||
|
LOG.error("Please do a manual restart");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<RepositoryLineEntry> getRepositoryLineEntries() {
|
private List<RepositoryLineEntry> getRepositoryLineEntries() {
|
||||||
|
|||||||
@@ -203,6 +203,18 @@
|
|||||||
"4GRrgkSC01": {
|
"4GRrgkSC01": {
|
||||||
"displayName": "Unerwartetes Merge-Ergebnis",
|
"displayName": "Unerwartetes Merge-Ergebnis",
|
||||||
"description": "Der Merge hatte ein unerwartetes Ergebis, das nicht automatisiert behandelt werden konnte. Nähere Details sind im Log zu finden. Führen Sie den Merge ggf. manuell durch."
|
"description": "Der Merge hatte ein unerwartetes Ergebis, das nicht automatisiert behandelt werden konnte. Nähere Details sind im Log zu finden. Führen Sie den Merge ggf. manuell durch."
|
||||||
|
},
|
||||||
|
"6mRuFxaWM1": {
|
||||||
|
"displayName": "Falsche Checksumme",
|
||||||
|
"description": "Die Checksumme des heruntergeladenen Plugins stimmt nicht mit der erwarteten Checksumme überein. Bitte versuchen Sie es erneut und prüfen Sie die Interneteinstellungen wie z. B. die Proxy-Einstellungen."
|
||||||
|
},
|
||||||
|
"9iRuFz1UB1": {
|
||||||
|
"displayName": "Fehler beim Herunterladen",
|
||||||
|
"description": "Das Plugin konnte nicht vom Server heruntergeladen werden. BitteThe plugin could not be loaded from the server. Bitte versuchen Sie es erneut und prüfen Sie die Interneteinstellungen wie z. B. die Proxy-Einstellungen. Weitere Details finden sich im Server Log."
|
||||||
|
},
|
||||||
|
"8nRuFzjss1": {
|
||||||
|
"displayName": "Fehler beim Löschen falscher Downloads",
|
||||||
|
"description": "Ein fehlerhaft heruntergeladenes Plugin konnte nicht gelöscht werden. Bitte prüfen Sie die Server Logs und löschen die Datei manuell."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"namespaceStrategies": {
|
"namespaceStrategies": {
|
||||||
|
|||||||
@@ -203,6 +203,18 @@
|
|||||||
"4GRrgkSC01": {
|
"4GRrgkSC01": {
|
||||||
"displayName": "Unexpected merge result",
|
"displayName": "Unexpected merge result",
|
||||||
"description": "The merge led to an unexpected result, that could not be handled automatically. More details could be found in the log. Please merge the branches manually."
|
"description": "The merge led to an unexpected result, that could not be handled automatically. More details could be found in the log. Please merge the branches manually."
|
||||||
|
},
|
||||||
|
"6mRuFxaWM1": {
|
||||||
|
"displayName": "Wrong checksum",
|
||||||
|
"description": "The checksum of the downloaded plugin did not match the expected checksum. Please try again or check the internet settings like proxies."
|
||||||
|
},
|
||||||
|
"9iRuFz1UB1": {
|
||||||
|
"displayName": "Could not load plugin",
|
||||||
|
"description": "The plugin could not be loaded from the server. Please try again or check the internet settings like proxies. More information can be found in the server log."
|
||||||
|
},
|
||||||
|
"8nRuFzjss1": {
|
||||||
|
"displayName": "Error while cleaning up failed plugin",
|
||||||
|
"description": "A failed plugin download could not be removed correctly. Please check the server log and remove the plugin manually."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"namespaceStrategies": {
|
"namespaceStrategies": {
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{{<layout}}
|
||||||
|
|
||||||
|
{{$title}}SCM-Manager needs to be restarted{{/title}}
|
||||||
|
|
||||||
|
{{$content}}
|
||||||
|
<p class="notification is-warning">
|
||||||
|
The migration is prepared and gets executed after SCM-Manager is restarted. <br/>
|
||||||
|
Please restart your SCM-Manager instance.
|
||||||
|
</p>
|
||||||
|
{{/content}}
|
||||||
|
|
||||||
|
{{/layout}}
|
||||||
@@ -89,6 +89,7 @@ public class ConfigResourceTest {
|
|||||||
initMocks(this);
|
initMocks(this);
|
||||||
|
|
||||||
ConfigResource configResource = new ConfigResource(dtoToConfigMapper, configToDtoMapper, createConfiguration(), namespaceStrategyValidator);
|
ConfigResource configResource = new ConfigResource(dtoToConfigMapper, configToDtoMapper, createConfiguration(), namespaceStrategyValidator);
|
||||||
|
configResource.setStore(config -> {});
|
||||||
|
|
||||||
dispatcher.addSingletonResource(configResource);
|
dispatcher.addSingletonResource(configResource);
|
||||||
}
|
}
|
||||||
@@ -128,7 +129,9 @@ public class ConfigResourceTest {
|
|||||||
|
|
||||||
request = MockHttpRequest.get("/" + ConfigResource.CONFIG_PATH_V2);
|
request = MockHttpRequest.get("/" + ConfigResource.CONFIG_PATH_V2);
|
||||||
response = new MockHttpResponse();
|
response = new MockHttpResponse();
|
||||||
dispatcher.invoke(request, response); assertEquals(HttpServletResponse.SC_OK, response.getStatus());
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
|
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
|
||||||
assertTrue(response.getContentAsString().contains("\"proxyPassword\":\"newPassword\""));
|
assertTrue(response.getContentAsString().contains("\"proxyPassword\":\"newPassword\""));
|
||||||
assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/config"));
|
assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/config"));
|
||||||
assertTrue("link not found", response.getContentAsString().contains("\"update\":{\"href\":\"/v2/config"));
|
assertTrue("link not found", response.getContentAsString().contains("\"update\":{\"href\":\"/v2/config"));
|
||||||
@@ -146,8 +149,6 @@ public class ConfigResourceTest {
|
|||||||
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
|
assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SubjectAware(username = "readWrite")
|
@SubjectAware(username = "readWrite")
|
||||||
public void shouldValidateNamespaceStrategy() throws URISyntaxException {
|
public void shouldValidateNamespaceStrategy() throws URISyntaxException {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
|||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import sonia.scm.lifecycle.Restarter;
|
||||||
import sonia.scm.plugin.AvailablePlugin;
|
import sonia.scm.plugin.AvailablePlugin;
|
||||||
import sonia.scm.plugin.AvailablePluginDescriptor;
|
import sonia.scm.plugin.AvailablePluginDescriptor;
|
||||||
import sonia.scm.plugin.InstalledPlugin;
|
import sonia.scm.plugin.InstalledPlugin;
|
||||||
@@ -66,10 +67,15 @@ class PendingPluginResourceTest {
|
|||||||
|
|
||||||
private RestDispatcher dispatcher = new RestDispatcher();
|
private RestDispatcher dispatcher = new RestDispatcher();
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
ResourceLinks resourceLinks = ResourceLinksMock.createMock(create("/"));
|
ResourceLinks resourceLinks = ResourceLinksMock.createMock(create("/"));
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
PluginManager pluginManager;
|
PluginManager pluginManager;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
Restarter restarter;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
PluginDtoMapper mapper;
|
PluginDtoMapper mapper;
|
||||||
|
|
||||||
@@ -109,6 +115,7 @@ class PendingPluginResourceTest {
|
|||||||
void bindSubject() {
|
void bindSubject() {
|
||||||
ThreadContext.bind(subject);
|
ThreadContext.bind(subject);
|
||||||
lenient().when(subject.isPermitted("plugin:manage")).thenReturn(true);
|
lenient().when(subject.isPermitted("plugin:manage")).thenReturn(true);
|
||||||
|
lenient().when(restarter.isSupported()).thenReturn(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
@@ -176,6 +183,23 @@ class PendingPluginResourceTest {
|
|||||||
assertThat(response.getContentAsString()).contains("\"execute\":{\"href\":\"/v2/plugins/pending/execute\"}");
|
assertThat(response.getContentAsString()).contains("\"execute\":{\"href\":\"/v2/plugins/pending/execute\"}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotReturnExecuteLinkIfRestartIsNotSupported() throws URISyntaxException, UnsupportedEncodingException {
|
||||||
|
when(restarter.isSupported()).thenReturn(false);
|
||||||
|
|
||||||
|
when(pluginManager.getAvailable()).thenReturn(emptyList());
|
||||||
|
InstalledPlugin installedPlugin = createInstalledPlugin("uninstalled-plugin");
|
||||||
|
when(installedPlugin.isMarkedForUninstall()).thenReturn(true);
|
||||||
|
when(pluginManager.getInstalled()).thenReturn(singletonList(installedPlugin));
|
||||||
|
|
||||||
|
MockHttpRequest request = MockHttpRequest.get("/v2/plugins/pending");
|
||||||
|
dispatcher.invoke(request, response);
|
||||||
|
|
||||||
|
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||||
|
assertThat(response.getContentAsString()).contains("\"uninstall\":[{\"name\":\"uninstalled-plugin\"");
|
||||||
|
assertThat(response.getContentAsString()).doesNotContain("\"execute\"");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldExecutePendingPlugins() throws URISyntaxException {
|
void shouldExecutePendingPlugins() throws URISyntaxException {
|
||||||
MockHttpRequest request = MockHttpRequest.post("/v2/plugins/pending/execute");
|
MockHttpRequest request = MockHttpRequest.post("/v2/plugins/pending/execute");
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
|||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import sonia.scm.lifecycle.Restarter;
|
||||||
import sonia.scm.plugin.AvailablePlugin;
|
import sonia.scm.plugin.AvailablePlugin;
|
||||||
import sonia.scm.plugin.AvailablePluginDescriptor;
|
import sonia.scm.plugin.AvailablePluginDescriptor;
|
||||||
import sonia.scm.plugin.InstalledPlugin;
|
import sonia.scm.plugin.InstalledPlugin;
|
||||||
@@ -59,6 +60,9 @@ class PluginDtoCollectionMapperTest {
|
|||||||
|
|
||||||
ResourceLinks resourceLinks = ResourceLinksMock.createMock(URI.create("/"));
|
ResourceLinks resourceLinks = ResourceLinksMock.createMock(URI.create("/"));
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private Restarter restarter;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
PluginDtoMapperImpl pluginDtoMapper;
|
PluginDtoMapperImpl pluginDtoMapper;
|
||||||
|
|
||||||
@@ -142,7 +146,7 @@ class PluginDtoCollectionMapperTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldAddInstallLinkForNewVersionWhenPermitted() {
|
void shouldAddUpdateLinkForNewVersionWhenPermitted() {
|
||||||
when(subject.isPermitted("plugin:manage")).thenReturn(true);
|
when(subject.isPermitted("plugin:manage")).thenReturn(true);
|
||||||
PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper, manager);
|
PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper, manager);
|
||||||
|
|
||||||
@@ -154,6 +158,21 @@ class PluginDtoCollectionMapperTest {
|
|||||||
assertThat(plugin.getLinks().getLinkBy("update")).isNotEmpty();
|
assertThat(plugin.getLinks().getLinkBy("update")).isNotEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldAddUpdateWithRestartLinkForNewVersionWhenPermitted() {
|
||||||
|
when(restarter.isSupported()).thenReturn(true);
|
||||||
|
when(subject.isPermitted("plugin:manage")).thenReturn(true);
|
||||||
|
PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper, manager);
|
||||||
|
|
||||||
|
HalRepresentation result = mapper.mapInstalled(
|
||||||
|
singletonList(createInstalledPlugin("scm-some-plugin", "1")),
|
||||||
|
singletonList(createAvailablePlugin("scm-some-plugin", "2")));
|
||||||
|
|
||||||
|
PluginDto plugin = getPluginDtoFromResult(result);
|
||||||
|
assertThat(plugin.getLinks().getLinkBy("update")).isNotEmpty();
|
||||||
|
assertThat(plugin.getLinks().getLinkBy("updateWithRestart")).isNotEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldSetInstalledPluginPendingWhenCorrespondingAvailablePluginIsPending() {
|
void shouldSetInstalledPluginPendingWhenCorrespondingAvailablePluginIsPending() {
|
||||||
when(subject.isPermitted("plugin:manage")).thenReturn(true);
|
when(subject.isPermitted("plugin:manage")).thenReturn(true);
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import org.mockito.Answers;
|
|||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import sonia.scm.lifecycle.Restarter;
|
||||||
import sonia.scm.plugin.AvailablePlugin;
|
import sonia.scm.plugin.AvailablePlugin;
|
||||||
import sonia.scm.plugin.AvailablePluginDescriptor;
|
import sonia.scm.plugin.AvailablePluginDescriptor;
|
||||||
import sonia.scm.plugin.InstalledPlugin;
|
import sonia.scm.plugin.InstalledPlugin;
|
||||||
@@ -56,6 +57,9 @@ class PluginDtoMapperTest {
|
|||||||
@SuppressWarnings("unused") // Is injected
|
@SuppressWarnings("unused") // Is injected
|
||||||
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(URI.create("https://hitchhiker.com/"));
|
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(URI.create("https://hitchhiker.com/"));
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private Restarter restarter;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private PluginDtoMapperImpl mapper;
|
private PluginDtoMapperImpl mapper;
|
||||||
|
|
||||||
@@ -122,6 +126,7 @@ class PluginDtoMapperTest {
|
|||||||
|
|
||||||
PluginDto dto = mapper.mapAvailable(plugin);
|
PluginDto dto = mapper.mapAvailable(plugin);
|
||||||
assertThat(dto.getLinks().getLinkBy("install")).isEmpty();
|
assertThat(dto.getLinks().getLinkBy("install")).isEmpty();
|
||||||
|
assertThat(dto.getLinks().getLinkBy("installWithRestart")).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -134,6 +139,17 @@ class PluginDtoMapperTest {
|
|||||||
.isEqualTo("https://hitchhiker.com/v2/plugins/available/scm-cas-plugin/install");
|
.isEqualTo("https://hitchhiker.com/v2/plugins/available/scm-cas-plugin/install");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldAppendInstallWithRestartLink() {
|
||||||
|
when(restarter.isSupported()).thenReturn(true);
|
||||||
|
when(subject.isPermitted("plugin:manage")).thenReturn(true);
|
||||||
|
AvailablePlugin plugin = createAvailable(createPluginInformation());
|
||||||
|
|
||||||
|
PluginDto dto = mapper.mapAvailable(plugin);
|
||||||
|
assertThat(dto.getLinks().getLinkBy("installWithRestart").get().getHref())
|
||||||
|
.isEqualTo("https://hitchhiker.com/v2/plugins/available/scm-cas-plugin/install?restart=true");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReturnMiscellaneousIfCategoryIsNull() {
|
void shouldReturnMiscellaneousIfCategoryIsNull() {
|
||||||
PluginInformation information = createPluginInformation();
|
PluginInformation information = createPluginInformation();
|
||||||
@@ -162,4 +178,17 @@ class PluginDtoMapperTest {
|
|||||||
assertThat(dto.getLinks().getLinkBy("uninstall").get().getHref())
|
assertThat(dto.getLinks().getLinkBy("uninstall").get().getHref())
|
||||||
.isEqualTo("https://hitchhiker.com/v2/plugins/installed/scm-cas-plugin/uninstall");
|
.isEqualTo("https://hitchhiker.com/v2/plugins/installed/scm-cas-plugin/uninstall");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldAppendUninstallWithRestartLink() {
|
||||||
|
when(restarter.isSupported()).thenReturn(true);
|
||||||
|
when(subject.isPermitted("plugin:manage")).thenReturn(true);
|
||||||
|
|
||||||
|
InstalledPlugin plugin = createInstalled(createPluginInformation());
|
||||||
|
when(plugin.isUninstallable()).thenReturn(true);
|
||||||
|
|
||||||
|
PluginDto dto = mapper.mapInstalled(plugin, emptyList());
|
||||||
|
assertThat(dto.getLinks().getLinkBy("uninstallWithRestart").get().getHref())
|
||||||
|
.isEqualTo("https://hitchhiker.com/v2/plugins/installed/scm-cas-plugin/uninstall?restart=true");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
package sonia.scm.lifecycle;
|
||||||
|
|
||||||
|
import com.github.legman.Subscribe;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Captor;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import sonia.scm.event.ScmEventBus;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class DefaultRestarterTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ScmEventBus eventBus;
|
||||||
|
|
||||||
|
@Captor
|
||||||
|
private ArgumentCaptor<RestartEvent> eventCaptor;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldLoadStrategyOnCreation() {
|
||||||
|
System.setProperty(RestartStrategyFactory.PROPERTY_STRATEGY, ExitRestartStrategy.NAME);
|
||||||
|
try {
|
||||||
|
DefaultRestarter restarter = new DefaultRestarter();
|
||||||
|
assertThat(restarter.isSupported()).isTrue();
|
||||||
|
} finally {
|
||||||
|
System.clearProperty(RestartStrategyFactory.PROPERTY_STRATEGY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnFalseIfRestartStrategyIsNotAvailable() {
|
||||||
|
DefaultRestarter restarter = new DefaultRestarter(eventBus, null);
|
||||||
|
assertThat(restarter.isSupported()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnTrueIfRestartStrategyIsAvailable() {
|
||||||
|
DefaultRestarter restarter = new DefaultRestarter();
|
||||||
|
assertThat(restarter.isSupported()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowRestartNotSupportedException() {
|
||||||
|
DefaultRestarter restarter = new DefaultRestarter(eventBus,null);
|
||||||
|
assertThrows(
|
||||||
|
RestartNotSupportedException.class, () -> restarter.restart(DefaultRestarterTest.class, "test")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFireRestartEvent() {
|
||||||
|
DefaultRestarter restarter = new DefaultRestarter(eventBus, new ExitRestartStrategy());
|
||||||
|
restarter.restart(DefaultRestarterTest.class, "testing");
|
||||||
|
|
||||||
|
verify(eventBus).post(eventCaptor.capture());
|
||||||
|
|
||||||
|
RestartEvent event = eventCaptor.getValue();
|
||||||
|
assertThat(event.getCause()).isEqualTo(DefaultRestarterTest.class);
|
||||||
|
assertThat(event.getReason()).isEqualTo("testing");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
package sonia.scm.lifecycle;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.util.function.IntConsumer;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class ExitRestartStrategyTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private RestartStrategy.InternalInjectionContext context;
|
||||||
|
|
||||||
|
private ExitRestartStrategy strategy;
|
||||||
|
private CapturingExiter exiter;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUpStrategy() {
|
||||||
|
strategy = new ExitRestartStrategy();
|
||||||
|
exiter = new CapturingExiter();
|
||||||
|
strategy.setExiter(exiter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldTearDownContextAndThenExit() {
|
||||||
|
strategy.restart(context);
|
||||||
|
|
||||||
|
verify(context).destroy();
|
||||||
|
assertThat(exiter.getExitCode()).isEqualTo(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUseExitCodeFromSystemProperty() {
|
||||||
|
System.setProperty(ExitRestartStrategy.PROPERTY_EXIT_CODE, "42");
|
||||||
|
try {
|
||||||
|
strategy.restart(context);
|
||||||
|
|
||||||
|
verify(context).destroy();
|
||||||
|
assertThat(exiter.getExitCode()).isEqualTo(42);
|
||||||
|
} finally {
|
||||||
|
System.clearProperty(ExitRestartStrategy.PROPERTY_EXIT_CODE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowExceptionForNonNumericExitCode() {
|
||||||
|
System.setProperty(ExitRestartStrategy.PROPERTY_EXIT_CODE, "xyz");
|
||||||
|
try {
|
||||||
|
assertThrows(RestartNotSupportedException.class, () -> strategy.restart(context));
|
||||||
|
} finally {
|
||||||
|
System.clearProperty(ExitRestartStrategy.PROPERTY_EXIT_CODE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class CapturingExiter implements IntConsumer {
|
||||||
|
|
||||||
|
private int exitCode = -1;
|
||||||
|
|
||||||
|
public int getExitCode() {
|
||||||
|
return exitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void accept(int exitCode) {
|
||||||
|
this.exitCode = exitCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
/*
|
|
||||||
* MIT License
|
|
||||||
*
|
|
||||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package sonia.scm.lifecycle;
|
|
||||||
|
|
||||||
import com.github.legman.Subscribe;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
|
||||||
import org.mockito.Mock;
|
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
|
||||||
import sonia.scm.event.RecreateEventBusEvent;
|
|
||||||
import sonia.scm.event.ScmEventBus;
|
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
import static org.awaitility.Awaitility.await;
|
|
||||||
import static org.mockito.Mockito.verify;
|
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
|
||||||
class InjectionContextRestartStrategyTest {
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private RestartStrategy.InjectionContext context;
|
|
||||||
|
|
||||||
private InjectionContextRestartStrategy strategy = new InjectionContextRestartStrategy(Thread.currentThread().getContextClassLoader());
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setWaitToZero() {
|
|
||||||
strategy.setWaitInMs(0L);
|
|
||||||
// disable gc during tests
|
|
||||||
strategy.setGcEnabled(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldCallDestroyAndInitialize() {
|
|
||||||
TestingInjectionContext ctx = new TestingInjectionContext();
|
|
||||||
strategy.restart(ctx);
|
|
||||||
await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> assertThat(ctx.destroyed).isTrue());
|
|
||||||
await().atMost(1, TimeUnit.SECONDS).untilAsserted(() -> assertThat(ctx.initialized).isTrue());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldFireRecreateEventBusEvent() {
|
|
||||||
Listener listener = new Listener();
|
|
||||||
ScmEventBus.getInstance().register(listener);
|
|
||||||
|
|
||||||
strategy.restart(context);
|
|
||||||
|
|
||||||
assertThat(listener.event).isNotNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldRegisterContextAfterRestart() throws InterruptedException {
|
|
||||||
TestingInjectionContext ctx = new TestingInjectionContext();
|
|
||||||
strategy.restart(ctx);
|
|
||||||
|
|
||||||
await().atMost(1, TimeUnit.SECONDS).until(() -> ctx.initialized);
|
|
||||||
Thread.sleep(50L);
|
|
||||||
ScmEventBus.getInstance().post("hello event");
|
|
||||||
|
|
||||||
assertThat(ctx.event).isEqualTo("hello event");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Listener {
|
|
||||||
|
|
||||||
private RecreateEventBusEvent event;
|
|
||||||
|
|
||||||
@Subscribe(async = false)
|
|
||||||
public void setEvent(RecreateEventBusEvent event) {
|
|
||||||
this.event = event;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class TestingInjectionContext implements RestartStrategy.InjectionContext {
|
|
||||||
|
|
||||||
private volatile String event;
|
|
||||||
private boolean initialized = false;
|
|
||||||
private boolean destroyed = false;
|
|
||||||
|
|
||||||
@Subscribe(async = false)
|
|
||||||
public void setEvent(String event) {
|
|
||||||
this.event = event;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void initialize() {
|
|
||||||
this.initialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void destroy() {
|
|
||||||
this.destroyed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
package sonia.scm.lifecycle;
|
||||||
|
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
|
import sonia.scm.util.SystemUtil;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
class RestartStrategyTest {
|
||||||
|
private final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnRestartStrategyFromSystemProperty() {
|
||||||
|
withStrategy(TestingRestartStrategy.class.getName(), (rs) -> {
|
||||||
|
assertThat(rs).containsInstanceOf(TestingRestartStrategy.class);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnRestartStrategyFromSystemPropertyWithClassLoaderConstructor() {
|
||||||
|
withStrategy(ComplexRestartStrategy.class.getName(), (rs) -> {
|
||||||
|
assertThat(rs).containsInstanceOf(ComplexRestartStrategy.class)
|
||||||
|
.get()
|
||||||
|
.extracting("classLoader")
|
||||||
|
.isSameAs(classLoader);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowExceptionForNonStrategyClass() {
|
||||||
|
withStrategy(RestartStrategyTest.class.getName(), () -> {
|
||||||
|
assertThrows(RestartNotSupportedException.class, () -> RestartStrategy.get(classLoader));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnEmpty() {
|
||||||
|
withStrategy(RestartStrategyFactory.STRATEGY_NONE, (rs) -> {
|
||||||
|
assertThat(rs).isEmpty();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnEmptyForUnknownOs() {
|
||||||
|
withSystemProperty(SystemUtil.PROPERTY_OSNAME, "hitchhiker-os", () -> {
|
||||||
|
Optional<RestartStrategy> restartStrategy = RestartStrategy.get(classLoader);
|
||||||
|
assertThat(restartStrategy).isEmpty();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnExitRestartStrategy() {
|
||||||
|
withStrategy(ExitRestartStrategy.NAME, (rs) -> {
|
||||||
|
assertThat(rs).containsInstanceOf(ExitRestartStrategy.class);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(strings = { "linux", "darwin", "solaris", "freebsd", "openbsd" })
|
||||||
|
void shouldReturnPosixRestartStrategyForPosixBased(String os) {
|
||||||
|
withSystemProperty(SystemUtil.PROPERTY_OSNAME, os, () -> {
|
||||||
|
Optional<RestartStrategy> restartStrategy = RestartStrategy.get(classLoader);
|
||||||
|
assertThat(restartStrategy).containsInstanceOf(PosixRestartStrategy.class);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void withStrategy(String strategy, Consumer<Optional<RestartStrategy>> consumer) {
|
||||||
|
withStrategy(strategy, () -> {
|
||||||
|
consumer.accept(RestartStrategy.get(classLoader));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void withStrategy(String strategy, Runnable runnable) {
|
||||||
|
withSystemProperty(RestartStrategyFactory.PROPERTY_STRATEGY, strategy, runnable);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void withSystemProperty(String key, String value, Runnable runnable) {
|
||||||
|
String oldValue = System.getProperty(key);
|
||||||
|
System.setProperty(key, value);
|
||||||
|
try {
|
||||||
|
runnable.run();
|
||||||
|
} finally {
|
||||||
|
if (Strings.isNullOrEmpty(oldValue)) {
|
||||||
|
System.clearProperty(key);
|
||||||
|
} else {
|
||||||
|
System.setProperty(key, oldValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class TestingRestartStrategy extends RestartStrategy {
|
||||||
|
@Override
|
||||||
|
protected void executeRestart(InjectionContext context) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ComplexRestartStrategy extends RestartStrategy {
|
||||||
|
|
||||||
|
private final ClassLoader classLoader;
|
||||||
|
|
||||||
|
public ComplexRestartStrategy(ClassLoader classLoader) {
|
||||||
|
this.classLoader = classLoader;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void executeRestart(InjectionContext context) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -30,20 +30,9 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
|
|
||||||
class ClassLoaderLifeCycleTest {
|
class ClassLoaderLifeCycleTest {
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldCreateSimpleClassLoader() {
|
|
||||||
System.setProperty(ClassLoaderLifeCycle.PROPERTY, SimpleClassLoaderLifeCycle.NAME);
|
|
||||||
try {
|
|
||||||
ClassLoaderLifeCycle classLoaderLifeCycle = ClassLoaderLifeCycle.create();
|
|
||||||
assertThat(classLoaderLifeCycle).isInstanceOf(SimpleClassLoaderLifeCycle.class);
|
|
||||||
} finally {
|
|
||||||
System.clearProperty(ClassLoaderLifeCycle.PROPERTY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldCreateDefaultClassLoader() {
|
void shouldCreateDefaultClassLoader() {
|
||||||
ClassLoaderLifeCycle classLoaderLifeCycle = ClassLoaderLifeCycle.create();
|
ClassLoaderLifeCycle classLoaderLifeCycle = ClassLoaderLifeCycle.create();
|
||||||
assertThat(classLoaderLifeCycle).isInstanceOf(ClassLoaderLifeCycleWithLeakPrevention.class);
|
assertThat(classLoaderLifeCycle).isInstanceOf(SimpleClassLoaderLifeCycle.class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,140 +0,0 @@
|
|||||||
/*
|
|
||||||
* MIT License
|
|
||||||
*
|
|
||||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package sonia.scm.lifecycle.classloading;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
|
||||||
import org.mockito.Mock;
|
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
|
||||||
import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventor;
|
|
||||||
import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventorFactory;
|
|
||||||
|
|
||||||
import java.io.Closeable;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.net.URLClassLoader;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.Mockito.*;
|
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
|
||||||
class ClassLoaderLifeCycleWithLeakPreventionTest {
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private ClassLoaderLeakPreventor classLoaderLeakPreventor;
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldThrowIllegalStateExceptionWithoutInit() {
|
|
||||||
ClassLoaderLifeCycleWithLeakPrevention lifeCycle = new ClassLoaderLifeCycleWithLeakPrevention(Thread.currentThread().getContextClassLoader());
|
|
||||||
assertThrows(IllegalStateException.class, lifeCycle::getBootstrapClassLoader);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldThrowIllegalStateExceptionAfterShutdown() {
|
|
||||||
ClassLoaderLifeCycleWithLeakPrevention lifeCycle = createMockedLifeCycle();
|
|
||||||
lifeCycle.initialize();
|
|
||||||
|
|
||||||
lifeCycle.shutdown();
|
|
||||||
assertThrows(IllegalStateException.class, lifeCycle::getBootstrapClassLoader);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldCreateBootstrapClassLoaderOnInit() {
|
|
||||||
ClassLoaderLifeCycleWithLeakPrevention lifeCycle = new ClassLoaderLifeCycleWithLeakPrevention(Thread.currentThread().getContextClassLoader());
|
|
||||||
lifeCycle.initialize();
|
|
||||||
|
|
||||||
assertThat(lifeCycle.getBootstrapClassLoader()).isNotNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldCallTheLeakPreventor() {
|
|
||||||
ClassLoaderLifeCycleWithLeakPrevention lifeCycle = createMockedLifeCycle();
|
|
||||||
|
|
||||||
lifeCycle.initialize();
|
|
||||||
verify(classLoaderLeakPreventor, times(1)).runPreClassLoaderInitiators();
|
|
||||||
|
|
||||||
lifeCycle.createChildFirstPluginClassLoader(new URL[0], null, "a");
|
|
||||||
lifeCycle.createPluginClassLoader(new URL[0], null, "b");
|
|
||||||
verify(classLoaderLeakPreventor, times(3)).runPreClassLoaderInitiators();
|
|
||||||
|
|
||||||
lifeCycle.shutdown();
|
|
||||||
verify(classLoaderLeakPreventor, times(3)).runCleanUps();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldCloseCloseableClassLoaders() throws IOException {
|
|
||||||
// we use URLClassLoader, because we must be sure that the classloader is closable
|
|
||||||
URLClassLoader webappClassLoader = spy(new URLClassLoader(new URL[0], Thread.currentThread().getContextClassLoader()));
|
|
||||||
|
|
||||||
ClassLoaderLifeCycleWithLeakPrevention lifeCycle = createMockedLifeCycle(webappClassLoader);
|
|
||||||
lifeCycle.setClassLoaderAppendListener(new ClassLoaderLifeCycleWithLeakPrevention.ClassLoaderAppendListener() {
|
|
||||||
@Override
|
|
||||||
public <C extends ClassLoader> C apply(C classLoader) {
|
|
||||||
return spy(classLoader);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
lifeCycle.initialize();
|
|
||||||
|
|
||||||
ClassLoader pluginA = lifeCycle.createChildFirstPluginClassLoader(new URL[0], null, "a");
|
|
||||||
ClassLoader pluginB = lifeCycle.createPluginClassLoader(new URL[0], null, "b");
|
|
||||||
|
|
||||||
lifeCycle.shutdown();
|
|
||||||
|
|
||||||
closed(pluginB);
|
|
||||||
closed(pluginA);
|
|
||||||
|
|
||||||
neverClosed(webappClassLoader);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void neverClosed(Object object) throws IOException {
|
|
||||||
Closeable closeable = closeable(object);
|
|
||||||
verify(closeable, never()).close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void closed(Object object) throws IOException {
|
|
||||||
Closeable closeable = closeable(object);
|
|
||||||
verify(closeable).close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Closeable closeable(Object object) {
|
|
||||||
assertThat(object).isInstanceOf(Closeable.class);
|
|
||||||
return (Closeable) object;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ClassLoaderLifeCycleWithLeakPrevention createMockedLifeCycle() {
|
|
||||||
return createMockedLifeCycle(Thread.currentThread().getContextClassLoader());
|
|
||||||
}
|
|
||||||
|
|
||||||
private ClassLoaderLifeCycleWithLeakPrevention createMockedLifeCycle(ClassLoader classLoader) {
|
|
||||||
when(classLoaderLeakPreventorFactory.newLeakPreventor(any(ClassLoader.class))).thenReturn(classLoaderLeakPreventor);
|
|
||||||
return new ClassLoaderLifeCycleWithLeakPrevention(classLoader, classLoaderLeakPreventorFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -36,10 +36,12 @@ import org.junit.jupiter.api.Test;
|
|||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.junitpioneer.jupiter.TempDirectory;
|
import org.junitpioneer.jupiter.TempDirectory;
|
||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import sonia.scm.NotFoundException;
|
import sonia.scm.NotFoundException;
|
||||||
import sonia.scm.ScmConstraintViolationException;
|
import sonia.scm.ScmConstraintViolationException;
|
||||||
|
import sonia.scm.lifecycle.Restarter;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@@ -78,13 +80,15 @@ class DefaultPluginManagerTest {
|
|||||||
@Mock
|
@Mock
|
||||||
private PluginInstaller installer;
|
private PluginInstaller installer;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private Restarter restarter;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
private DefaultPluginManager manager;
|
private DefaultPluginManager manager;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private Subject subject;
|
private Subject subject;
|
||||||
|
|
||||||
private boolean restartTriggered = false;
|
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void mockInstaller() {
|
void mockInstaller() {
|
||||||
lenient().when(installer.install(any())).then(ic -> {
|
lenient().when(installer.install(any())).then(ic -> {
|
||||||
@@ -93,16 +97,6 @@ class DefaultPluginManagerTest {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void createPluginManagerToTestWithCapturedRestart() {
|
|
||||||
manager = new DefaultPluginManager(null, loader, center, installer) { // event bus is only used in restart and this is replaced here
|
|
||||||
@Override
|
|
||||||
void triggerRestart(String cause) {
|
|
||||||
restartTriggered = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
class WithAdminPermissions {
|
class WithAdminPermissions {
|
||||||
|
|
||||||
@@ -209,7 +203,7 @@ class DefaultPluginManagerTest {
|
|||||||
manager.install("scm-git-plugin", false);
|
manager.install("scm-git-plugin", false);
|
||||||
|
|
||||||
verify(installer).install(git);
|
verify(installer).install(git);
|
||||||
assertThat(restartTriggered).isFalse();
|
verify(restarter, never()).restart(any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -258,7 +252,7 @@ class DefaultPluginManagerTest {
|
|||||||
PendingPluginInstallation pendingMail = mock(PendingPluginInstallation.class);
|
PendingPluginInstallation pendingMail = mock(PendingPluginInstallation.class);
|
||||||
doReturn(pendingMail).when(installer).install(mail);
|
doReturn(pendingMail).when(installer).install(mail);
|
||||||
|
|
||||||
doThrow(new PluginChecksumMismatchException("checksum does not match")).when(installer).install(review);
|
doThrow(new PluginChecksumMismatchException(mail, "1", "2")).when(installer).install(review);
|
||||||
|
|
||||||
assertThrows(PluginInstallException.class, () -> manager.install("scm-review-plugin", false));
|
assertThrows(PluginInstallException.class, () -> manager.install("scm-review-plugin", false));
|
||||||
|
|
||||||
@@ -287,7 +281,7 @@ class DefaultPluginManagerTest {
|
|||||||
manager.install("scm-git-plugin", true);
|
manager.install("scm-git-plugin", true);
|
||||||
|
|
||||||
verify(installer).install(git);
|
verify(installer).install(git);
|
||||||
assertThat(restartTriggered).isTrue();
|
verify(restarter).restart(any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -296,7 +290,7 @@ class DefaultPluginManagerTest {
|
|||||||
when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(gitInstalled));
|
when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(gitInstalled));
|
||||||
|
|
||||||
manager.install("scm-git-plugin", true);
|
manager.install("scm-git-plugin", true);
|
||||||
assertThat(restartTriggered).isFalse();
|
verify(restarter, never()).restart(any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -318,14 +312,14 @@ class DefaultPluginManagerTest {
|
|||||||
manager.install("scm-review-plugin", false);
|
manager.install("scm-review-plugin", false);
|
||||||
manager.executePendingAndRestart();
|
manager.executePendingAndRestart();
|
||||||
|
|
||||||
assertThat(restartTriggered).isTrue();
|
verify(restarter).restart(any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldNotSendRestartEventWithoutPendingPlugins() {
|
void shouldNotSendRestartEventWithoutPendingPlugins() {
|
||||||
manager.executePendingAndRestart();
|
manager.executePendingAndRestart();
|
||||||
|
|
||||||
assertThat(restartTriggered).isFalse();
|
verify(restarter, never()).restart(any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -476,7 +470,7 @@ class DefaultPluginManagerTest {
|
|||||||
|
|
||||||
manager.executePendingAndRestart();
|
manager.executePendingAndRestart();
|
||||||
|
|
||||||
assertThat(restartTriggered).isTrue();
|
verify(restarter).restart(any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import org.junit.jupiter.api.Test;
|
|||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import sonia.scm.lifecycle.Restarter;
|
||||||
import sonia.scm.update.repository.DefaultMigrationStrategyDAO;
|
import sonia.scm.update.repository.DefaultMigrationStrategyDAO;
|
||||||
import sonia.scm.update.repository.MigrationStrategy;
|
import sonia.scm.update.repository.MigrationStrategy;
|
||||||
import sonia.scm.update.repository.V1Repository;
|
import sonia.scm.update.repository.V1Repository;
|
||||||
@@ -51,6 +52,8 @@ class MigrationWizardServletTest {
|
|||||||
XmlRepositoryV1UpdateStep updateStep;
|
XmlRepositoryV1UpdateStep updateStep;
|
||||||
@Mock
|
@Mock
|
||||||
DefaultMigrationStrategyDAO migrationStrategyDao;
|
DefaultMigrationStrategyDAO migrationStrategyDao;
|
||||||
|
@Mock
|
||||||
|
Restarter restarter;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
HttpServletRequest request;
|
HttpServletRequest request;
|
||||||
@@ -64,7 +67,7 @@ class MigrationWizardServletTest {
|
|||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void initServlet() {
|
void initServlet() {
|
||||||
servlet = new MigrationWizardServlet(updateStep, migrationStrategyDao) {
|
servlet = new MigrationWizardServlet(updateStep, migrationStrategyDao, restarter) {
|
||||||
@Override
|
@Override
|
||||||
void respondWithTemplate(HttpServletResponse resp, Map<String, Object> model, String templateName) {
|
void respondWithTemplate(HttpServletResponse resp, Map<String, Object> model, String templateName) {
|
||||||
renderedTemplateName = templateName;
|
renderedTemplateName = templateName;
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import sonia.scm.lifecycle.RestartEvent;
|
|||||||
import sonia.scm.cache.Cache;
|
import sonia.scm.cache.Cache;
|
||||||
import sonia.scm.cache.CacheManager;
|
import sonia.scm.cache.CacheManager;
|
||||||
import sonia.scm.event.ScmEventBus;
|
import sonia.scm.event.ScmEventBus;
|
||||||
|
import sonia.scm.lifecycle.RestartEventFactory;
|
||||||
import sonia.scm.plugin.PluginLoader;
|
import sonia.scm.plugin.PluginLoader;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
@@ -138,7 +139,7 @@ public class I18nServletTest {
|
|||||||
public void shouldCleanCacheOnRestartEvent() {
|
public void shouldCleanCacheOnRestartEvent() {
|
||||||
ScmEventBus.getInstance().register(servlet);
|
ScmEventBus.getInstance().register(servlet);
|
||||||
|
|
||||||
ScmEventBus.getInstance().post(new RestartEvent(I18nServlet.class, "Restart to reload the plugin resources"));
|
ScmEventBus.getInstance().post(RestartEventFactory.create(I18nServlet.class, "Restart to reload the plugin resources"));
|
||||||
|
|
||||||
verify(cache).clear();
|
verify(cache).clear();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user