• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

openmrs / openmrs-core / 29845725671

21 Jul 2026 03:48PM UTC coverage: 63.994% (+0.01%) from 63.981%
29845725671

push

github

web-flow
TRUNK-6713: Switch to saner DaemonThread protection scheme (#6360)

54 of 80 new or added lines in 6 files covered. (67.5%)

7 existing lines in 6 files now uncovered.

24227 of 37858 relevant lines covered (63.99%)

0.64 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

53.9
/api/src/main/java/org/openmrs/api/context/Daemon.java
1
/**
2
 * This Source Code Form is subject to the terms of the Mozilla Public License,
3
 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
4
 * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
5
 * the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
6
 *
7
 * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
8
 * graphic logo is a trademark of OpenMRS Inc.
9
 */
10
package org.openmrs.api.context;
11

12
import java.util.List;
13
import java.util.concurrent.Callable;
14
import java.util.concurrent.CountDownLatch;
15
import java.util.concurrent.ExecutionException;
16
import java.util.concurrent.Future;
17
import java.util.stream.Collectors;
18

19
import org.apache.commons.collections.CollectionUtils;
20
import org.openmrs.Role;
21
import org.openmrs.User;
22
import org.openmrs.api.APIAuthenticationException;
23
import org.openmrs.api.APIException;
24
import org.openmrs.api.OpenmrsService;
25
import org.openmrs.api.db.ContextDAO;
26
import org.openmrs.api.db.hibernate.HibernateContextDAO;
27
import org.openmrs.module.DaemonToken;
28
import org.openmrs.module.Module;
29
import org.openmrs.module.ModuleException;
30
import org.openmrs.module.ModuleFactory;
31
import org.openmrs.scheduler.jobrunr.JobRequestHandlerAdapter;
32
import org.openmrs.util.OpenmrsThreadPoolHolder;
33
import org.slf4j.Logger;
34
import org.slf4j.LoggerFactory;
35
import org.springframework.context.support.AbstractRefreshableApplicationContext;
36

37
/**
38
 * This class allows certain tasks to run with elevated privileges. Primary use is scheduling and
39
 * module startup when there is no user to authenticate as.
40
 */
41
public final class Daemon {
42

43
        /**
44
         * The uuid defined for the daemon user object
45
         */
46
        static final String DAEMON_USER_UUID = "A4F30A1B-5EB9-11DF-A648-37A07F9C90FB";
47

48
        private static final ThreadLocal<Boolean> isDaemonThread = new ThreadLocal<>();
1✔
49

50
        private static final ThreadLocal<User> daemonThreadUser = new ThreadLocal<>();
1✔
51

52
        private static final Logger log = LoggerFactory.getLogger("org.openmrs.api");
1✔
53

54
        /**
55
         * Capability token passed to expected callers allowed to create DaemonThreads.
56
         *
57
         * @since 3.0.0, 2.9.0, 2.8.9
58
         */
59
        public static final class CallerKey {
60

61
                private CallerKey() {
62
                }
63
        }
64

65
        /**
66
         * The single {@link CallerKey} instance. Must not be exposed to the public API
67
         */
68
        private static final CallerKey CALLER_KEY = new CallerKey();
1✔
69

70
        static {
71
                // send the key to our known collaborators
72
                HibernateContextDAO.setDaemonCallerKey(CALLER_KEY);
1✔
73
                ModuleFactory.setDaemonCallerKey(CALLER_KEY);
1✔
74
                JobRequestHandlerAdapter.setDaemonCallerKey(CALLER_KEY);
1✔
75
                // WebDaemon lives in the web module, which the api module cannot reference at compile time, so
76
                // hand it the key reflectively.
77
                try {
NEW
78
                        Class.forName("org.openmrs.web.WebDaemon").getMethod("setDaemonCallerKey", CallerKey.class).invoke(null,
×
79
                            CALLER_KEY);
80
                }
81
                catch (ClassNotFoundException e) {
1✔
82
                        log.debug("Could not load WebDaemon class", e);
1✔
83
                }
NEW
84
                catch (ReflectiveOperationException e) {
×
NEW
85
                        log.error("Exception caught while trying to provide DaemonCallerKey to WebDaemon", e);
×
86
                }
1✔
87
        }
1✔
88

89
        /**
90
         * private constructor to override the default constructor to prevent it from being instantiated.
91
         */
92
        private Daemon() {
93
        }
94

95
        /**
96
         * Forces this class to initialize, which distributes the caller key to its trusted collaborators.
97
         *
98
         * @since 3.0.0, 2.9.0, 2.8.9
99
         */
100
        public static void ensureInitialized() {
101
                // Merely invoking a static method guarantees Daemon's static initializer has run; there is
102
                // deliberately nothing else to do here.
NEW
103
        }
×
104

105
        /**
106
         * @see #startModule(Module, boolean, AbstractRefreshableApplicationContext, CallerKey)
107
         */
108
        public static Module startModule(Module module) throws ModuleException {
NEW
109
                return startModule(module, false, null, CALLER_KEY);
×
110
        }
111

112
        /**
113
         * This method should not be called directly. The {@link ModuleFactory#startModule(Module)} method
114
         * uses this to start the given module in a new thread that is authenticated as the daemon user.
115
         * <br>
116
         * If a non-null application context is passed in, it gets refreshed to make the module's services
117
         * available
118
         *
119
         * @param module the module to start
120
         * @param isOpenmrsStartup Specifies whether this module is being started at application startup or
121
         *            not
122
         * @param applicationContext the spring application context instance to refresh
123
         * @param callerKey the {@link CallerKey} proving the caller is permitted to start modules
124
         * @return the module returned from {@link ModuleFactory#startModuleInternal(Module)}
125
         */
126
        public static Module startModule(final Module module, final boolean isOpenmrsStartup,
127
                final AbstractRefreshableApplicationContext applicationContext, CallerKey callerKey) throws ModuleException {
128
                requireDaemonCaller(callerKey, "Module.factory.only");
1✔
129

130
                Future<Module> moduleStartFuture = runInDaemonThreadInternal(
1✔
131
                    () -> ModuleFactory.startModuleInternal(module, isOpenmrsStartup, applicationContext));
1✔
132

133
                // wait for the "startModule" thread to finish
134
                try {
135
                        return moduleStartFuture.get();
1✔
NEW
136
                } catch (InterruptedException e) {
×
137
                        // ignore
138
                } catch (ExecutionException e) {
×
139
                        if (e.getCause() instanceof ModuleException) {
×
140
                                throw (ModuleException) e.getCause();
×
141
                        } else {
142
                                throw new ModuleException("Unable to start module " + module.getName(), e);
×
143
                        }
144
                }
×
145

146
                return module;
×
147
        }
148

149
        /**
150
         * This method should not be called directly; it is guarded by the daemon caller key, which is
151
         * issued only to the {@link ContextDAO} implementation.
152
         * <p>
153
         * <strong>Should</strong> only allow the creation of new users, not the edition of existing ones
154
         *
155
         * @param user A new user to be created.
156
         * @param password The password to set for the new user.
157
         * @param roleNames A list of role names to fetch the roles to add to the user.
158
         * @param callerKey the {@link CallerKey} proving the caller is permitted to create users
159
         * @return The newly created user
160
         * @since 2.3.0
161
         */
162
        public static User createUser(User user, String password, List<String> roleNames, CallerKey callerKey) throws Exception {
163
                requireDaemonCaller(callerKey, "Context.DAO.only");
1✔
164

165
                // create a new thread and execute that task in it
166
                Future<User> userFuture = runInDaemonThreadInternal(() -> {
1✔
167
                        if ((user.getId() != null && Context.getUserService().getUser(user.getId()) != null)
1✔
168
                                || Context.getUserService().getUserByUuid(user.getUuid()) != null
1✔
169
                                || Context.getUserService().getUserByUsername(user.getUsername()) != null || (user.getEmail() != null
1✔
NEW
170
                                        && Context.getUserService().getUserByUsernameOrEmail(user.getEmail()) != null)) {
×
171
                                throw new APIException("User.creating.already.exists", new Object[] { user.getDisplayString() });
1✔
172
                        }
173

174
                        if (!CollectionUtils.isEmpty(roleNames)) {
1✔
175
                                List<Role> roles = roleNames.stream().map(roleName -> Context.getUserService().getRole(roleName))
1✔
176
                                        .collect(Collectors.toList());
1✔
177
                                roles.forEach(user::addRole);
1✔
178
                        }
179

180
                        return Context.getUserService().createUser(user, password);
1✔
181
                });
182

183
                // wait for the 'create user' thread to finish
184
                try {
185
                        return userFuture.get();
1✔
NEW
186
                } catch (InterruptedException e) {
×
187
                        // ignore
188
                } catch (ExecutionException e) {
1✔
189
                        if (e.getCause() instanceof Exception) {
1✔
190
                                throw (Exception) e.getCause();
1✔
191
                        } else {
192
                                throw new RuntimeException(e.getCause());
×
193
                        }
194
                }
×
195

196
                return null;
×
197
        }
198

199
        /**
200
         * Call this method if you are inside a Daemon thread (for example in a Module activator or a
201
         * scheduled task) and you want to start up a new parallel Daemon thread. You may only call this
202
         * method from a Daemon thread.
203
         *
204
         * @param runnable what to run in a new thread
205
         * @return the newly spawned {@link Thread}
206
         * @deprecated As of 2.7.0, consider using {@link #runNewDaemonTask(Runnable)} instead
207
         */
208
        @Deprecated
209
        public static Thread runInNewDaemonThread(final Runnable runnable) {
210
                // make sure we're already in a daemon thread
211
                if (!isDaemonThread()) {
1✔
212
                        throw new APIAuthenticationException("Only daemon threads can spawn new daemon threads");
1✔
213
                }
214

215
                // the previous implementation ensured that Thread.start() was called before this function returned
216
                // since we cannot guarantee that the executor will run the thread when `execute()` is called, we need another
217
                // mechanism to ensure the submitted Runnable was actually started.
218
                final CountDownLatch countDownLatch = new CountDownLatch(1);
×
219

220
                // we should consider making DaemonThread public, so the caller can access returnedObject and exceptionThrown
221
                DaemonThread thread = new DaemonThread() {
×
222

223
                        @Override
224
                        public void run() {
225
                                isDaemonThread.set(true);
×
226
                                try {
227
                                        Context.openSession();
×
228
                                        countDownLatch.countDown();
×
229
                                        //Suppressing sonar issue "squid:S1217"
230
                                        //We intentionally do not start a new thread yet, rather wrap the run call in a session.
231
                                        runnable.run();
×
232
                                } finally {
233
                                        try {
234
                                                Context.closeSession();
×
235
                                        } finally {
236
                                                isDaemonThread.remove();
×
237
                                                daemonThreadUser.remove();
×
238
                                        }
239
                                }
240
                        }
×
241
                };
242

243
                OpenmrsThreadPoolHolder.threadExecutor.execute(thread);
×
244

245
                // do not return until the thread is actually started to emulate the previous behaviour
246
                try {
247
                        countDownLatch.await();
×
NEW
248
                } catch (InterruptedException ignored) {}
×
249

250
                return thread;
×
251
        }
252

253
        /**
254
         * Call this method if you are inside a Daemon thread (for example in a Module activator or a
255
         * scheduled task) and you want to start up a new parallel Daemon thread. You may only call this
256
         * method from a Daemon thread.
257
         *
258
         * @param callable what to run in a new thread
259
         * @return a future that completes when the task is done;
260
         * @since 2.7.0
261
         */
262
        @SuppressWarnings({ "squid:S1217", "unused" })
263
        public static <T> Future<T> runInNewDaemonThread(final Callable<T> callable) {
264
                // make sure we're already in a daemon thread
265
                if (!isDaemonThread()) {
1✔
266
                        throw new APIAuthenticationException("Only daemon threads can spawn new daemon threads");
1✔
267
                }
268

269
                return runInDaemonThreadInternal(callable);
×
270
        }
271

272
        /**
273
         * Call this method if you are inside a Daemon thread (for example in a Module activator or a
274
         * scheduled task) and you want to start up a new parallel Daemon thread. You may only call this
275
         * method from a Daemon thread.
276
         *
277
         * @param runnable what to run in a new thread
278
         * @return a future that completes when the task is done;
279
         * @since 2.7.0
280
         */
281
        @SuppressWarnings({ "squid:S1217", "unused" })
282
        public static Future<?> runNewDaemonTask(final Runnable runnable) {
283
                // make sure we're already in a daemon thread
284
                if (!isDaemonThread()) {
1✔
285
                        throw new APIAuthenticationException("Only daemon threads can spawn new daemon threads");
1✔
286
                }
287

NEW
288
                return runInDaemonThreadInternal(runnable);
×
289
        }
290

291
        /**
292
         * Runs the given task on a new daemon thread, authorized by a {@link CallerKey}. This exists
293
         * strictly for internal use by trusted core entry points (such as {@code WebDaemon} startup) that
294
         * must launch daemon work from a non-daemon thread.
295
         *
296
         * @param runnable what to run in a new daemon thread
297
         * @param callerKey the {@link CallerKey} proving the caller is a trusted daemon entry point
298
         * @return a future that completes when the task is done
299
         * @since 3.0.0, 2.9.0, 2.8.9
300
         */
301
        @SuppressWarnings({ "squid:S1217", "unused" })
302
        public static Future<?> runNewDaemonTask(final Runnable runnable, CallerKey callerKey) {
303
                requireDaemonCaller(callerKey, "runNewDaemonTask can only be called by an authorized daemon entry point");
1✔
304

305
                return runInDaemonThreadInternal(runnable);
1✔
306
        }
307

308
        /**
309
         * @return true if the current thread was started by this class and so is a daemon thread that has
310
         *         all privileges
311
         * @see Context#hasPrivilege(String)
312
         */
313
        public static boolean isDaemonThread() {
314
                Boolean b = isDaemonThread.get();
1✔
315
                return b != null && b;
1✔
316
        }
317

318
        /**
319
         * Calls the {@link OpenmrsService#onStartup()} method, as a daemon, for an instance implementing
320
         * the {@link OpenmrsService} interface.
321
         *
322
         * @param service instance implementing the {@link OpenmrsService} interface.
323
         * @param callerKey the {@link CallerKey} proving the caller is permitted to run service startup
324
         * @since 1.9
325
         */
326
        public static void runStartupForService(final OpenmrsService service, CallerKey callerKey) throws ModuleException {
327
                requireDaemonCaller(callerKey, "Service.context.only");
1✔
328

329
                Future<?> future = runInDaemonThreadInternal(service::onStartup);
1✔
330

331
                // wait for the "onStartup" thread to finish
332
                try {
333
                        future.get();
1✔
NEW
334
                } catch (InterruptedException e) {
×
335
                        // ignore
NEW
336
                } catch (ExecutionException e) {
×
337
                        if (e.getCause() instanceof ModuleException) {
×
338
                                throw (ModuleException) e.getCause();
×
339
                        } else {
NEW
340
                                throw new ModuleException("Unable to run onStartup() method of service {}",
×
NEW
341
                                        service.getClass().getSimpleName(), e);
×
342
                        }
343
                }
1✔
344
        }
1✔
345

346
        /**
347
         * Executes the given runnable in a new thread that is authenticated as the daemon user.
348
         *
349
         * @param runnable an object implementing the {@link Runnable} interface.
350
         * @param token the token required to run code as the daemon user
351
         * @return the newly spawned {@link Thread}
352
         * @since 1.9.2
353
         * @deprecated Since 2.7.0 use {@link #runInDaemonThreadWithoutResult(Runnable, DaemonToken)}
354
         *             instead
355
         */
356
        @Deprecated
357
        @SuppressWarnings({ "squid:S1217", "unused" })
358
        public static Thread runInDaemonThread(final Runnable runnable, DaemonToken token) {
359
                if (!ModuleFactory.isTokenValid(token)) {
×
360
                        throw new ContextAuthenticationException("Invalid token " + token);
×
361
                }
362

363
                DaemonThread thread = new DaemonThread() {
×
364

365
                        @Override
366
                        public void run() {
367
                                isDaemonThread.set(true);
×
368
                                try {
369
                                        Context.openSession();
×
370
                                        //Suppressing sonar issue "squid:S1217"
371
                                        //We intentionally do not start a new thread yet, rather wrap the run call in a session.
372
                                        runnable.run();
×
373
                                } finally {
374
                                        try {
375
                                                Context.closeSession();
×
376
                                        } finally {
377
                                                isDaemonThread.remove();
×
378
                                                daemonThreadUser.remove();
×
379
                                        }
380
                                }
381
                        }
×
382
                };
383

384
                OpenmrsThreadPoolHolder.threadExecutor.execute(thread);
×
385
                return thread;
×
386
        }
387

388
        /**
389
         * Executes the given runnable in a new thread that is authenticated as the daemon user.
390
         *
391
         * @param callable an object implementing the {@link Callable<T>} interface to be run
392
         * @param token the token required to run code as the daemon user
393
         * @return the newly spawned {@link Thread}
394
         * @since 2.7.0
395
         */
396
        @SuppressWarnings({ "squid:S1217", "unused" })
397
        public static <T> Future<T> runInDaemonThread(final Callable<T> callable, DaemonToken token) {
398
                if (!ModuleFactory.isTokenValid(token)) {
×
399
                        throw new ContextAuthenticationException("Invalid token");
×
400
                }
401

402
                return runInDaemonThreadInternal(callable);
×
403
        }
404

405
        /**
406
         * Executes the given runnable in a new thread that is authenticated as the daemon user.
407
         *
408
         * @param runnable an object implementing the {@link Runnable} interface to be run
409
         * @param token the token required to run code as the daemon user
410
         * @return the newly spawned {@link Thread}
411
         * @since 2.7.0
412
         */
413
        @SuppressWarnings("squid:S1217")
414
        public static Future<?> runInDaemonThreadWithoutResult(final Runnable runnable, DaemonToken token) {
415
                if (!ModuleFactory.isTokenValid(token)) {
×
416
                        throw new ContextAuthenticationException("Invalid token");
×
417
                }
418

419
                return runInDaemonThreadInternal(runnable);
×
420
        }
421

422
        /**
423
         * Executes the given runnable in a new thread that is authenticated as the daemon user and wait for
424
         * the thread to finish.
425
         *
426
         * @param runnable an object implementing the {@link Runnable} interface.
427
         * @param token the token required to run code as the daemon user
428
         * @since 2.7.0
429
         */
430
        public static void runInDaemonThreadAndWait(final Runnable runnable, DaemonToken token) {
431
                Future<?> daemonThread = runInDaemonThreadWithoutResult(runnable, token);
×
432

433
                try {
434
                        daemonThread.get();
×
NEW
435
                } catch (InterruptedException | ExecutionException e) {
×
436
                        // Ignored
437
                }
×
438
        }
×
439

440
        private static <T> Future<T> runInDaemonThreadInternal(Callable<T> callable) {
441
                return OpenmrsThreadPoolHolder.threadExecutor.submit(() -> {
1✔
442
                        isDaemonThread.set(true);
1✔
443
                        try {
444
                                Context.openSession();
1✔
445
                                return callable.call();
1✔
446
                        } finally {
447
                                try {
448
                                        Context.closeSession();
1✔
449
                                } finally {
450
                                        isDaemonThread.remove();
1✔
451
                                        daemonThreadUser.remove();
1✔
452
                                }
453
                        }
454
                });
455
        }
456

457
        private static Future<?> runInDaemonThreadInternal(Runnable runnable) {
458
                // for Threads, we used to guarantee that Thread.start() was called before the function returned
459
                // since we cannot guarantee that the executor actually started executing the thread, we use a CountDownLatch
460
                // to emulate this behaviour when the user submits a Thread. Other runnables are unaffected.
461
                CountDownLatch countDownLatch = getCountDownLatch(runnable instanceof Thread);
1✔
462

463
                Future<?> result = OpenmrsThreadPoolHolder.threadExecutor.submit(() -> {
1✔
464
                        isDaemonThread.set(true);
1✔
465
                        try {
466
                                Context.openSession();
1✔
467
                                countDownLatch.countDown();
1✔
468
                                runnable.run();
1✔
469
                        } finally {
470
                                try {
471
                                        Context.closeSession();
1✔
472
                                } finally {
473
                                        isDaemonThread.remove();
1✔
474
                                        daemonThreadUser.remove();
1✔
475
                                }
476
                        }
477
                });
1✔
478

479
                try {
480
                        countDownLatch.await();
1✔
481
                } catch (InterruptedException ignored) {}
1✔
482

483
                return result;
1✔
484
        }
485

486
        private static CountDownLatch getCountDownLatch(boolean isThread) {
487
                return isThread ? new CountDownLatch(1) : new CountDownLatch(0);
1✔
488
        }
489

490
        /**
491
         * @return the capability required to invoke Daemon's guarded entry points. Package-private, so it
492
         *         is reachable only by trusted collaborators.
493
         */
494
        static CallerKey callerKey() {
495
                return CALLER_KEY;
1✔
496
        }
497

498
        /**
499
         * Rejects the call unless the supplied key is the genuine {@link #CALLER_KEY}. Fails closed for any
500
         * other value (including null).
501
         *
502
         * @param callerKey the key presented by the caller
503
         * @param messageCode the message (code) identifying the guarded operation
504
         */
505
        private static void requireDaemonCaller(CallerKey callerKey, String messageCode) {
506
                if (callerKey != CALLER_KEY) {
1✔
507
                        throw new APIException(messageCode, new Object[] { "an unauthorized caller" });
1✔
508
                }
509
        }
1✔
510

511
        /**
512
         * Executes the given task as the given user. <br>
513
         * <br>
514
         * This is guarded by the daemon caller key, which is issued only to {@link JobRequestHandlerAdapter}.
515
         * <p>
516
         * <strong>Should</strong> not be called from other methods other than JobRequestHandlerAdapter
517
         * <strong>Should</strong> not throw error if called from a JobRequestHandlerAdapter class
518
         *
519
         * @param userSystemId the user to run as
520
         * @param runnable the task to run
521
         * @param callerKey the {@link CallerKey} proving the caller is permitted to execute scheduled tasks
522
         * @since 2.9.0
523
         */
524
        public static void executeScheduledTaskAsUser(String userSystemId, DaemonTask runnable, CallerKey callerKey)
525
                throws Exception {
526
                requireDaemonCaller(callerKey, "executeScheduledTaskAsUser can only be called from JobRequestHandlerAdapter");
1✔
527

528
                isDaemonThread.set(true);
1✔
529
                try {
530
                        Context.openSession();
1✔
531
                        Context.getUserContext().becomeUser(userSystemId);
×
532
                        isDaemonThread.remove();
×
533
                        runnable.run();
×
534
                } finally {
535
                        isDaemonThread.remove();
1✔
536
                        Context.closeSession();
1✔
537
                }
538
        }
×
539

540
        /**
541
         * Thread class used by the {@link Daemon#startModule(Module)} method so that the returned object
542
         * and the exception thrown can be returned to calling class
543
         */
544
        protected static class DaemonThread extends Thread {
×
545

546
                /**
547
                 * The object returned from the method called in {@link #run()}
548
                 */
549
                protected Object returnedObject = null;
×
550

551
                /**
552
                 * The exception thrown (if any) by the method called in {@link #run()}
553
                 */
554
                protected Exception exceptionThrown = null;
×
555

556
                /**
557
                 * Gets the exception thrown (if any) by the method called in {@link #run()}
558
                 *
559
                 * @return the thrown exception (if any).
560
                 */
561
                public Exception getExceptionThrown() {
562
                        return exceptionThrown;
×
563
                }
564
        }
565

566
        @FunctionalInterface
567
        public interface DaemonTask {
568

569
                void run() throws Exception;
570
        }
571

572
        /**
573
         * Checks whether user is Daemon. However, this is not the preferred method for checking to see if
574
         * the current thread is a daemon thread, rather use {@link #isDaemonThread()}. isDaemonThread is
575
         * preferred for checking to see if you are in that thread or if the current thread is daemon.
576
         *
577
         * @param user user whom we are checking if daemon
578
         * @return true if user is Daemon
579
         */
580
        public static boolean isDaemonUser(User user) {
581
                return DAEMON_USER_UUID.equals(user.getUuid());
1✔
582
        }
583

584
        /**
585
         * @return the current thread daemon user or null if not assigned
586
         * @since 2.0.0, 1.12.0, 1.11.6, 1.10.4, 1.9.11
587
         */
588
        public static User getDaemonThreadUser() {
589
                if (isDaemonThread()) {
1✔
590
                        User user = daemonThreadUser.get();
1✔
591
                        if (user == null) {
1✔
592
                                user = Context.getContextDAO().getUserByUuid(DAEMON_USER_UUID);
1✔
593
                                daemonThreadUser.set(user);
1✔
594
                        }
595
                        return user;
1✔
596
                } else {
597
                        return null;
×
598
                }
599
        }
600

601
        public static String getDaemonUserUuid() {
602
                return DAEMON_USER_UUID;
1✔
603
        }
604
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc