/**
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
* the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
*
* Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
* graphic logo is a trademark of OpenMRS Inc.
*/
package org.openmrs.api.context;
import org.openmrs.User;
import org.openmrs.api.APIAuthenticationException;
import org.openmrs.api.APIException;
import org.openmrs.api.OpenmrsService;
import org.openmrs.module.DaemonToken;
import org.openmrs.module.Module;
import org.openmrs.module.ModuleException;
import org.openmrs.module.ModuleFactory;
import org.openmrs.scheduler.Task;
import org.openmrs.scheduler.timer.TimerSchedulerTask;
import org.openmrs.util.OpenmrsSecurityManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.support.AbstractRefreshableApplicationContext;
/**
* This class allows certain tasks to run with elevated privileges. Primary use is scheduling and
* module startup when there is no user to authenticate as.
*/
public class Daemon {
protected static final Logger log = LoggerFactory.getLogger(Daemon.class);
/**
* The uuid defined for the daemon user object
*/
protected static final String DAEMON_USER_UUID = "A4F30A1B-5EB9-11DF-A648-37A07F9C90FB";
protected static final ThreadLocal<Boolean> isDaemonThread = new ThreadLocal<Boolean>();
protected static final ThreadLocal<User> daemonThreadUser = new ThreadLocal<User>();
/**
* @see #startModule(Module, boolean, AbstractRefreshableApplicationContext)
*/
public static Module startModule(Module module) throws ModuleException {
return startModule(module, false, null);
}
/**
* This method should not be called directly. The {@link ModuleFactory#startModule(Module)}
* method uses this to start the given module in a new thread that is authenticated as the
* daemon user. <br>
* If a non null application context is passed in, it gets refreshed to make the module's
* services available
*
* @param module the module to start
* @param isOpenmrsStartup Specifies whether this module is being started at application startup
* or not
* @param applicationContext the spring application context instance to refresh
* @return the module returned from {@link ModuleFactory#startModuleInternal(Module)}
*/
public static Module startModule(final Module module, final boolean isOpenmrsStartup,
final AbstractRefreshableApplicationContext applicationContext) throws ModuleException {
// create a new thread and execute that task in it
DaemonThread startModuleThread = new DaemonThread() {
@Override
public void run() {
isDaemonThread.set(true);
try {
Context.openSession();
returnedObject = ModuleFactory.startModuleInternal(module, isOpenmrsStartup, applicationContext);
}
catch (Exception e) {
exceptionThrown = e;
}
finally {
Context.closeSession();
}
}
};
startModuleThread.start();
// wait for the "startModule" thread to finish
try {
startModuleThread.join();
}
catch (InterruptedException e) {
// ignore
}
if (startModuleThread.exceptionThrown != null) {
if (startModuleThread.exceptionThrown instanceof ModuleException) {
throw (ModuleException) startModuleThread.exceptionThrown;
} else {
throw new ModuleException("Unable to start module as Daemon", startModuleThread.exceptionThrown);
}
}
Module startedModule = (Module) startModuleThread.returnedObject;
return startedModule;
}
/**
* Executes the given task in a new thread that is authenticated as the daemon user. <br>
* <br>
* This can only be called from {@link TimerSchedulerTask} during actual task execution
*
* @param task the task to run
* @should not be called from other methods other than TimerSchedulerTask
* @should not throw error if called from a TimerSchedulerTask class
*/
public static void executeScheduledTask(final Task task) throws Exception {
// quick check to make sure we're only being called by ourselves
//Class<?> callerClass = Reflection.getCallerClass(0);
Class<?> callerClass = new OpenmrsSecurityManager().getCallerClass(0);
if (!TimerSchedulerTask.class.isAssignableFrom(callerClass)) {
throw new APIException("Scheduler.timer.task.only", new Object[] { callerClass.getName() });
}
// now create a new thread and execute that task in it
DaemonThread executeTaskThread = new DaemonThread() {
@Override
public void run() {
isDaemonThread.set(true);
try {
Context.openSession();
TimerSchedulerTask.execute(task);
}
catch (Exception e) {
exceptionThrown = e;
}
finally {
Context.closeSession();
}
}
};
executeTaskThread.start();
// wait for the "executeTaskThread" thread to finish
try {
executeTaskThread.join();
}
catch (InterruptedException e) {
// ignore
}
if (executeTaskThread.exceptionThrown != null) {
throw executeTaskThread.exceptionThrown;
}
}
/**
* Call this method if you are inside a Daemon thread (for example in a Module activator or a
* scheduled task) and you want to start up a new parallel Daemon thread. You may only call this
* method from a Daemon thread.
*
* @param runnable what to run in a new thread
* @return the newly spawned {@link Thread}
* @should throw error if called from a non daemon thread
* @should not throw error if called from a daemon thread
*/
@SuppressWarnings("squid:S1217")
public static Thread runInNewDaemonThread(final Runnable runnable) {
// make sure we're already in a daemon thread
if (!isDaemonThread()) {
throw new APIAuthenticationException("Only daemon threads can spawn new daemon threads");
}
// we should consider making DaemonThread public, so the caller can access returnedObject and exceptionThrown
DaemonThread thread = new DaemonThread() {
@Override
public void run() {
isDaemonThread.set(true);
try {
Context.openSession();
//Suppressing sonar issue "squid:S1217"
//We intentionally do not start a new thread yet, rather wrap the run call in a session.
runnable.run();
}
finally {
Context.closeSession();
}
}
};
thread.start();
return thread;
}
/**
* @return true if the current thread was started by this class and so is a daemon thread that
* has all privileges
* @see Context#hasPrivilege(String)
*/
public static boolean isDaemonThread() {
Boolean b = isDaemonThread.get();
if (b == null) {
return false;
} else {
return b.booleanValue();
}
}
/**
* Calls the {@link OpenmrsService#onStartup()} method, as a daemon, for an instance
* implementing the {@link OpenmrsService} interface.
*
* @param service instance implementing the {@link OpenmrsService} interface.
* @since 1.9
*/
public static void runStartupForService(final OpenmrsService service) throws ModuleException {
DaemonThread onStartupThread = new DaemonThread() {
@Override
public void run() {
isDaemonThread.set(true);
try {
Context.openSession();
service.onStartup();
}
catch (Exception e) {
exceptionThrown = e;
}
finally {
Context.closeSession();
}
}
};
onStartupThread.start();
// wait for the "onStartup" thread to finish
try {
onStartupThread.join();
}
catch (InterruptedException e) {
// ignore
log.error("Thread was interrupted", e);
}
if (onStartupThread.exceptionThrown != null) {
if (onStartupThread.exceptionThrown instanceof ModuleException) {
throw (ModuleException) onStartupThread.exceptionThrown;
} else {
throw new ModuleException("Unable to run onStartup() method as Daemon", onStartupThread.exceptionThrown);
}
}
}
/**
* Executes the given runnable in a new thread that is authenticated as the daemon user.
*
* @param runnable an object implementing the {@link Runnable} interface.
* @param token the token required to run code as the daemon user
* @return the newly spawned {@link Thread}
* @since 1.9.2
*/
@SuppressWarnings("squid:S1217")
public static Thread runInDaemonThread(final Runnable runnable, DaemonToken token) {
if (!ModuleFactory.isTokenValid(token)) {
throw new ContextAuthenticationException("Invalid token " + token);
}
DaemonThread thread = new DaemonThread() {
@Override
public void run() {
isDaemonThread.set(true);
try {
Context.openSession();
//Suppressing sonar issue "squid:S1217"
//We intentionally do not start a new thread yet, rather wrap the run call in a session.
runnable.run();
}
finally {
Context.closeSession();
}
}
};
thread.start();
return thread;
}
/**
* Executes the given runnable in a new thread that is authenticated as the daemon user and wait
* for the thread to finish.
*
* @param runnable an object implementing the {@link Runnable} interface.
* @param token the token required to run code as the daemon user
* @since 1.9.2
*/
public static void runInDaemonThreadAndWait(final Runnable runnable, DaemonToken token) {
Thread daemonThread = runInDaemonThread(runnable, token);
try {
daemonThread.join();
}
catch (InterruptedException e) {
//Ignore
}
}
/**
* Thread class used by the {@link Daemon#startModule(Module)} and
* {@link Daemon#executeScheduledTask(Task)} methods so that the returned object and the
* exception thrown can be returned to calling class
*/
protected static class DaemonThread extends Thread {
/**
* The object returned from the method called in {@link #run()}
*/
protected Object returnedObject = null;
/**
* The exception thrown (if any) by the method called in {@link #run()}
*/
protected Exception exceptionThrown = null;
/**
* Gets the exception thrown (if any) by the method called in {@link #run()}
*
* @return the thrown exception (if any).
*/
public Exception getExceptionThrown() {
return exceptionThrown;
}
}
/**
* Checks whether user is Daemon.
* However this is not the preferred method for checking to see if the current thread is a daemon thread,
* rather use Daemon.isDeamonThread().
* isDaemonThread is preferred for checking to see if you are in that thread or if the current thread is daemon.
*
* @param user user whom we are checking if daemon
* @return true if user is Daemon
* @should return true for a daemon user
* @should return false if the user is not a daemon
*/
public static boolean isDaemonUser(User user) {
return DAEMON_USER_UUID.equals(user.getUuid());
}
/**
* @return the current thread daemon user or null if not assigned
* @since 2.0.0, 1.12.0, 1.11.6, 1.10.4, 1.9.11
*/
public static User getDaemonThreadUser() {
if (isDaemonThread()) {
User user = daemonThreadUser.get();
if (user == null) {
user = Context.getContextDAO().getUserByUuid(DAEMON_USER_UUID);
daemonThreadUser.set(user);
}
return user;
} else {
return null;
}
}
}