com.microsoft.azure.javamsalruntime.HandleBase Maven / Gradle / Ivy
Show all versions of javamsalruntime Show documentation
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
package com.microsoft.azure.javamsalruntime;
import com.sun.jna.Memory;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.ptr.IntByReference;
import com.sun.jna.ptr.LongByReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
/**
* Interface used to define common behavior for the various classes that represent MSALRuntime
* handles
*
* Extends LongByReference to allow JNA to convert it to an equivalent of MSALRuntime's *HANDLE
* types, and implements AutoCloseable in order to be used in a try-with-resources block
*/
abstract class HandleBase extends LongByReference implements AutoCloseable {
private static final Logger LOG = LoggerFactory.getLogger(HandleBase.class);
protected LongByReference msalRuntimeHandle;
ReleaseMethod releaseMethod;
/**
* Thread that manages cleaning up Handles and their PhantomReferences
*/
private final HandleFinalizerThread HANDLE_FINALIZER_THREAD = new HandleFinalizerThread();
HandleBase(ReleaseMethod releaseMethod) {
this.msalRuntimeHandle = new LongByReference();
this.releaseMethod = releaseMethod;
HANDLE_FINALIZER_THREAD.addReference(this, msalRuntimeHandle, releaseMethod);
}
HandleBase(LongByReference msalRuntimeHandle, ReleaseMethod releaseMethod) {
this.msalRuntimeHandle = msalRuntimeHandle;
this.releaseMethod = releaseMethod;
HANDLE_FINALIZER_THREAD.addReference(this, msalRuntimeHandle, releaseMethod);
}
/**
* Returns the handle value that this Handle instance represents. This value is generally set by
* MSALRuntime, and is how the interop can access data managed by MSALRuntime
*/
public long value() {
return this.getValue();
}
/**
* Handle objects extend LongByReference, so they will have a default value of 0
*
* Various MSALRuntime APIs take a fresh (value = 0) Handle as a parameter and sets it to some
* non-zero value, allowing it to act as a reference to some underlying data managed by
* MSALRuntime
*
* @return boolean true if this handle was set to some non-zero value, false otherwise
*/
boolean isHandleValid() {
return value() != 0;
}
/**
* All Java objects created by the interop will either be cleaned up by the JVM's garbage
* collector or by the operating system if the JVM shuts down, however the MSALRuntime handles
* and their underlying data won't be since the JVM doesn't actually know about that data
* So, to avoid memory leaks all MSALRuntime handles must eventually be 'released' by calling
* the appropriate MSALRuntime MSALRuntime_RELEASE* API for that type of handle
In most
* cases a handle can be released immediately after using it to retrieve some data, however
* there are some scenarios where handles must be stored for an indefinite amount of time and we
* must rely on Java's PhantomReference and our AsyncHandleFinalizerThread
*/
public void release() {
if (isHandleValid()) {
try {
// Release handle using the MSALRuntime API set when this object was created
MsalRuntimeInterop.ERROR_HELPER.checkMsalRuntimeError(releaseMethod.release(this.value()));
// Set local handle to null, to indicate that it has been released and prevent
// attempts to use it again
this.msalRuntimeHandle = null;
} catch (MsalInteropException e) {
throw e;
} catch (Exception e) {
MsalRuntimeInterop.ERROR_HELPER.logUnknownErrorReleasingHandle(e);
}
}
}
/**
* Used to automatically release handles created in a try-with-resources block
*/
@Override
public void close() {
release();
}
/**
* Helper method for returning a String from MSALRuntime
*
* Any MSALRuntime API that populates a String requires two calls: the first is needed to figure
* out the size of the String, and the second call actually populates the String
*
* @param handle a handle representing some block of data which (should) contain a
* String
* @param getMSALRuntimeString the MSALRuntime API that we will call in order to retrieve a
* String
* @return the String retrieved from MSALRuntime
*/
static String getString(HandleBase handle, GetMsalRuntimeString getMSALRuntimeString) {
IntByReference bufferSize = new IntByReference(0);
// First, we must call the MSALRuntime API to populate the bufferSize with the size of the
// String
boolean insufficientBufferError = MsalRuntimeInterop.ERROR_HELPER.checkResponseStatus(
getMSALRuntimeString.getString(handle, null, bufferSize),
MsalRuntimeResponseStatus.MSALRUNTIME_RESPONSE_STATUS_INSUFFICIENTBUFFER);
// If we get an insufficient buffer error like we expect, then the bufferSize should have
// been populated and we can get the actual String
if (insufficientBufferError) {
if (bufferSize.getValue() != 0) {
// Create a memory location the same size as the String we want to retrieve
Pointer stringMemoryLocation = new Memory(Native.WCHAR_SIZE * (bufferSize.getValue()));
// Send that memory location to MSALRuntime to copy the String into
MsalRuntimeInterop.ERROR_HELPER.checkMsalRuntimeError(
getMSALRuntimeString.getString(handle, stringMemoryLocation, bufferSize));
// Retrieve the copied string from the memory location
return stringMemoryLocation.getWideString(0);
} else {
// String of size 0, nothing to retrieve
LOG.warn("Buffer size is 0");
return "";
}
} else {
LOG.warn("Could not parse string.");
}
return "";
}
/**
* Interface which allows an MSALRuntime_RELEASE* function to be passed as a parameter, allowing
* all MSALRuntime release methods to be called by this class
*/
@FunctionalInterface
interface ReleaseMethod {
ErrorHandle release(long handle);
}
/**
* Interface which allows data to be retrieved from any MSALRuntime API which populates a String
*/
@FunctionalInterface
interface GetMsalRuntimeString {
ErrorHandle getString(HandleBase handle, Pointer stringReference, IntByReference bufferSize);
}
/**
* Class used to represent a phantom reference to a Handle instance, allowing handles to be
* released via AsyncHandleFinalizerThread when the phantom reference is the instance's only
* reference
*
* This class cannot have reference to the actual Handle instance, since that would create a
* strong reference that is never removed, so it must hold copies of any Handle data needed for
* calling the MSALRuntime release API
*/
class HandleFinalizer extends PhantomReference {
private final Logger LOG = LoggerFactory.getLogger(HandleFinalizer.class);
private LongByReference finalizerMsalRuntimeHandle;
private ReleaseMethod finalizerReleaseMethod;
private HandleFinalizer(
HandleBase handle,
LongByReference msalRuntimeHandle,
ReleaseMethod releaseMethod,
ReferenceQueue queue) {
super(handle, queue);
// Copy Handle's value and release methods, so the
this.finalizerMsalRuntimeHandle = msalRuntimeHandle;
this.finalizerReleaseMethod = releaseMethod;
}
private void release() {
LOG.debug("Releasing a handle via PhantomReference");
if (finalizerMsalRuntimeHandle != null && finalizerMsalRuntimeHandle.getValue() != 0) {
try {
// Release handle using the MSALRuntime API set when this object was created
MsalRuntimeInterop.ERROR_HELPER.checkMsalRuntimeError(
finalizerReleaseMethod.release(finalizerMsalRuntimeHandle.getValue()));
// Set local handle to null, to indicate that it has been released and prevent
// attempts to use it again
this.finalizerMsalRuntimeHandle = null;
} catch (MsalInteropException e) {
throw e;
} catch (Exception e) {
MsalRuntimeInterop.ERROR_HELPER.logUnknownErrorReleasingHandle(e);
}
}
}
}
/**
* Thread which will start when the first Handle is created.
*
* This thread will be responsible for releasing handles in scenarios where we can't release
* them immediately, and as a fail-safe in case a handle isn't released properly
*/
class HandleFinalizerThread extends Thread {
private final Logger LOG = LoggerFactory.getLogger(HandleFinalizerThread.class);
private ReferenceQueue handleReferenceQueue = new ReferenceQueue<>();
HandleFinalizerThread() {
setDaemon(true);
}
/**
* Create a new PhantomReference to a give Handle by creating a HandleFinalizer with this
* Handle's value and release method When the PhantomReference is the only remaining
* reference to the Handle, the HandleFinalizer will appear in handleReferenceQueue and the
* Handle will be released
*/
void addReference(HandleBase handle, LongByReference msalRuntimeHandle, ReleaseMethod releaseMethod) {
// When the first Handle is created, start the finalizer thread that all Handles share
if (!HANDLE_FINALIZER_THREAD.isAlive()) {
// Set up unknown exception handling for the thread, to ensure as much as possible
// gets released cleanly
HANDLE_FINALIZER_THREAD.setUncaughtExceptionHandler((th, ex) -> {
LOG.error(
"Unexpected exception in HandleFinalizerThread with {} open async handles. Will attempt to "
+ "cancel any async operations before stopping thread.",
MsalRuntimeFuture.msalRuntimeFutures.size());
for (MsalRuntimeFuture future : MsalRuntimeFuture.msalRuntimeFutures.values()) {
future.cancelAsyncOperation();
future.handle.release();
}
});
HANDLE_FINALIZER_THREAD.start();
}
new HandleFinalizer(handle, msalRuntimeHandle, releaseMethod, handleReferenceQueue);
}
@Override
public void run() {
try {
while (true) {
// Although this is an infinite loop, ReferenceQueue's remove() method causes it
// to wait until an entry appears in handleReferenceQueue. This will only happen
// when a Handle is reachable only through a PhantomReference, and can therefore
// be released
HandleFinalizer handleFinalizer = (HandleFinalizer)handleReferenceQueue.remove();
LOG.info("Found Handle with no references, closing.");
handleFinalizer.release();
}
} catch (InterruptedException e) {
// Ideally, this will only run when the entire program shuts down, and most handles
// will be released via their close() method if their in a try-with-resources block
//
// MsalRuntimeFuture.msalRuntimeFutures allows us to track async handles, so we can
// at least guarantee they always get canceled/released
LOG.error(
"HandleFinalizerThread interrupted with {} open async handles. Will attempt to cancel any async "
+ "operations before stopping thread.",
MsalRuntimeFuture.msalRuntimeFutures.size());
for (MsalRuntimeFuture future : MsalRuntimeFuture.msalRuntimeFutures.values()) {
future.cancelAsyncOperation();
future.handle.release();
}
}
}
}
}