diff --git a/README.md b/README.md index 7356ec34..d15ce2bf 100644 --- a/README.md +++ b/README.md @@ -60,10 +60,12 @@ Property | Description | Default Value ------------ | ------------- | ---------- Email Enabled? | Check to enable email notification on completion of script execution. | `false` Email Recipients | Email addresses to receive notification. | `[]` -Allowed Groups | List of group names that are authorized to use the console. By default, only the `admin` user has permission to execute scripts. +Script Execution Allowed Groups | List of group names that are authorized to use the console. By default, only the 'admin' user has permission to execute scripts. | `[]` +Scheduled Jobs Allowed Groups | List of group names that are authorized to schedule jobs. By default, only the 'admin' user has permission to schedule jobs. | `[]` Vanity Path Enabled? | Enables `/groovyconsole` vanity path. | `false` Audit Disabled? | Disables auditing of script execution history. | `false` Display All Audit Records? | If enabled, all audit records (including records for other users) will be displayed in the console history. | `false` +Thread Timeout | Time in seconds that scripts are allowed to execute before being interrupted. If 0, no timeout is enforced. | 0 ## Batch Script Execution diff --git a/pom.xml b/pom.xml index ceb4c05f..d42980fb 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ com.icfolson.aem.groovy.console aem-groovy-console jar - 15.0.1 + 15.1.0 AEM Groovy Console The AEM Groovy Console provides an interface for running Groovy scripts in the AEM container. Scripts can be diff --git a/src/main/content/jcr_root/apps/groovyconsole/clientlibs/js/audit.js b/src/main/content/jcr_root/apps/groovyconsole/clientlibs/js/audit.js index 9afd15a5..8ce02a81 100644 --- a/src/main/content/jcr_root/apps/groovyconsole/clientlibs/js/audit.js +++ b/src/main/content/jcr_root/apps/groovyconsole/clientlibs/js/audit.js @@ -27,6 +27,9 @@ GroovyConsole.Audit = function () { data: 'date', searchable: false }, + { + data: 'jobTitle' + }, { data: 'script', orderable: false @@ -46,7 +49,7 @@ GroovyConsole.Audit = function () { order: [[2, 'desc']], language: { emptyTable: 'No audit records found.', - search: 'Script Contains: ', + search: 'Contains: ', zeroRecords: 'No matching audit records found.', info: 'Showing _START_ to _END_ of _TOTAL_ records', infoEmpty: '', @@ -58,8 +61,8 @@ GroovyConsole.Audit = function () { } $('td:eq(2)', row).html('' + data.date + ''); - $('td:eq(3)', row).html('' + data.scriptPreview + ''); - $('td:eq(3)', row).popover({ + $('td:eq(4)', row).html('' + data.scriptPreview + ''); + $('td:eq(4)', row).popover({ container: 'body', content: '
' + data.script + '
', html: true, @@ -68,7 +71,7 @@ GroovyConsole.Audit = function () { }); if (data.exception.length) { - $('td:eq(4)', row).html('' + data.exception + ''); + $('td:eq(5)', row).html('' + data.exception + ''); } } }); diff --git a/src/main/content/jcr_root/apps/groovyconsole/components/console/active-jobs.html b/src/main/content/jcr_root/apps/groovyconsole/components/console/active-jobs.html new file mode 100644 index 00000000..1fbd7d28 --- /dev/null +++ b/src/main/content/jcr_root/apps/groovyconsole/components/console/active-jobs.html @@ -0,0 +1,25 @@ +
+
+

Active Jobs

+
+ + + + + + + + + + + + + + + + + + + +
IDStart TimeTitleDescriptionScript
${job.id}${job.formattedStartTime}${job.title}${job.description}${job.script}
+
\ No newline at end of file diff --git a/src/main/content/jcr_root/apps/groovyconsole/components/console/body.html b/src/main/content/jcr_root/apps/groovyconsole/components/console/body.html index dfedcd3f..1771a6eb 100644 --- a/src/main/content/jcr_root/apps/groovyconsole/components/console/body.html +++ b/src/main/content/jcr_root/apps/groovyconsole/components/console/body.html @@ -44,9 +44,11 @@
Running Time

     
 
