diff --git a/nucleus/admin/rest/rest-service/src/main/java/org/glassfish/admin/rest/utils/DetachedCommandHelper.java b/nucleus/admin/rest/rest-service/src/main/java/org/glassfish/admin/rest/utils/DetachedCommandHelper.java index c3c6bb9f4e1..0a619e18e30 100644 --- a/nucleus/admin/rest/rest-service/src/main/java/org/glassfish/admin/rest/utils/DetachedCommandHelper.java +++ b/nucleus/admin/rest/rest-service/src/main/java/org/glassfish/admin/rest/utils/DetachedCommandHelper.java @@ -37,10 +37,10 @@ * only if the new code is made subject to such option by the copyright * holder. */ -// Portions Copyright [2022] [Payara Foundation and/or its affiliates] package org.glassfish.admin.rest.utils; import com.sun.enterprise.admin.remote.AdminCommandStateImpl; +import com.sun.enterprise.v3.admin.JobManagerService; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.logging.Level; @@ -48,6 +48,7 @@ import org.glassfish.api.admin.AdminCommandEventBroker; import org.glassfish.api.admin.AdminCommandState; import org.glassfish.api.admin.CommandRunner; +import org.glassfish.internal.api.Globals; /** * @@ -77,6 +78,8 @@ public static String invokeAsync(CommandRunner.CommandInvocation commandInvocati CountDownLatch latch = new CountDownLatch(1); DetachedCommandHelper helper = new DetachedCommandHelper(commandInvocation, latch); commandInvocation.listener(".*", helper); + JobManagerService jobManagerService = Globals.getDefaultHabitat().getService(JobManagerService.class); + jobManagerService.getThreadPool().execute(helper); try { if (!latch.await(10, TimeUnit.SECONDS)) { RestLogging.restLogger.log(Level.FINE, "latch.await() returned false"); diff --git a/nucleus/admin/rest/rest-service/src/main/java/org/glassfish/admin/rest/utils/SseCommandHelper.java b/nucleus/admin/rest/rest-service/src/main/java/org/glassfish/admin/rest/utils/SseCommandHelper.java index 19739db07a9..1a09ee2db5b 100644 --- a/nucleus/admin/rest/rest-service/src/main/java/org/glassfish/admin/rest/utils/SseCommandHelper.java +++ b/nucleus/admin/rest/rest-service/src/main/java/org/glassfish/admin/rest/utils/SseCommandHelper.java @@ -37,12 +37,13 @@ * only if the new code is made subject to such option by the copyright * holder. */ -// Portions Copyright [2019-2022] [Payara Foundation and/or its affiliates] +// Portions Copyright [2019-2021] [Payara Foundation and/or its affiliates] package org.glassfish.admin.rest.utils; import com.sun.enterprise.admin.remote.AdminCommandStateImpl; import com.sun.enterprise.util.LocalStringManagerImpl; +import com.sun.enterprise.v3.admin.JobManagerService; import com.sun.enterprise.admin.report.PropsFileActionReporter; import java.io.IOException; import java.util.logging.Level; @@ -54,6 +55,7 @@ import org.glassfish.api.admin.AdminCommandState; import org.glassfish.api.admin.CommandRunner; import org.glassfish.api.admin.CommandRunner.CommandInvocation; +import org.glassfish.internal.api.Globals; import org.glassfish.jersey.media.sse.EventOutput; import org.glassfish.jersey.media.sse.OutboundEvent; @@ -176,6 +178,8 @@ public static EventOutput invokeAsync(CommandInvocation commandInvocation, Actio } SseCommandHelper helper = new SseCommandHelper(commandInvocation, processor); commandInvocation.listener(".*", helper); + JobManagerService jobManagerService = Globals.getDefaultHabitat().getService(JobManagerService.class); + jobManagerService.getThreadPool().execute(helper); return helper.eventOuptut; } } diff --git a/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/AdminCommandInstanceImpl.java b/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/AdminCommandInstanceImpl.java index 00389eb5a51..30de7696801 100644 --- a/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/AdminCommandInstanceImpl.java +++ b/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/AdminCommandInstanceImpl.java @@ -37,7 +37,6 @@ * only if the new code is made subject to such option by the copyright * holder. */ -// Portions Copyright [2022] [Payara Foundation and/or its affiliates] package com.sun.enterprise.v3.admin; import com.sun.enterprise.admin.event.AdminCommandEventBrokerImpl; @@ -48,6 +47,9 @@ import org.glassfish.api.admin.Job; import org.glassfish.api.admin.CommandProgress; import org.glassfish.api.admin.Payload; +import org.glassfish.api.admin.progress.JobInfo; +import org.glassfish.api.admin.progress.JobPersistence; +import org.glassfish.internal.api.Globals; import org.glassfish.security.services.common.SubjectUtil; import javax.security.auth.Subject; @@ -169,7 +171,41 @@ public void complete(ActionReport report, Payload.Outbound outbound) { super.actionReport = report; this.payload = outbound; this.completionDate = System.currentTimeMillis(); - setState(State.COMPLETED); + if (isManagedJob) { + if (getState().equals(State.RUNNING_RETRYABLE) && failToRetryable) { + JobManagerService jobManager = Globals.getDefaultHabitat().getService(JobManagerService.class); + jobManager.getRetryableJobsInfo().put(id, CheckpointHelper.CheckpointFilename.createBasic(this)); + jobManager.purgeJob(id); + setState(State.FAILED_RETRYABLE); + } else { + JobPersistence jobPersistenceService; + if (scope != null) { + jobPersistenceService = Globals.getDefaultHabitat().getService(JobPersistence.class,scope+"job-persistence"); + } else { + jobPersistenceService = Globals.getDefaultHabitat().getService(JobPersistenceService.class); + } + State finalState = State.COMPLETED; + if (getState().equals(State.REVERTING)) { + finalState = State.REVERTED; + } + String user = null; + if(subjectUsernames.size() > 0){ + user = subjectUsernames.get(0); + } + jobPersistenceService.persist(new JobInfo(id,commandName,executionDate,report.getActionExitCode().name(),user,report.getMessage(),getJobsFile(),finalState.name(),completionDate)); + if (getState().equals(State.RUNNING_RETRYABLE) || getState().equals(State.REVERTING)) { + JobManagerService jobManager = Globals.getDefaultHabitat().getService(JobManagerService.class); + File jobFile = getJobsFile(); + if (jobFile == null) { + jobFile = jobManager.getJobsFile(); + } + jobManager.deleteCheckpoint(jobFile.getParentFile(), getId()); + } + setState(finalState); + } + } else { + setState(State.COMPLETED); + } } @Override diff --git a/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/CommandRunnerImpl.java b/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/CommandRunnerImpl.java index e021b722e67..155e9bf8dc7 100644 --- a/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/CommandRunnerImpl.java +++ b/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/CommandRunnerImpl.java @@ -37,7 +37,7 @@ * only if the new code is made subject to such option by the copyright * holder. */ -// Portions Copyright [2017-2022] [Payara Foundation and/or its affiliates] +// Portions Copyright [2017-2021] [Payara Foundation and/or its affiliates] package com.sun.enterprise.v3.admin; @@ -1805,6 +1805,8 @@ private void executeFromCheckpoint(JobManager.Checkpoint checkpoint, boolean rev } ((AdminCommandInstanceImpl) job).setEventBroker(eventBroker); ((AdminCommandInstanceImpl) job).setState(revert ? AdminCommandState.State.REVERTING : AdminCommandState.State.RUNNING_RETRYABLE); + JobManager jobManager = habitat.getService(JobManagerService.class); + jobManager.registerJob(job); //command AdminCommand command = checkpoint.getCommand(); if (command == null) { @@ -1849,8 +1851,10 @@ public Subject run() { isManagedJob = AnnotationUtil.presentTransitive(ManagedJob.class, command.getClass()); } JobCreator jobCreator = null; + JobManager jobManager = null; jobCreator = habitat.getService(JobCreator.class,scope+"job-creator"); + jobManager = habitat.getService(JobManagerService.class); if (jobCreator == null ) { jobCreator = habitat.getService(JobCreatorService.class); @@ -1858,14 +1862,20 @@ public Subject run() { } Job job = null; - job = jobCreator.createJob(null, scope(), name(), subject, isManagedJob, parameters()); - + if (isManagedJob) { + job = jobCreator.createJob(jobManager.getNewId(), scope(), name(), subject, isManagedJob, parameters()); + } else { + job = jobCreator.createJob(null, scope(), name(), subject, isManagedJob, parameters()); + } //Register the brokers else the detach functionality will not work for (NameListerPair nameListerPair : nameListerPairs) { job.getEventBroker().registerListener(nameListerPair.nameRegexp, nameListerPair.listener); } + if (isManagedJob) { + jobManager.registerJob(job); + } CommandRunnerImpl.this.doCommand(this, command, subject, job); job.complete(report(), outboundPayload()); if (progressStatusChild != null) { diff --git a/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/JobAuthorizationAttributeProcessor.java b/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/JobAuthorizationAttributeProcessor.java new file mode 100644 index 00000000000..4f4e593ae50 --- /dev/null +++ b/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/JobAuthorizationAttributeProcessor.java @@ -0,0 +1,111 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright (c) 2012-2013 Oracle and/or its affiliates. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common Development + * and Distribution License("CDDL") (collectively, the "License"). You + * may not use this file except in compliance with the License. You can + * obtain a copy of the License at + * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html + * or packager/legal/LICENSE.txt. See the License for the specific + * language governing permissions and limitations under the License. + * + * When distributing the software, include this License Header Notice in each + * file and include the License file at packager/legal/LICENSE.txt. + * + * GPL Classpath Exception: + * Oracle designates this particular file as subject to the "Classpath" + * exception as provided by Oracle in the GPL Version 2 section of the License + * file that accompanied this code. + * + * Modifications: + * If applicable, add the following below the License Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyright [year] [name of copyright owner]" + * + * Contributor(s): + * If you wish your version of this file to be governed by only the CDDL or + * only the GPL Version 2, indicate your decision by adding "[Contributor] + * elects to include this software in this distribution under the [CDDL or GPL + * Version 2] license." If you don't indicate a single choice of license, a + * recipient has the option to distribute your version of this file under + * either the CDDL, the GPL Version 2 or to extend the choice of license to + * its licensees as provided above. However, if you add GPL Version 2 code + * and therefore, elected the GPL Version 2 license, then the option applies + * only if the new code is made subject to such option by the copyright + * holder. + */ +package com.sun.enterprise.v3.admin; + +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import javax.security.auth.Subject; +import org.glassfish.api.admin.AdminCommand; +import org.glassfish.api.admin.AuthorizationPreprocessor; +import org.glassfish.api.admin.Job; +import org.glassfish.api.admin.progress.JobInfo; +import org.jvnet.hk2.annotations.Service; + +/** + * Attaches a user attribute to job resources for authorization. + * + * @author Tim Quinn + * @author Bhakti Mehta + */ +@Service +@Singleton +public class JobAuthorizationAttributeProcessor implements AuthorizationPreprocessor { + + private final static String USER_ATTRIBUTE_NAME = "user"; + + public final static String JOB_RESOURCE_NAME_PREFIX_NO_SLASH = "jobs/job"; + public final static String JOB_RESOURCE_NAME_PREFIX = JOB_RESOURCE_NAME_PREFIX_NO_SLASH + '/'; + + public final static Pattern JOB_PATTERN = Pattern.compile("(?:" + JOB_RESOURCE_NAME_PREFIX_NO_SLASH + "(?:/(\\d*))?)"); + + @Inject + private JobManagerService jobManager; + + @Override + public void describeAuthorization(Subject subject, String resourceName, String action, AdminCommand command, Map context, Map subjectAttributes, Map resourceAttributes, Map actionAttributes) { + final Matcher m = JOB_PATTERN.matcher(resourceName); + if ( ! m.matches()) { + return; + } + if (m.groupCount() == 0) { + /* + * The resource name pattern did not match for including a job ID, + * so we will not be able to attach a user attribute to the resource. + */ + return; + } + final String jobID = m.group(1); + final Job job = jobManager.get(jobID); + String userID = null; + + /* + * This logic might run before any validation in the command has run, + * in which case the job ID would be invalid and the job manager and/or + * the completed jobs store might not know about the job. + */ + if (job != null && job.getSubjectUsernames().size() > 0) { + userID = job.getSubjectUsernames().get(0); + } else { + if (jobManager.getCompletedJobs(jobManager.getJobsFile()) != null) { + final JobInfo jobInfo = (JobInfo) jobManager.getCompletedJobForId(jobID); + if (jobInfo != null) { + userID = jobInfo.user; + } + } + } + + if (userID != null) { + resourceAttributes.put(USER_ATTRIBUTE_NAME, userID); + } + } +} diff --git a/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/JobCleanUpService.java b/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/JobCleanUpService.java new file mode 100644 index 00000000000..197207325f2 --- /dev/null +++ b/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/JobCleanUpService.java @@ -0,0 +1,247 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright (c) 2013 Oracle and/or its affiliates. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common Development + * and Distribution License("CDDL") (collectively, the "License"). You + * may not use this file except in compliance with the License. You can + * obtain a copy of the License at + * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html + * or packager/legal/LICENSE.txt. See the License for the specific + * language governing permissions and limitations under the License. + * + * When distributing the software, include this License Header Notice in each + * file and include the License file at packager/legal/LICENSE.txt. + * + * GPL Classpath Exception: + * Oracle designates this particular file as subject to the "Classpath" + * exception as provided by Oracle in the GPL Version 2 section of the License + * file that accompanied this code. + * + * Modifications: + * If applicable, add the following below the License Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyright [year] [name of copyright owner]" + * + * Contributor(s): + * If you wish your version of this file to be governed by only the CDDL or + * only the GPL Version 2, indicate your decision by adding "[Contributor] + * elects to include this software in this distribution under the [CDDL or GPL + * Version 2] license." If you don't indicate a single choice of license, a + * recipient has the option to distribute your version of this file under + * either the CDDL, the GPL Version 2 or to extend the choice of license to + * its licensees as provided above. However, if you add GPL Version 2 code + * and therefore, elected the GPL Version 2 license, then the option applies + * only if the new code is made subject to such option by the copyright + * holder. + */ +// Portions Copyright [2020-2021] Payara Foundation and/or affiliates +package com.sun.enterprise.v3.admin; + +import com.sun.appserv.server.util.Version; +import com.sun.enterprise.config.serverbeans.Domain; +import com.sun.enterprise.config.serverbeans.ManagedJobConfig; +import com.sun.enterprise.util.LocalStringManagerImpl; +import org.glassfish.api.StartupRunLevel; +import org.glassfish.api.admin.progress.JobInfo; +import org.glassfish.hk2.api.PostConstruct; +import org.glassfish.hk2.runlevel.RunLevel; +import org.glassfish.kernel.KernelLoggerInfo; +import org.jvnet.hk2.annotations.Service; +import org.jvnet.hk2.config.Changed; +import org.jvnet.hk2.config.ConfigBeanProxy; +import org.jvnet.hk2.config.ConfigListener; +import org.jvnet.hk2.config.ConfigSupport; +import org.jvnet.hk2.config.NotProcessed; +import org.jvnet.hk2.config.ObservableBean; +import org.jvnet.hk2.config.UnprocessedChangeEvents; + +import jakarta.inject.Inject; +import java.beans.PropertyChangeEvent; +import java.io.File; +import java.util.*; +import java.util.concurrent.*; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.glassfish.api.admin.ProcessEnvironment; + +/** + * + * This is an hk2 service which will clear all expired and inactive jobs + * @author Bhakti Mehta + */ +@Service(name="job-cleanup") +@RunLevel(value= StartupRunLevel.VAL) +public class JobCleanUpService implements PostConstruct,ConfigListener { + + @Inject + JobManagerService jobManagerService; + + @Inject + Domain domain; + + @Inject + private ProcessEnvironment processEnv; + + private ManagedJobConfig managedJobConfig; + + private final static Logger logger = KernelLoggerInfo.getLogger(); + + private ScheduledExecutorService scheduler; + + + private static final LocalStringManagerImpl adminStrings = + new LocalStringManagerImpl(JobCleanUpService.class); + + private boolean micro; + + @Override + public void postConstruct() { + micro = Version.getFullVersion().contains("Micro"); + if (micro) { + //if Micro we don't have any jobs to cleanup + return; + } + + logger.log(Level.FINE,KernelLoggerInfo.initializingJobCleanup); + + scheduler = Executors.newScheduledThreadPool(10, new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread result = new Thread(r); + result.setDaemon(true); + result.setName("Job Cleanup Service"); + return result; + } + }); + + managedJobConfig = domain.getExtensionByType(ManagedJobConfig.class); + ObservableBean bean = (ObservableBean) ConfigSupport.getImpl(managedJobConfig); + logger.fine(KernelLoggerInfo.initializingManagedConfigBean); + bean.addListener(this); + + + + scheduleCleanUp(); + } + + + /** + * This will schedule a cleanup of expired jobs based on configurable values + */ + private void scheduleCleanUp() { + if (micro) { + return; + } + + if (managedJobConfig == null) { + managedJobConfig = domain.getExtensionByType(ManagedJobConfig.class); + ObservableBean bean = (ObservableBean) ConfigSupport.getImpl(managedJobConfig); + logger.fine(KernelLoggerInfo.initializingManagedConfigBean); + bean.addListener(this); + } + + logger.fine(KernelLoggerInfo.schedulingCleanup); + //default values to 20 minutes for delayBetweenRuns and initialDelay + long delayBetweenRuns = 1200000; + long initialDelay = 1200000; + + delayBetweenRuns = jobManagerService.convert(managedJobConfig.getPollInterval()); + initialDelay = jobManagerService.convert(managedJobConfig.getInitialDelay()); + + + ScheduledFuture cleanupFuture = scheduler.scheduleAtFixedRate(new JobCleanUpTask(),initialDelay,delayBetweenRuns,TimeUnit.MILLISECONDS); + + } + + /** + * This method is notified for any changes in job-inactivity-limit or + * job-retention-period or persist, initial-delay or poll-interval option in + * ManagedJobConfig. Any change results + * in the job cleanup service to change the behaviour + * being updated. + * @param events the configuration change events. + * @return the unprocessed change events. + */ + @Override + public UnprocessedChangeEvents changed(PropertyChangeEvent[] events) { + return ConfigSupport.sortAndDispatch(events, new PropertyChangeHandler(), logger); + } + + + private final class JobCleanUpTask implements Runnable { + public void run() { + try { + //This can have data when server starts up initially or as jobs complete + ConcurrentHashMap completedJobsMap = jobManagerService.getCompletedJobsInfo(); + + Iterator completedJobs = new HashSet(completedJobsMap.values()).iterator(); + while (completedJobs.hasNext() ) { + CompletedJob completedJob = completedJobs.next(); + + logger.log(Level.FINE,KernelLoggerInfo.cleaningJob, new Object[]{completedJob.getId()}); + + cleanUpExpiredJobs(completedJob.getJobsFile()); + } + } catch (Exception e ) { + throw new RuntimeException(KernelLoggerInfo.exceptionCleaningJobs,e); + } + + } + + + } + + /** + * This will periodically purge expired jobs + */ + private void cleanUpExpiredJobs(File file) { + ArrayList expiredJobs = jobManagerService.getExpiredJobs(file); + if (expiredJobs.size() > 0 ) { + for (JobInfo job: expiredJobs) { + //remove from Job registy + jobManagerService.purgeJob(job.jobId); + //remove from jobs.xml file + jobManagerService.purgeCompletedJobForId(job.jobId,file); + //remove from local cache for completed jobs + jobManagerService.removeFromCompletedJobs(job.jobId); + if(logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE,KernelLoggerInfo.cleaningJob, job.jobId); + } + } + } + + } + + + class PropertyChangeHandler implements Changed { + + @Override + public NotProcessed changed(TYPE type, Class changedType, T changedInstance) { + NotProcessed np = null; + switch (type) { + case CHANGE: + if(logger.isLoggable(Level.FINE)) { + + logger.log(Level.FINE, KernelLoggerInfo.changeManagedJobConfig, new Object[]{ + changedType.getName() + ,changedInstance.toString()}); + } + np = handleChangeEvent(changedInstance); + break; + default: + } + return np; + } + + private NotProcessed handleChangeEvent(T instance) { + scheduleCleanUp(); + return null; + } + } +} + + + diff --git a/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/JobCreatorService.java b/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/JobCreatorService.java index dacca4f7f85..a13d85ac0fd 100644 --- a/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/JobCreatorService.java +++ b/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/JobCreatorService.java @@ -37,7 +37,6 @@ * only if the new code is made subject to such option by the copyright * holder. */ -// Portions Copyright [2022] [Payara Foundation and/or its affiliates] package com.sun.enterprise.v3.admin; import org.glassfish.api.admin.Job; @@ -61,6 +60,8 @@ public class JobCreatorService implements JobCreator { @Inject private ServerEnvironment serverEnvironment; + @Inject JobManagerService jobManagerService; + private static final String JOBS_FILE = "jobs.xml"; /** * This will create a new job with the name of command and a new unused id for the job @@ -73,7 +74,12 @@ public class JobCreatorService implements JobCreator { @Override public Job createJob(String id, String scope, String name, Subject subject, boolean isManagedJob, ParameterMap parameters) { AdminCommandInstanceImpl job = null; - job = new AdminCommandInstanceImpl(name, scope, subject, false, parameters); + if (isManagedJob) { + job = new AdminCommandInstanceImpl(id, name, scope, subject, true, parameters); + job.setJobsFile(jobManagerService.jobsFile); + } else { + job = new AdminCommandInstanceImpl(name, scope, subject, false, parameters); + } return job; } diff --git a/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/JobManagerService.java b/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/JobManagerService.java new file mode 100644 index 00000000000..ac3a5e2a089 --- /dev/null +++ b/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/JobManagerService.java @@ -0,0 +1,570 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright (c) 2013 Oracle and/or its affiliates. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common Development + * and Distribution License("CDDL") (collectively, the "License"). You + * may not use this file except in compliance with the License. You can + * obtain a copy of the License at + * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html + * or packager/legal/LICENSE.txt. See the License for the specific + * language governing permissions and limitations under the License. + * + * When distributing the software, include this License Header Notice in each + * file and include the License file at packager/legal/LICENSE.txt. + * + * GPL Classpath Exception: + * Oracle designates this particular file as subject to the "Classpath" + * exception as provided by Oracle in the GPL Version 2 section of the License + * file that accompanied this code. + * + * Modifications: + * If applicable, add the following below the License Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyright [year] [name of copyright owner]" + * + * Contributor(s): + * If you wish your version of this file to be governed by only the CDDL or + * only the GPL Version 2, indicate your decision by adding "[Contributor] + * elects to include this software in this distribution under the [CDDL or GPL + * Version 2] license." If you don't indicate a single choice of license, a + * recipient has the option to distribute your version of this file under + * either the CDDL, the GPL Version 2 or to extend the choice of license to + * its licensees as provided above. However, if you add GPL Version 2 code + * and therefore, elected the GPL Version 2 license, then the option applies + * only if the new code is made subject to such option by the copyright + * holder. + */ +package com.sun.enterprise.v3.admin; + +import com.sun.enterprise.admin.event.AdminCommandEventBrokerImpl; +import com.sun.enterprise.admin.remote.RestPayloadImpl; +import com.sun.enterprise.config.serverbeans.Domain; +import com.sun.enterprise.config.serverbeans.ManagedJobConfig; +import com.sun.enterprise.util.LocalStringManagerImpl; +import com.sun.enterprise.util.StringUtils; +import com.sun.enterprise.util.SystemPropertyConstants; +import com.sun.enterprise.v3.admin.CheckpointHelper.CheckpointFilename; +import com.sun.enterprise.v3.server.ExecutorServiceFactory; +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.Serializable; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.xml.bind.JAXBContext; +import jakarta.xml.bind.JAXBException; +import jakarta.xml.bind.Unmarshaller; + +import org.glassfish.api.admin.*; +import org.glassfish.api.admin.progress.JobInfo; +import org.glassfish.api.admin.progress.JobInfos; +import org.glassfish.api.event.EventListener; +import org.glassfish.api.event.EventTypes; +import org.glassfish.api.event.Events; +import org.glassfish.api.event.RestrictTo; +import org.glassfish.hk2.api.PostConstruct; +import org.glassfish.hk2.api.ServiceLocator; +import org.glassfish.kernel.KernelLoggerInfo; +import org.jvnet.hk2.annotations.Service; +import jakarta.xml.bind.Marshaller; +import org.glassfish.api.ActionReport; +import org.glassfish.api.admin.AdminCommandState.State; + +/** + * This is the implementation for the JobManagerService + * The JobManager is responsible + * 1. generating unique ids for jobs + * 2. serving as a registry for jobs + * 3. creating threadpools for jobs + * 4.removing expired jobs + * + * @author Martin Mares + * @author Bhakti Mehta + */ + +@Service(name="job-manager") +@Singleton +public class JobManagerService implements JobManager, PostConstruct, EventListener { + + private static final String CHECKPOINT_MAINDATA = "MAINCMD"; + + @Inject + private Domain domain; + + private ManagedJobConfig managedJobConfig; + + private static final int MAX_SIZE = 65535; + + private final ConcurrentHashMap jobRegistry = new ConcurrentHashMap(); + + private final AtomicInteger lastId = new AtomicInteger(0); + + protected static final LocalStringManagerImpl adminStrings = + new LocalStringManagerImpl(JobManagerService.class); + + private final static Logger logger = KernelLoggerInfo.getLogger(); + + private ExecutorService pool; + + @Inject + private ExecutorServiceFactory executorFactory; + + @Inject + private ServerEnvironment serverEnvironment; + + private final String JOBS_FILE = "jobs.xml"; + + protected JAXBContext jaxbContext; + + protected File jobsFile; + + @Inject + private ServiceLocator serviceLocator; + + @Inject + private JobFileScanner jobFileScanner; + + @Inject + Events events; + + @Inject + CheckpointHelper checkpointHelper; + + @Inject + CommandRunnerImpl commandRunner; + + // This will store the data related to completed jobs so that unique ids + // can be generated for new jobs. This is populated lazily the first + // time the JobManagerService is created, it will scan the + //jobs.xml and load the information in memory + private final ConcurrentHashMap completedJobsInfo = new ConcurrentHashMap(); + private final ConcurrentHashMap retryableJobsInfo = new ConcurrentHashMap(); + + /** + * This will return a new id which is unused + * @return + */ + @Override + public synchronized String getNewId() { + + int nextId = lastId.incrementAndGet(); + if (nextId > MAX_SIZE) { + reset(); + } + String nextIdToUse = String.valueOf(nextId); + return !idInUse(nextIdToUse) ? String.valueOf(nextId): getNewId(); + } + + public JobInfo getCompletedJobForId(String id, File file) { + for (JobInfo jobInfo: getCompletedJobs(file).getJobInfoList()) { + if (jobInfo.jobId.equals(id)) { + return jobInfo; + } + + } + return null; + } + + @Override + public JobInfo getCompletedJobForId(String id) { + return getCompletedJobForId(id,getJobsFile()); + } + + + /** + * This resets the id to 0 + */ + private void reset() { + lastId.set(0); + } + + /** + * This method will return if the id is in use + * @param id + * @return true if id is in use + */ + private boolean idInUse(String id) { + return jobRegistry.containsKey(id) + || completedJobsInfo.containsKey(id) + || retryableJobsInfo.containsKey(id); + } + + + + /** + * This adds the jobs + * @param job + * @throws IllegalArgumentException + */ + @Override + public synchronized void registerJob(Job job) throws IllegalArgumentException { + if (job == null) { + throw new IllegalArgumentException(adminStrings.getLocalString("job.cannot.be.null","Job cannot be null")); + } + if (jobRegistry.containsKey(job.getId())) { + throw new IllegalArgumentException(adminStrings.getLocalString("job.id.in.use","Job id is already in use.")); + } + + retryableJobsInfo.remove(job.getId()); + jobRegistry.put(job.getId(), job); + + if (job.getState() == State.PREPARED && (job instanceof AdminCommandInstanceImpl)) { + ((AdminCommandInstanceImpl) job).setState(AdminCommandState.State.RUNNING); + } + } + + /** + * This returns all the jobs in the registry + * @return The iterator of jobs + */ + @Override + public Iterator getJobs() { + return jobRegistry.values().iterator(); + } + + /** + * This will return a job associated with the id + * @param id The job whose id matches + * @return + */ + @Override + public Job get(String id) { + return jobRegistry.get(id); + } + + /** + * This will return a list of jobs which have crossed the JOBS_RETENTION_PERIOD + * and need to be purged + * @return list of jobs to be purged + */ + public ArrayList getExpiredJobs(File file) { + ArrayList expiredJobs = new ArrayList(); + synchronized (file) { + JobInfos jobInfos = getCompletedJobs(file); + for(JobInfo job:jobInfos.getJobInfoList()) { + + long executedTime = job.commandExecutionDate; + long currentTime = System.currentTimeMillis(); + + long jobsRetentionPeriod = 86400000; + + + managedJobConfig = domain.getExtensionByType(ManagedJobConfig.class); + jobsRetentionPeriod = convert(managedJobConfig.getJobRetentionPeriod()); + + if (currentTime - executedTime > jobsRetentionPeriod && + (job.state.equals(AdminCommandState.State.COMPLETED.name()) || + job.state.equals(AdminCommandState.State.REVERTED.name()))) { + expiredJobs.add(job); + } + } + } + return expiredJobs; + } + + public long convert(String input ) { + String period = input.substring(0,input.length()-1); + Long timeInterval = new Long(period); + String s = input.toLowerCase(Locale.US); + long milliseconds = 86400000; + if (s.indexOf('s') > 0 ) { + milliseconds = timeInterval*1000; + } + else if (s.indexOf('h') > 0 ) { + milliseconds = timeInterval*3600*1000; + + } + else if (s.indexOf('m') > 0 ) { + milliseconds = timeInterval*60*1000; + } + return milliseconds; + } + + + /** + * This will remove the job from the registry + * @param id The job id of the job to be removed + */ + @Override + public synchronized void purgeJob(final String id) { + Job job = jobRegistry.remove(id); + logger.fine(adminStrings.getLocalString("removed.expired.job","Removed expired job ", job)); + } + + public void deleteCheckpoint(final File parentDir, final String jobId) { + //list all related files + File[] toDelete = parentDir.listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.startsWith(jobId + ".") || name.startsWith(jobId + "-"); + } + }); + for (File td : toDelete) { + td.delete(); + } + } + + public ExecutorService getThreadPool() { + return pool ; + } + + + /** + * This will load the jobs which have already completed + * and persisted in the jobs.xml + * @return JobsInfos which contains information about completed jobs + */ + @Override + public JobInfos getCompletedJobs(File jobsFile) { + synchronized (jobsFile) { + try { + if (jaxbContext == null) + jaxbContext = JAXBContext.newInstance(JobInfos.class); + Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); + + if (jobsFile != null && jobsFile.exists()) { + JobInfos jobInfos = (JobInfos)unmarshaller.unmarshal(jobsFile); + return jobInfos; + } + } catch (JAXBException e) { + throw new RuntimeException(adminStrings.getLocalString("error.reading.completed.jobs","Error reading completed jobs ", e.getLocalizedMessage()), e); + } + return null; + } + } + + /** + * This method looks for the completed jobs + * and purges a job which is marked with the jobId + * @param jobId the job to purge + * @return the new list of completed jobs + */ + + public JobInfos purgeCompletedJobForId(String jobId, File file) { + JobInfos completedJobInfos = getCompletedJobs(file); + synchronized (file) { + CopyOnWriteArrayList jobList = new CopyOnWriteArrayList(); + + if (completedJobInfos != null) { + jobList.addAll(completedJobInfos.getJobInfoList()); + + for (JobInfo jobInfo: jobList ) { + if (jobInfo.jobId.equals(jobId)) { + jobList.remove(jobInfo); + } + + } + } + + JobInfos jobInfos = new JobInfos(); + // if (jobList.size() > 0) { + try { + if (jaxbContext == null) + jaxbContext = JAXBContext.newInstance(JobInfos.class); + + jobInfos.setJobInfoList(jobList); + Marshaller jaxbMarshaller = jaxbContext.createMarshaller(); + jaxbMarshaller.marshal(jobInfos, file); + } catch (JAXBException e) { + throw new RuntimeException(adminStrings.getLocalString("error.purging.completed.job","Error purging completed job ", jobId,e.getLocalizedMessage()), e); + } + //} + return jobInfos; + } + + } + + @Override + public JobInfos purgeCompletedJobForId(String id) { + return purgeCompletedJobForId(id, getJobsFile()) ; + } + + + @Override + public void postConstruct() { + jobsFile = + new File(serverEnvironment.getConfigDirPath(),JOBS_FILE); + + pool = executorFactory.provide(); + + HashSet persistedJobFiles = jobFileScanner.getJobFiles(); + persistedJobFiles.add(jobsFile); + + // Check if there are jobs.xml files which have completed jobs so that + // unique ids get generated + for (File jobfile : persistedJobFiles) { + if (jobfile != null) { + reapCompletedJobs(jobfile); + boolean dropInterruptedCommands = Boolean.valueOf(System.getProperty(SystemPropertyConstants.DROP_INTERRUPTED_COMMANDS)); + Collection listed = checkpointHelper.listCheckpoints(jobfile.getParentFile()); + for (CheckpointFilename cf : listed) { + if (dropInterruptedCommands) { + logger.info("Dropping checkpoint: " + cf.getFile()); + deleteCheckpoint(cf.getParentDir(), cf.getJobId()); + } else { + this.retryableJobsInfo.put(cf.getJobId(), cf); + } + } + } + } + events.register(this); + } + + @Override + public File getJobsFile() { + return jobsFile; + } + + public void addToCompletedJobs(CompletedJob job) { + completedJobsInfo.put(job.getId(),job); + + } + + public void removeFromCompletedJobs(String id) { + completedJobsInfo.remove(id); + } + + public ConcurrentHashMap getCompletedJobsInfo() { + return completedJobsInfo; + } + + public ConcurrentHashMap getRetryableJobsInfo() { + return retryableJobsInfo; + } + + @Override + public void checkpoint(AdminCommandContext context, Serializable data) throws IOException { + checkpoint((AdminCommand) null, context); + if (data != null) { + checkpointAttachement(context.getJobId(), CHECKPOINT_MAINDATA, data); + } + } + + @Override + public void checkpoint(AdminCommand command, AdminCommandContext context) throws IOException { + if (!StringUtils.ok(context.getJobId())) { + throw new IllegalArgumentException("Command is not managed"); + } + Job job = get(context.getJobId()); + if (job.getJobsFile() == null) { + job.setJobsFile(getJobsFile()); + } + Checkpoint chkp = new Checkpoint(job, command, context); + checkpointHelper.save(chkp); + if (job instanceof AdminCommandInstanceImpl) { + ((AdminCommandInstanceImpl) job).setState(AdminCommandState.State.RUNNING_RETRYABLE); + } + } + + public void checkpointAttachement(String jobId, String attachId, Serializable data) throws IOException { + Job job = get(jobId); + if (job.getJobsFile() == null) { + job.setJobsFile(getJobsFile()); + } + checkpointHelper.saveAttachment(data, job, attachId); + } + + public T loadCheckpointAttachement(String jobId, String attachId) throws IOException, ClassNotFoundException { + Job job = get(jobId); + if (job.getJobsFile() == null) { + job.setJobsFile(getJobsFile()); + } + return checkpointHelper.loadAttachment(job, attachId); + } + + @Override + public Serializable loadCheckpointData(String jobId) throws IOException, ClassNotFoundException { + return loadCheckpointAttachement(jobId, CHECKPOINT_MAINDATA); + } + + public Checkpoint loadCheckpoint(String jobId, Payload.Outbound outbound) throws IOException, ClassNotFoundException { + Job job = get(jobId); + CheckpointFilename cf = null; + if (job == null) { + cf = getRetryableJobsInfo().get(jobId); + if (cf == null) { + cf = CheckpointFilename.createBasic(jobId, getJobsFile()); + } + } else { + cf = CheckpointFilename.createBasic(job); + } + return loadCheckpoint(cf, outbound); + } + + private Checkpoint loadCheckpoint(CheckpointFilename cf, Payload.Outbound outbound) throws IOException, ClassNotFoundException { + Checkpoint result = checkpointHelper.load(cf, outbound); + if (result != null) { + serviceLocator.inject(result.getJob()); + serviceLocator.postConstruct(result.getJob()); + if (result.getCommand() != null) { + serviceLocator.inject(result.getCommand()); + serviceLocator.postConstruct(result.getCommand()); + } + } + return result; + } + + /* This method will look for completed jobs from the jobs.xml + * files and load the information in a local datastructure for + * faster access + */ + protected void reapCompletedJobs(File file) { + if (file != null && file.exists()) { + JobInfos jobInfos = getCompletedJobs(file); + if (jobInfos != null) { + for (JobInfo jobInfo: jobInfos.getJobInfoList()) { + addToCompletedJobs(new CompletedJob(jobInfo.jobId,jobInfo.commandCompletionDate,jobInfo.getJobsFile())); + } + } + } + } + + @Override + public void event(@RestrictTo(EventTypes.SERVER_READY_NAME) Event event) { + if (event.is(EventTypes.SERVER_READY)) { + if (retryableJobsInfo.size() > 0) { + Runnable runnable = new Runnable() { + @Override + public void run() { + logger.fine("Restarting retryable jobs"); + for (CheckpointFilename cf : retryableJobsInfo.values()) { + reexecuteJobFromCheckpoint(cf); + } + } + }; + (new Thread(runnable)).start(); + } else { + logger.fine("No retryable job found"); + } + } + } + + private void reexecuteJobFromCheckpoint(CheckpointFilename cf) { + Checkpoint checkpoint = null; + try { + RestPayloadImpl.Outbound outbound = new RestPayloadImpl.Outbound(true); + checkpoint = loadCheckpoint(cf, outbound); + } catch (Exception ex) { + logger.log(Level.WARNING, KernelLoggerInfo.exceptionLoadCheckpoint, ex); + } + if (checkpoint != null) { + logger.log(Level.INFO, KernelLoggerInfo.checkpointAutoResumeStart, + new Object[]{checkpoint.getJob().getName()}); + commandRunner.executeFromCheckpoint(checkpoint, false, new AdminCommandEventBrokerImpl()); + ActionReport report = checkpoint.getContext().getActionReport(); + logger.log(Level.INFO, KernelLoggerInfo.checkpointAutoResumeDone, + new Object[]{checkpoint.getJob().getName(), report.getActionExitCode(), report.getTopMessagePart()}); + } + } + +} diff --git a/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/JobPersistenceService.java b/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/JobPersistenceService.java new file mode 100644 index 00000000000..5830b4fd736 --- /dev/null +++ b/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/JobPersistenceService.java @@ -0,0 +1,121 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright (c) 2013 Oracle and/or its affiliates. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common Development + * and Distribution License("CDDL") (collectively, the "License"). You + * may not use this file except in compliance with the License. You can + * obtain a copy of the License at + * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html + * or packager/legal/LICENSE.txt. See the License for the specific + * language governing permissions and limitations under the License. + * + * When distributing the software, include this License Header Notice in each + * file and include the License file at packager/legal/LICENSE.txt. + * + * GPL Classpath Exception: + * Oracle designates this particular file as subject to the "Classpath" + * exception as provided by Oracle in the GPL Version 2 section of the License + * file that accompanied this code. + * + * Modifications: + * If applicable, add the following below the License Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyright [year] [name of copyright owner]" + * + * Contributor(s): + * If you wish your version of this file to be governed by only the CDDL or + * only the GPL Version 2, indicate your decision by adding "[Contributor] + * elects to include this software in this distribution under the [CDDL or GPL + * Version 2] license." If you don't indicate a single choice of license, a + * recipient has the option to distribute your version of this file under + * either the CDDL, the GPL Version 2 or to extend the choice of license to + * its licensees as provided above. However, if you add GPL Version 2 code + * and therefore, elected the GPL Version 2 license, then the option applies + * only if the new code is made subject to such option by the copyright + * holder. + */ +package com.sun.enterprise.v3.admin; + +import com.sun.enterprise.util.LocalStringManagerImpl; +import java.io.File; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.logging.Logger; +import jakarta.inject.Inject; +import jakarta.xml.bind.JAXBContext; +import jakarta.xml.bind.JAXBException; +import jakarta.xml.bind.Marshaller; +import jakarta.xml.bind.Unmarshaller; +import org.glassfish.api.admin.progress.JobInfo; +import org.glassfish.api.admin.progress.JobInfos; +import org.glassfish.api.admin.progress.JobPersistence; +import org.glassfish.kernel.KernelLoggerInfo; +import org.jvnet.hk2.annotations.Service; + + +/** + * This service persists information for managed jobs to the file + * @author Bhakti Mehta + */ +@Service(name="job-persistence") +public class JobPersistenceService implements JobPersistence { + + protected Marshaller jaxbMarshaller ; + + protected Unmarshaller jaxbUnmarshaller; + + protected JobInfos jobInfos; + + @Inject + private JobManagerService jobManager; + + protected JAXBContext jaxbContext; + + protected final static Logger logger = KernelLoggerInfo.getLogger(); + + + protected static final LocalStringManagerImpl adminStrings = + new LocalStringManagerImpl(JobPersistenceService.class); + @Override + public void persist(Object obj) { + JobInfo jobInfo = (JobInfo)obj; + + jobInfos = jobManager.getCompletedJobs(jobManager.getJobsFile()); + + doPersist(jobInfos,jobInfo); + + } + + public void doPersist(JobInfos jobInfos, JobInfo jobInfo) { + File file = jobInfo.getJobsFile(); + synchronized (file) { + + if (jobInfos == null) { + jobInfos = new JobInfos(); + } + + try { + JAXBContext jaxbContext = JAXBContext.newInstance(JobInfos.class); + jaxbMarshaller = jaxbContext.createMarshaller(); + jaxbMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); + + CopyOnWriteArrayList jobList = new CopyOnWriteArrayList(jobInfos.getJobInfoList()); + jobInfos.setJobInfoList(jobList); + jobList.add(jobInfo); + jaxbMarshaller.marshal(jobInfos, file); + jobManager.addToCompletedJobs(new CompletedJob(jobInfo.jobId,jobInfo.commandCompletionDate,jobInfo.getJobsFile())); + jobManager.purgeJob(jobInfo.jobId); + + } catch (JAXBException e) { + throw new RuntimeException(adminStrings.getLocalString("error.persisting.jobs","Error while persisting jobs",jobInfo.jobId,e.getLocalizedMessage()),e); + + } + } + } + + + + +} diff --git a/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/commands/AttachCommand.java b/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/commands/AttachCommand.java index c59fcd0bf42..41df6a3538c 100644 --- a/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/commands/AttachCommand.java +++ b/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/commands/AttachCommand.java @@ -37,12 +37,14 @@ * only if the new code is made subject to such option by the copyright * holder. */ -// Portions Copyright [2022] [Payara Foundation and/or its affiliates] + package com.sun.enterprise.v3.admin.commands; import com.sun.enterprise.admin.remote.AdminCommandStateImpl; import com.sun.enterprise.util.LocalStringManagerImpl; +import jakarta.inject.Inject; +import com.sun.enterprise.v3.admin.JobManagerService; import org.glassfish.api.ActionReport; import org.glassfish.api.I18n; import org.glassfish.api.Param; @@ -81,12 +83,34 @@ public class AttachCommand implements AdminCommand, AdminCommandListener { protected AdminCommandEventBroker eventBroker; protected Job attached; + + @Inject + JobManagerService registry; @Param(primary=true, optional=false, multiple=false) protected String jobID; @Override public void execute(AdminCommandContext context) { + eventBroker = context.getEventBroker(); + + attached = registry.get(jobID); + JobInfo jobInfo = null; + String jobName = null; + + if (attached == null) { + //try for completed jobs + if (registry.getCompletedJobs(registry.getJobsFile()) != null) { + jobInfo = (JobInfo) registry.getCompletedJobForId(jobID); + } + if (jobInfo != null) { + jobName = jobInfo.jobName; + } + + } + + attach(attached,jobInfo,context,jobName); + } @Override @@ -104,6 +128,15 @@ public void onAdminCommandEvent(String name, Object event) { } } + + protected void purgeJob(String jobid) { + try { + registry.purgeJob(jobid); + registry.purgeCompletedJobForId(jobid); + } catch (Exception ex) { + } + } + public void attach(Job attached, JobInfo jobInfo, AdminCommandContext context,String jobName) { ActionReport ar = context.getActionReport(); String attachedUser = SubjectUtil.getUsernamesFromSubject(context.getSubject()).get(0); @@ -151,6 +184,10 @@ public void attach(Job attached, JobInfo jobInfo, AdminCommandContext context,St String commandUser = attached.getSubjectUsernames().get(0); //In most cases if the user who attaches to the command is the same //as one who started it then purge the job once it is completed + if ((commandUser != null && commandUser.equals(attachedUser)) && attached.isOutboundPayloadEmpty()) { + purgeJob(attached.getId()); + + } ar.setActionExitCode(attached.getActionReport().getActionExitCode()); ar.appendMessage(strings.getLocalString("attach.finished", "Command {0} executed with status {1}",attached.getName(),attached.getActionReport().getActionExitCode())); } @@ -161,6 +198,10 @@ public void attach(Job attached, JobInfo jobInfo, AdminCommandContext context,St //In most cases if the user who attaches to the command is the same //as one who started it then purge the job once it is completed + if (attachedUser!= null && attachedUser.equals( jobInfo.user)) { + purgeJob(jobInfo.jobId); + + } ar.setActionExitCode(ActionReport.ExitCode.SUCCESS); ar.appendMessage(strings.getLocalString("attach.finished", "Command {0} executed{1}",jobName,jobInfo.exitCode)); } diff --git a/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/commands/ListJobsCommand.java b/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/commands/ListJobsCommand.java index 48b73f01667..d595e6d209a 100644 --- a/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/commands/ListJobsCommand.java +++ b/nucleus/core/kernel/src/main/java/com/sun/enterprise/v3/admin/commands/ListJobsCommand.java @@ -37,22 +37,27 @@ * only if the new code is made subject to such option by the copyright * holder. */ -// Portions Copyright [2022] [Payara Foundation and/or its affiliates] package com.sun.enterprise.v3.admin.commands; +import com.sun.enterprise.admin.progress.ProgressStatusClient; +import com.sun.enterprise.util.StringUtils; import com.sun.enterprise.util.i18n.StringManager; +import com.sun.enterprise.v3.admin.JobAuthorizationAttributeProcessor; import java.text.SimpleDateFormat; import java.util.*; import java.util.Collection; import java.util.Date; import jakarta.inject.Inject; +import com.sun.enterprise.v3.admin.JobManagerService; import org.glassfish.api.ActionReport; import org.glassfish.api.ActionReport.MessagePart; import org.glassfish.api.I18n; import org.glassfish.api.Param; import org.glassfish.api.admin.*; import org.glassfish.api.admin.progress.JobInfo; +import org.glassfish.api.admin.progress.JobInfos; import org.glassfish.hk2.api.PerLookup; +import org.glassfish.security.services.common.SubjectUtil; import org.jvnet.hk2.annotations.Service; @@ -70,6 +75,9 @@ public class ListJobsCommand implements AdminCommand,AdminCommandSecurity.Access private ActionReport report; private static final String DEFAULT_USER_STRING = "-"; + @Inject + private JobManagerService jobManagerService; + /** * Associates an access check with each candidate JobInfo we might report on. */ @@ -102,6 +110,14 @@ public class ListJobsCommand implements AdminCommand,AdminCommandSecurity.Access final private static StringManager localStrings = StringManager.getManager(ListJobsCommand.class); + + protected JobInfos getCompletedJobs() { + return jobManagerService.getCompletedJobs(jobManagerService.getJobsFile()); + } + + protected JobInfo getCompletedJobForId(final String jobID) { + return (JobInfo) jobManagerService.getCompletedJobForId(jobID); + } protected boolean isSingleJobOK(final Job singleJob) { return (singleJob != null); @@ -115,7 +131,67 @@ protected boolean checkScope(Job job) { return job.getScope()==null; } + private List chooseJobs() { + List jobsToReport = new ArrayList(); + + if (jobID != null) { + Job oneJob = jobManagerService.get(jobID); + JobInfo info = null; + + if (isSingleJobOK(oneJob)) { + List userList = oneJob.getSubjectUsernames(); + ActionReport actionReport = oneJob.getActionReport(); + String message = actionReport == null ? "" : actionReport.getMessage(); + + if (!StringUtils.ok(message)) { + message = ProgressStatusClient.composeMessageForPrint(oneJob.getCommandProgress()); + } + String exitCode = actionReport == null ? "" : actionReport.getActionExitCode().name(); + info = new JobInfo(oneJob.getId(),oneJob.getName(),oneJob.getCommandExecutionDate(),exitCode,userList.get(0),message,oneJob.getJobsFile(),oneJob.getState().name(),0); + + } else { + if (getCompletedJobs() != null) { + info = getCompletedJobForId(jobID); + } + } + if (info != null && !skipJob(info.jobName)) { + jobsToReport.add(info); + } + + } else { + + for (Iterator iterator = jobManagerService.getJobs(); iterator.hasNext(); ) { + Job job = iterator.next(); + if (isJobEligible(job)) { + List userList = job.getSubjectUsernames(); + ActionReport actionReport = job.getActionReport(); + + String message = actionReport == null ? "" : actionReport.getMessage(); + if (!StringUtils.ok(message)) { + message = ProgressStatusClient.composeMessageForPrint(job.getCommandProgress()); + } + String exitCode = actionReport == null ? "" : actionReport.getActionExitCode().name(); + + String user = DEFAULT_USER_STRING; + if(userList.size() > 0){ + user = userList.get(0); + } + jobsToReport.add(new JobInfo(job.getId(),job.getName(),job.getCommandExecutionDate(),exitCode,user,message,job.getJobsFile(),job.getState().name(),0)); + } + } + + JobInfos completedJobs = getCompletedJobs(); + if (completedJobs != null ) { + for (JobInfo info : completedJobs.getJobInfoList()) { + if (!skipJob(info.jobName)) { + jobsToReport.add(info); + } + } + } + } + return jobsToReport; + } @Override public void execute(AdminCommandContext context) { @@ -129,6 +205,11 @@ public static boolean skipJob(String name) { @Override public Collection getAccessChecks() { + final List jobInfoList = chooseJobs(); + for (JobInfo jobInfo : jobInfoList) { + jobAccessChecks.add(new AccessRequired.AccessCheck(jobInfo, + JobAuthorizationAttributeProcessor.JOB_RESOURCE_NAME_PREFIX + jobInfo.jobId,"read", false)); + } return jobAccessChecks; } diff --git a/nucleus/tests/admin/pom.xml b/nucleus/tests/admin/pom.xml index 326d10d89c3..d9159ddb572 100644 --- a/nucleus/tests/admin/pom.xml +++ b/nucleus/tests/admin/pom.xml @@ -73,7 +73,7 @@ modify org.glassfish.nucleus.admin.NucleusStartStopTest static initializer of COPY_LIB map constant.) --> - + 4.0.0 @@ -182,7 +182,7 @@ of COPY_LIB map constant.) jakarta.json jakarta.json-api - 1.1.6 + 1.1 diff --git a/nucleus/tests/admin/src/test/java/org/glassfish/nucleus/admin/progress/DetachAttachTest.java b/nucleus/tests/admin/src/test/java/org/glassfish/nucleus/admin/progress/DetachAttachTest.java index 103559127ce..4de0c99c351 100644 --- a/nucleus/tests/admin/src/test/java/org/glassfish/nucleus/admin/progress/DetachAttachTest.java +++ b/nucleus/tests/admin/src/test/java/org/glassfish/nucleus/admin/progress/DetachAttachTest.java @@ -37,7 +37,6 @@ * only if the new code is made subject to such option by the copyright * holder. */ -// Portions Copyright [2022] [Payara Foundation and/or its affiliates] package org.glassfish.nucleus.admin.progress; import java.util.ArrayList; @@ -67,6 +66,7 @@ public class DetachAttachTest { @AfterTest public void cleanUp() throws Exception { nadmin("stop-domain"); + JobManagerTest.deleteJobsFile(); nadmin("start-domain"); } diff --git a/nucleus/tests/admin/src/test/java/org/glassfish/nucleus/admin/progress/JobManagerTest.java b/nucleus/tests/admin/src/test/java/org/glassfish/nucleus/admin/progress/JobManagerTest.java new file mode 100644 index 00000000000..a7f38ecbb60 --- /dev/null +++ b/nucleus/tests/admin/src/test/java/org/glassfish/nucleus/admin/progress/JobManagerTest.java @@ -0,0 +1,208 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright (c) 2012 Oracle and/or its affiliates. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common Development + * and Distribution License("CDDL") (collectively, the "License"). You + * may not use this file except in compliance with the License. You can + * obtain a copy of the License at + * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html + * or packager/legal/LICENSE.txt. See the License for the specific + * language governing permissions and limitations under the License. + * + * When distributing the software, include this License Header Notice in each + * file and include the License file at packager/legal/LICENSE.txt. + * + * GPL Classpath Exception: + * Oracle designates this particular file as subject to the "Classpath" + * exception as provided by Oracle in the GPL Version 2 section of the License + * file that accompanied this code. + * + * Modifications: + * If applicable, add the following below the License Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyright [year] [name of copyright owner]" + * + * Contributor(s): + * If you wish your version of this file to be governed by only the CDDL or + * only the GPL Version 2, indicate your decision by adding "[Contributor] + * elects to include this software in this distribution under the [CDDL or GPL + * Version 2] license." If you don't indicate a single choice of license, a + * recipient has the option to distribute your version of this file under + * either the CDDL, the GPL Version 2 or to extend the choice of license to + * its licensees as provided above. However, if you add GPL Version 2 code + * and therefore, elected the GPL Version 2 license, then the option applies + * only if the new code is made subject to such option by the copyright + * holder. + */ +package org.glassfish.nucleus.admin.progress; + +import java.io.File; +import java.lang.InterruptedException; +import java.lang.Thread; + +import static org.glassfish.tests.utils.NucleusTestUtils.*; +import org.glassfish.tests.utils.NucleusTestUtils; +import static org.testng.AssertJUnit.assertTrue; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +/** + * This tests the functionality of JobManager, list-jobs + * @author Bhakti Mehta + */ +@Test(testName="JobManagerTest", enabled=true) +public class JobManagerTest { + private static File nucleusRoot = NucleusTestUtils.getNucleusRoot(); + + private final String COMMAND1 = "progress-simple"; + + @BeforeTest + public void setUp() throws Exception { + nadmin("stop-domain"); + //delete jobs.xml incase there were other jobs run + deleteJobsFile(); + + nadmin("start-domain"); + + + } + + @AfterTest + public void cleanUp() throws Exception { + nadmin("stop-domain"); + nadmin("start-domain"); + + } + + @Test(enabled=true) + public void noJobsTest() { + nadmin("stop-domain"); + //delete jobs.xml incase there were other jobs run + deleteJobsFile(); + nadmin("start-domain"); + String result = null; + result = nadminWithOutput("list-jobs").outAndErr; + assertTrue(matchString("Nothing to list", result)); + + + } + + @Test(dependsOnMethods = { "noJobsTest" },enabled=true) + public void runJobTest() { + String result = null; + + NadminReturn result1 = nadminWithOutput("--terse", "progress-simple"); + assertTrue(result1.returnValue); + //check list-jobs + result = nadminWithOutput("list-jobs").out; + assertTrue( result.contains(COMMAND1) && result.contains("COMPLETED")); + //check list-jobs with id 1 + result = nadminWithOutput("list-jobs","1").out; + assertTrue( result.contains(COMMAND1) && result.contains("COMPLETED")); + //shutdown server + assertTrue( nadmin("stop-domain")); + //restart + assertTrue( nadmin("start-domain")); + //check jobs + result = nadminWithOutput("list-jobs","1").out; + assertTrue( result.contains(COMMAND1) && result.contains("COMPLETED")); + nadmin("start-domain"); + + } + + @Test(dependsOnMethods = { "runJobTest" }, enabled=true) + public void runDetachTest() { + String result = null; + //shutdown server + assertTrue( nadmin("stop-domain")); + + //delete the jobs file + deleteJobsFile(); + + //restart + assertTrue( nadmin("start-domain")); + result = nadminDetachWithOutput( COMMAND1).out; + //Detached job id is returned + assertTrue( result.contains("Job ID: ")); + + //list-jobs + result = nadminWithOutput("list-jobs","1").out; + assertTrue( result.contains(COMMAND1) ); + //attach to the job + assertTrue(nadmin("attach", "1")); + + //list-jobs and it should be purged since the user + //starting is the same as the user who attached to it + result = nadminWithOutput("list-jobs").outAndErr; + assertTrue(matchString("Nothing to list", result)); + + //delete the jobs file + deleteJobsFile(); + + + + } + + @Test(dependsOnMethods = { "runDetachTest" }, enabled=true) + public void runConfigureManagedJobsTest() throws InterruptedException { + try { + String result = null; + //shutdown server + assertTrue( nadmin("stop-domain")); + + //delete the jobs file + deleteJobsFile(); + + //restart + assertTrue( nadmin("start-domain")); + //configure-managed-jobs + assertTrue( nadmin("configure-managed-jobs","--job-retention-period=6s","--cleanup-initial-delay=2s", + "--cleanup-poll-interval=2s")); + assertTrue(COMMAND1, nadmin(COMMAND1)); + + + //list-jobs + result = nadminWithOutput("list-jobs","1").out; + assertTrue( result.contains(COMMAND1) ); + //shutdown server + assertTrue( nadmin("stop-domain")); + + //start server + assertTrue( nadmin("start-domain")); + Thread.sleep(5000L); + + //list-jobs there should be none since the configure-managed-jobs command will purge it + result = nadminWithOutput("list-jobs").outAndErr; + assertTrue(matchString("Nothing to list", result)); + + } finally { + //reset configure-managed-jobs + assertTrue( nadmin("configure-managed-jobs","--job-retention-period=24h","--cleanup-initial-delay=20m", + "--cleanup-poll-interval=20m")); + } + //delete the jobs file + deleteJobsFile(); + + + + } + + /** + * This will delete the jobs.xml file + */ + public static void deleteJobsFile() { + File configDir = new File(nucleusRoot,"domains/domain1/config"); + File jobsFile = new File (configDir,"jobs.xml"); + System.out.println("Deleting.. " + jobsFile); + if (jobsFile!= null && jobsFile.exists()) { + jobsFile.delete(); + } + } + + +} +