diff options
Diffstat (limited to 'org.eclipse.jgit/src/org/eclipse/jgit/internal/util')
3 files changed, 400 insertions, 0 deletions
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/util/CleanupService.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/util/CleanupService.java new file mode 100644 index 0000000000..29ed7564d3 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/util/CleanupService.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2024, Thomas Wolf <twolf@apache.org> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.internal.util; + +import org.eclipse.jgit.internal.JGitText; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A class that is registered as an OSGi service via the manifest. If JGit runs + * in OSGi, OSGi will instantiate a singleton as soon as the bundle is activated + * since this class is an immediate OSGi component with no dependencies. OSGi + * will then call its {@link #start()} method. If JGit is not running in OSGi, + * {@link #getInstance()} will lazily create an instance. + * <p> + * An OSGi-created {@link CleanupService} will run the registered cleanup when + * the {@code org.eclipse.jgit} bundle is deactivated. A lazily created instance + * will register the cleanup as a JVM shutdown hook. + * </p> + */ +public final class CleanupService { + + private static final Logger LOG = LoggerFactory + .getLogger(CleanupService.class); + + private static final Object LOCK = new Object(); + + private static CleanupService INSTANCE; + + private final boolean isOsgi; + + private JGitText jgitText; + + private Runnable cleanup; + + /** + * Public component constructor for OSGi DS. Do <em>not</em> call this + * explicitly! (Unfortunately this constructor must be public because of + * OSGi requirements.) + */ + public CleanupService() { + this.isOsgi = true; + setInstance(this); + } + + private CleanupService(boolean isOsgi) { + this.isOsgi = isOsgi; + } + + private static void setInstance(CleanupService service) { + synchronized (LOCK) { + INSTANCE = service; + } + } + + /** + * Obtains the singleton instance of the {@link CleanupService} that knows + * whether or not it is running on OSGi. + * + * @return the {@link CleanupService} singleton instance + */ + public static CleanupService getInstance() { + synchronized (LOCK) { + if (INSTANCE == null) { + INSTANCE = new CleanupService(false); + } + return INSTANCE; + } + } + + void start() { + // Nothing to do + } + + void register(Runnable cleanUp) { + if (isOsgi) { + cleanup = cleanUp; + } else { + // Ensure the JGitText class is loaded. Depending on the framework + // JGit runs in, it may not be possible anymore to load classes when + // the hook runs. For instance when run in a maven plug-in: the + // Plexus class world that loaded JGit may already have been + // disposed by the time the JVM shutdown hook runs when the whole + // maven build terminates. + jgitText = JGitText.get(); + assert jgitText != null; + try { + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + try { + cleanUp.run(); + // Don't catch exceptions; let the JVM do the problem + // reporting. + } finally { + jgitText = null; + } + })); + } catch (IllegalStateException e) { + // Ignore -- the JVM is already shutting down. + } + } + } + + void shutDown() { + if (isOsgi && cleanup != null) { + Runnable r = cleanup; + cleanup = null; + try { + r.run(); + } catch (RuntimeException e) { + LOG.error(JGitText.get().shutdownCleanupFailed, e); + } + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/util/Optionally.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/util/Optionally.java new file mode 100644 index 0000000000..270b760562 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/util/Optionally.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2023 Qualcomm Innovation Center, Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package org.eclipse.jgit.internal.util; + +import java.lang.ref.SoftReference; +import java.util.Optional; + +/** + * Interface representing a reference to a potentially mutable optional object. + * + * @param <T> + * type of the mutable optional object + * + * @since 6.7 + */ +public interface Optionally<T> { + /** + * A permanently empty Optionally + * + * @param <T> + * type of the mutable optional object + * + */ + public class Empty<T> implements Optionally<T> { + @Override + public void clear() { + // empty + } + + @Override + public Optional<T> getOptional() { + return Optional.empty(); + } + } + + /** + * A permanent(hard) reference to an object + * + * @param <T> + * type of the mutable optional object + * + */ + public class Hard<T> implements Optionally<T> { + /** + * The mutable optional object + */ + protected Optional<T> optional; + + /** + * @param element + * the mutable optional object + */ + public Hard(T element) { + optional = Optional.ofNullable(element); + } + + @Override + public void clear() { + optional = Optional.empty(); + } + + @Override + public Optional<T> getOptional() { + return optional; + } + } + + /** + * A SoftReference Optionally + * + * @param <T> + * type of the mutable optional object + * + */ + public class Soft<T> extends SoftReference<T> implements Optionally<T> { + /** + * @param t + * the mutable optional object + */ + public Soft(T t) { + super(t); + } + + @Override + public Optional<T> getOptional() { + return Optional.ofNullable(get()); + } + } + + /** + * The empty Optionally + */ + public static final Optionally<?> EMPTY = new Empty<>(); + + /** + * Get empty Optionally + * + * @param <T> + * type of the empty Optionally + * @return the empty Optionally + */ + @SuppressWarnings("unchecked") + public static <T> Optionally<T> empty() { + return (Optionally<T>) EMPTY; + } + + /** + * Clear the object + */ + void clear(); + + /** + * Get an Optional representing the current state of the object + * + * @return the mutable optional object + */ + Optional<T> getOptional(); +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/util/ShutdownHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/util/ShutdownHook.java new file mode 100644 index 0000000000..f6b4723489 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/util/ShutdownHook.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2023, SAP SE and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.internal.util; + +import java.text.MessageFormat; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.eclipse.jgit.internal.JGitText; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The singleton {@link ShutdownHook} provides a means to register + * {@link Listener}s that are run when JGit is uninstalled, either + * <ul> + * <li>in an OSGi framework when this bundle is deactivated, or</li> + * <li>otherwise, when the JVM as a whole shuts down.</li> + * </ul> + */ +@SuppressWarnings("ImmutableEnumChecker") +public enum ShutdownHook { + /** + * Singleton + */ + INSTANCE; + + /** + * Object that needs to cleanup on shutdown. + */ + public interface Listener { + /** + * Cleanup resources when JGit is shut down. + * <p> + * Implementations should be coded defensively + * <ul> + * <li>they should finish their work quickly + * <li>they should be written to be thread-safe and to avoid deadlocks + * insofar as possible + * <li>they should not rely blindly upon services that may have + * registered their own shutdown hooks and therefore may themselves be + * in the process of shutting down + * <li>attempts to use other thread-based services may lead to + * deadlocks. + * </ul> + * See {@link Runtime#addShutdownHook} for more details. + */ + public void onShutdown(); + } + + private static final Logger LOG = LoggerFactory + .getLogger(ShutdownHook.class); + + private final Set<Listener> listeners = ConcurrentHashMap.newKeySet(); + + private final AtomicBoolean shutdownInProgress = new AtomicBoolean(); + + private ShutdownHook() { + CleanupService.getInstance().register(this::cleanup); + } + + private void cleanup() { + if (!shutdownInProgress.getAndSet(true)) { + ExecutorService runner = Executors.newWorkStealingPool(); + try { + runner.submit(() -> { + this.doCleanup(); + return null; + }).get(30L, TimeUnit.SECONDS); + } catch (InterruptedException | ExecutionException + | TimeoutException e) { + throw new RuntimeException(e.getMessage(), e); + } finally { + runner.shutdownNow(); + } + } + } + + private void doCleanup() { + listeners.parallelStream().forEach(this::notify); + } + + private void notify(Listener l) { + LOG.debug(JGitText.get().shutdownCleanup, l); + try { + l.onShutdown(); + } catch (RuntimeException e) { + LOG.error(MessageFormat.format( + JGitText.get().shutdownCleanupListenerFailed, l), e); + } + } + + /** + * Register object that needs cleanup during JGit shutdown if it is not + * already registered. Registration is disabled when JGit shutdown is + * already in progress. + * + * @param l + * the object to call {@link Listener#onShutdown} on when JGit + * shuts down + * @return {@code true} if this object has been registered + */ + public boolean register(Listener l) { + if (shutdownInProgress.get()) { + return listeners.contains(l); + } + LOG.debug("register {} with shutdown hook", l); //$NON-NLS-1$ + listeners.add(l); + return true; + } + + /** + * Unregister object that no longer needs cleanup during JGit shutdown if it + * is still registered. Unregistration is disabled when JGit shutdown is + * already in progress. + * + * @param l + * the object registered to be notified for cleanup when the JVM + * shuts down + * @return {@code true} if this object is no longer registered + */ + public boolean unregister(Listener l) { + if (shutdownInProgress.get()) { + return !listeners.contains(l); + } + LOG.debug("unregister {} from shutdown hook", l); //$NON-NLS-1$ + listeners.remove(l); + return true; + } + + /** + * Whether a JGit shutdown is in progress + * + * @return {@code true} if a JGit shutdown is in progress + */ + public boolean isShutdownInProgress() { + return shutdownInProgress.get(); + } +} |