+    
+
     
- + diff --git a/src/main/content/jcr_root/apps/groovyconsole/components/console/history.html b/src/main/content/jcr_root/apps/groovyconsole/components/console/history.html index 2f0b88de..28463fcd 100644 --- a/src/main/content/jcr_root/apps/groovyconsole/components/console/history.html +++ b/src/main/content/jcr_root/apps/groovyconsole/components/console/history.html @@ -39,6 +39,7 @@

Date + Job Title Script diff --git a/src/main/content/jcr_root/apps/groovyconsole/components/console/scheduled-jobs.html b/src/main/content/jcr_root/apps/groovyconsole/components/console/scheduled-jobs.html index 1283104b..983092ab 100644 --- a/src/main/content/jcr_root/apps/groovyconsole/components/console/scheduled-jobs.html +++ b/src/main/content/jcr_root/apps/groovyconsole/components/console/scheduled-jobs.html @@ -1,4 +1,4 @@ -
+

Scheduled Jobs diff --git a/src/main/groovy/com/icfolson/aem/groovy/console/GroovyConsoleService.groovy b/src/main/groovy/com/icfolson/aem/groovy/console/GroovyConsoleService.groovy index 479811f9..f2660238 100644 --- a/src/main/groovy/com/icfolson/aem/groovy/console/GroovyConsoleService.groovy +++ b/src/main/groovy/com/icfolson/aem/groovy/console/GroovyConsoleService.groovy @@ -1,5 +1,6 @@ package com.icfolson.aem.groovy.console +import com.icfolson.aem.groovy.console.api.ActiveJob import com.icfolson.aem.groovy.console.api.JobProperties import com.icfolson.aem.groovy.console.api.context.ScriptContext import com.icfolson.aem.groovy.console.api.context.ScriptData @@ -34,4 +35,11 @@ interface GroovyConsoleService { * @return true if job was successfully added */ boolean addScheduledJob(JobProperties jobProperties) + + /** + * Get a list of all active jobs. + * + * @return list of active jobs + */ + List getActiveJobs() } \ No newline at end of file diff --git a/src/main/groovy/com/icfolson/aem/groovy/console/api/ActiveJob.groovy b/src/main/groovy/com/icfolson/aem/groovy/console/api/ActiveJob.groovy new file mode 100644 index 00000000..8997460e --- /dev/null +++ b/src/main/groovy/com/icfolson/aem/groovy/console/api/ActiveJob.groovy @@ -0,0 +1,38 @@ +package com.icfolson.aem.groovy.console.api + +import com.icfolson.aem.groovy.console.constants.GroovyConsoleConstants +import com.icfolson.aem.groovy.console.utils.GroovyScriptUtils +import groovy.transform.Memoized +import groovy.transform.TupleConstructor +import org.apache.sling.event.jobs.Job + +@TupleConstructor +class ActiveJob { + + Job job + + String getFormattedStartTime() { + job.processingStarted.format(GroovyConsoleConstants.DATE_FORMAT_DISPLAY) + } + + String getId() { + job.id + } + + String getTitle() { + jobProperties.jobTitle + } + + String getDescription() { + jobProperties.jobDescription + } + + String getScript() { + GroovyScriptUtils.getScriptPreview(jobProperties.script) + } + + @Memoized + JobProperties getJobProperties() { + JobProperties.fromJob(job) + } +} diff --git a/src/main/groovy/com/icfolson/aem/groovy/console/audit/AuditService.groovy b/src/main/groovy/com/icfolson/aem/groovy/console/audit/AuditService.groovy index 9a8a497c..b9a7db79 100644 --- a/src/main/groovy/com/icfolson/aem/groovy/console/audit/AuditService.groovy +++ b/src/main/groovy/com/icfolson/aem/groovy/console/audit/AuditService.groovy @@ -67,4 +67,13 @@ interface AuditService { * @return list of audit records in the given date range */ List getAuditRecords(String userId, Calendar startDate, Calendar endDate) + + /** + * Get a list of scheduled job audit records for the given date range. + * + * @param startDate start date + * @param endDate end date + * @return list of scheduled job audit records in the given date range + */ + List getScheduledJobAuditRecords(Calendar startDate, Calendar endDate) } diff --git a/src/main/groovy/com/icfolson/aem/groovy/console/audit/impl/DefaultAuditService.groovy b/src/main/groovy/com/icfolson/aem/groovy/console/audit/impl/DefaultAuditService.groovy index 355c756d..3d8f38bf 100644 --- a/src/main/groovy/com/icfolson/aem/groovy/console/audit/impl/DefaultAuditService.groovy +++ b/src/main/groovy/com/icfolson/aem/groovy/console/audit/impl/DefaultAuditService.groovy @@ -164,16 +164,12 @@ class DefaultAuditService implements AuditService { @Override List getAuditRecords(String userId, Calendar startDate, Calendar endDate) { - getAllAuditRecords(userId).findAll { auditRecord -> - def auditRecordDate = auditRecord.date - - auditRecordDate.set(Calendar.HOUR_OF_DAY, 0) - auditRecordDate.set(Calendar.MINUTE, 0) - auditRecordDate.set(Calendar.SECOND, 0) - auditRecordDate.set(Calendar.MILLISECOND, 0) + getAuditRecordsForDateRange(getAllAuditRecords(userId), startDate, endDate) + } - !auditRecordDate.before(startDate) && !auditRecordDate.after(endDate) - } + @Override + List getScheduledJobAuditRecords(Calendar startDate, Calendar endDate) { + getAuditRecordsForDateRange(allScheduledJobAuditRecords, startDate, endDate) } @Activate @@ -280,6 +276,19 @@ class DefaultAuditService implements AuditService { auditRecords } + private List getAuditRecordsForDateRange(List auditRecords, Calendar startDate, Calendar endDate) { + auditRecords.findAll { auditRecord -> + def auditRecordDate = auditRecord.date + + auditRecordDate.set(Calendar.HOUR_OF_DAY, 0) + auditRecordDate.set(Calendar.MINUTE, 0) + auditRecordDate.set(Calendar.SECOND, 0) + auditRecordDate.set(Calendar.MILLISECOND, 0) + + !auditRecordDate.before(startDate) && !auditRecordDate.after(endDate) + } + } + private T withResourceResolver(Closure closure) { resourceResolverFactory.getServiceResourceResolver(null).withCloseable(closure) } diff --git a/src/main/groovy/com/icfolson/aem/groovy/console/components/ActiveJobsPanel.groovy b/src/main/groovy/com/icfolson/aem/groovy/console/components/ActiveJobsPanel.groovy new file mode 100644 index 00000000..ad0d8e00 --- /dev/null +++ b/src/main/groovy/com/icfolson/aem/groovy/console/components/ActiveJobsPanel.groovy @@ -0,0 +1,18 @@ +package com.icfolson.aem.groovy.console.components + +import com.icfolson.aem.groovy.console.GroovyConsoleService +import com.icfolson.aem.groovy.console.api.ActiveJob +import org.apache.sling.api.SlingHttpServletRequest +import org.apache.sling.models.annotations.Model +import org.apache.sling.models.annotations.injectorspecific.OSGiService + +@Model(adaptables = SlingHttpServletRequest) +class ActiveJobsPanel { + + @OSGiService + private GroovyConsoleService groovyConsoleService + + List getActiveJobs() { + groovyConsoleService.activeJobs + } +} diff --git a/src/main/groovy/com/icfolson/aem/groovy/console/components/Body.groovy b/src/main/groovy/com/icfolson/aem/groovy/console/components/Body.groovy index 31397205..e7f35810 100644 --- a/src/main/groovy/com/icfolson/aem/groovy/console/components/Body.groovy +++ b/src/main/groovy/com/icfolson/aem/groovy/console/components/Body.groovy @@ -1,5 +1,6 @@ package com.icfolson.aem.groovy.console.components +import com.icfolson.aem.groovy.console.GroovyConsoleService import com.icfolson.aem.groovy.console.audit.AuditRecord import com.icfolson.aem.groovy.console.audit.AuditService import com.icfolson.aem.groovy.console.configuration.ConfigurationService @@ -7,9 +8,9 @@ import groovy.json.JsonBuilder import org.apache.sling.api.SlingHttpServletRequest import org.apache.sling.models.annotations.Model import org.apache.sling.models.annotations.injectorspecific.OSGiService +import org.apache.sling.models.annotations.injectorspecific.Self import javax.annotation.PostConstruct -import javax.inject.Inject import static com.icfolson.aem.groovy.console.constants.GroovyConsoleConstants.SCRIPT import static com.icfolson.aem.groovy.console.constants.GroovyConsoleConstants.USER_ID @@ -20,12 +21,15 @@ class Body { @OSGiService private AuditService auditService - @Inject - private SlingHttpServletRequest request - @OSGiService private ConfigurationService configurationService + @OSGiService + private GroovyConsoleService groovyConsoleService + + @Self + private SlingHttpServletRequest request + private AuditRecord auditRecord @PostConstruct @@ -46,6 +50,10 @@ class Body { configurationService.hasScheduledJobPermission(request) } + boolean isHasActiveJobs() { + groovyConsoleService.activeJobs + } + boolean isAuditEnabled() { !configurationService.auditDisabled } diff --git a/src/main/groovy/com/icfolson/aem/groovy/console/components/HistoryPanel.groovy b/src/main/groovy/com/icfolson/aem/groovy/console/components/HistoryPanel.groovy index b41e6b9c..581a3065 100644 --- a/src/main/groovy/com/icfolson/aem/groovy/console/components/HistoryPanel.groovy +++ b/src/main/groovy/com/icfolson/aem/groovy/console/components/HistoryPanel.groovy @@ -4,8 +4,7 @@ import com.icfolson.aem.groovy.console.audit.AuditService import org.apache.sling.api.SlingHttpServletRequest import org.apache.sling.models.annotations.Model import org.apache.sling.models.annotations.injectorspecific.OSGiService - -import javax.inject.Inject +import org.apache.sling.models.annotations.injectorspecific.Self @Model(adaptables = SlingHttpServletRequest) class HistoryPanel { @@ -13,7 +12,7 @@ class HistoryPanel { @OSGiService private AuditService auditService - @Inject + @Self private SlingHttpServletRequest request Boolean isHasAuditRecords() { diff --git a/src/main/groovy/com/icfolson/aem/groovy/console/configuration/ConfigurationService.groovy b/src/main/groovy/com/icfolson/aem/groovy/console/configuration/ConfigurationService.groovy index 983e2971..786d4377 100644 --- a/src/main/groovy/com/icfolson/aem/groovy/console/configuration/ConfigurationService.groovy +++ b/src/main/groovy/com/icfolson/aem/groovy/console/configuration/ConfigurationService.groovy @@ -58,4 +58,11 @@ interface ConfigurationService { * @return if true, display all audit records */ boolean isDisplayAllAuditRecords() + + /** + * Get the thread timeout value in seconds. Scripts will be interrupted when the timeout value is reached. If zero, no timeout will be enforced. + * + * @return thread timeout + */ + long getThreadTimeout() } \ No newline at end of file diff --git a/src/main/groovy/com/icfolson/aem/groovy/console/configuration/impl/DefaultConfigurationService.groovy b/src/main/groovy/com/icfolson/aem/groovy/console/configuration/impl/DefaultConfigurationService.groovy index f162a21d..b2a3b621 100755 --- a/src/main/groovy/com/icfolson/aem/groovy/console/configuration/impl/DefaultConfigurationService.groovy +++ b/src/main/groovy/com/icfolson/aem/groovy/console/configuration/impl/DefaultConfigurationService.groovy @@ -39,6 +39,8 @@ class DefaultConfigurationService implements ConfigurationService { private boolean displayAllAuditRecords + private long threadTimeout + @Override boolean hasPermission(SlingHttpServletRequest request) { isAdminOrAllowedGroupMember(request, allowedGroups) @@ -74,6 +76,11 @@ class DefaultConfigurationService implements ConfigurationService { displayAllAuditRecords } + @Override + long getThreadTimeout() { + threadTimeout + } + @Activate @Modified @Synchronized @@ -85,6 +92,7 @@ class DefaultConfigurationService implements ConfigurationService { vanityPathEnabled = properties.vanityPathEnabled() auditDisabled = properties.auditDisabled() displayAllAuditRecords = properties.auditDisplayAll() + threadTimeout = properties.threadTimeout() } private boolean isAdminOrAllowedGroupMember(SlingHttpServletRequest request, Set groupIds) { diff --git a/src/main/groovy/com/icfolson/aem/groovy/console/impl/DefaultGroovyConsoleService.groovy b/src/main/groovy/com/icfolson/aem/groovy/console/impl/DefaultGroovyConsoleService.groovy index 69dbe954..f583e839 100755 --- a/src/main/groovy/com/icfolson/aem/groovy/console/impl/DefaultGroovyConsoleService.groovy +++ b/src/main/groovy/com/icfolson/aem/groovy/console/impl/DefaultGroovyConsoleService.groovy @@ -4,6 +4,7 @@ import com.day.cq.commons.jcr.JcrConstants import com.day.cq.commons.jcr.JcrUtil import com.google.common.net.MediaType import com.icfolson.aem.groovy.console.GroovyConsoleService +import com.icfolson.aem.groovy.console.api.ActiveJob import com.icfolson.aem.groovy.console.api.JobProperties import com.icfolson.aem.groovy.console.api.context.ScriptContext import com.icfolson.aem.groovy.console.api.context.ScriptData @@ -17,11 +18,13 @@ import com.icfolson.aem.groovy.console.response.SaveScriptResponse import com.icfolson.aem.groovy.console.response.impl.DefaultRunScriptResponse import com.icfolson.aem.groovy.console.response.impl.DefaultSaveScriptResponse import groovy.transform.Synchronized +import groovy.transform.TimedInterrupt import groovy.util.logging.Slf4j import org.apache.jackrabbit.util.Text import org.apache.sling.event.jobs.JobManager import org.codehaus.groovy.control.CompilerConfiguration import org.codehaus.groovy.control.MultipleCompilationErrorsException +import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer import org.codehaus.groovy.control.customizers.CompilationCustomizer import org.osgi.service.component.annotations.Component import org.osgi.service.component.annotations.Reference @@ -127,6 +130,13 @@ class DefaultGroovyConsoleService implements GroovyConsoleService { new DefaultSaveScriptResponse(fileName) } + @Override + List getActiveJobs() { + jobManager.findJobs(JobManager.QueryType.ACTIVE, GroovyConsoleConstants.JOB_TOPIC, 0, null).collect { job -> + new ActiveJob(job) + } + } + @Override boolean addScheduledJob(JobProperties jobProperties) { if (jobProperties.cronExpression) { @@ -182,7 +192,14 @@ class DefaultGroovyConsoleService implements GroovyConsoleService { } private CompilerConfiguration getConfiguration() { - new CompilerConfiguration().addCompilationCustomizers(extensionService.compilationCustomizers + def configuration = new CompilerConfiguration() + + if (configurationService.threadTimeout > 0) { + // add timed interrupt using configured timeout value + configuration.addCompilationCustomizers(new ASTTransformationCustomizer(value: configurationService.threadTimeout, TimedInterrupt)) + } + + configuration.addCompilationCustomizers(extensionService.compilationCustomizers as CompilationCustomizer[]) } diff --git a/src/main/groovy/com/icfolson/aem/groovy/console/servlets/AuditServlet.groovy b/src/main/groovy/com/icfolson/aem/groovy/console/servlets/AuditServlet.groovy index b9cb80a1..fc7d9696 100644 --- a/src/main/groovy/com/icfolson/aem/groovy/console/servlets/AuditServlet.groovy +++ b/src/main/groovy/com/icfolson/aem/groovy/console/servlets/AuditServlet.groovy @@ -57,6 +57,7 @@ class AuditServlet extends AbstractJsonResponseServlet { [ date: auditRecord.date.format(GroovyConsoleConstants.DATE_FORMAT_DISPLAY), scriptPreview: GroovyScriptUtils.getScriptPreview(auditRecord.script), + jobTitle: auditRecord.jobProperties.jobTitle, userId: auditRecord.userId, script: auditRecord.script, data: auditRecord.data, @@ -72,16 +73,23 @@ class AuditServlet extends AbstractJsonResponseServlet { def startDateParameter = request.getParameter(GroovyConsoleConstants.START_DATE) def endDateParameter = request.getParameter(GroovyConsoleConstants.END_DATE) - def auditRecords + def auditRecords = [] as List if (!startDateParameter || !endDateParameter) { - auditRecords = auditService.getAllAuditRecords(request.resourceResolver.userID) + if (configurationService.hasScheduledJobPermission(request)) { + auditRecords.addAll(auditService.allScheduledJobAuditRecords) + } + + auditRecords.addAll(auditService.getAllAuditRecords(request.resourceResolver.userID)) } else { - def startDate = Date.parse(DATE_FORMAT, startDateParameter) - def endDate = Date.parse(DATE_FORMAT, endDateParameter) + def startDate = Date.parse(DATE_FORMAT, startDateParameter).toCalendar() + def endDate = Date.parse(DATE_FORMAT, endDateParameter).toCalendar() + + if (configurationService.hasScheduledJobPermission(request)) { + auditRecords.addAll(auditService.getScheduledJobAuditRecords(startDate, endDate)) + } - auditRecords = auditService.getAuditRecords(request.resourceResolver.userID, startDate.toCalendar(), - endDate.toCalendar()) + auditRecords.addAll(auditService.getAuditRecords(request.resourceResolver.userID, startDate, endDate)) } auditRecords.sort { a, b -> b.date.timeInMillis <=> a.date.timeInMillis } diff --git a/src/main/groovy/com/icfolson/aem/groovy/console/servlets/ScheduledJobsServlet.groovy b/src/main/groovy/com/icfolson/aem/groovy/console/servlets/ScheduledJobsServlet.groovy index 3849c857..84c4587c 100755 --- a/src/main/groovy/com/icfolson/aem/groovy/console/servlets/ScheduledJobsServlet.groovy +++ b/src/main/groovy/com/icfolson/aem/groovy/console/servlets/ScheduledJobsServlet.groovy @@ -63,7 +63,7 @@ class ScheduledJobsServlet extends AbstractJsonResponseServlet { new ImmutableMap.Builder() .putAll(scheduledJobInfo.jobProperties) - .put("downloadUrl", auditRecords ? auditRecords.last().downloadUrl : null) + .put("downloadUrl", (auditRecords ? auditRecords.last().downloadUrl : null) ?: "") .put("scriptPreview", GroovyScriptUtils.getScriptPreview(scheduledJobInfo.jobProperties[SCRIPT] as String)) .put("nextExecutionDate", scheduledJobInfo.nextScheduledExecution.format(GroovyConsoleConstants.DATE_FORMAT_DISPLAY)) .build() diff --git a/src/main/java/com/icfolson/aem/groovy/console/configuration/impl/ConfigurationServiceProperties.java b/src/main/java/com/icfolson/aem/groovy/console/configuration/impl/ConfigurationServiceProperties.java index dc299e3a..9486ac55 100644 --- a/src/main/java/com/icfolson/aem/groovy/console/configuration/impl/ConfigurationServiceProperties.java +++ b/src/main/java/com/icfolson/aem/groovy/console/configuration/impl/ConfigurationServiceProperties.java @@ -34,4 +34,8 @@ @AttributeDefinition(name = "Display All Audit Records?", description = "If enabled, all audit records (including records for other users) will be displayed in the console history.") boolean auditDisplayAll() default false; + + @AttributeDefinition(name = "Thread Timeout", + description = "Time in seconds that scripts are allowed to execute before being interrupted. If 0, no timeout is enforced.") + long threadTimeout() default 0; } \ No newline at end of file