/** * Copyright (c) 2000-present Liferay, Inc. All rights reserved. * * This library is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 2.1 of the License, or (at your option) * any later version. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. */ package com.liferay.portal.kernel.process.local; import com.liferay.portal.kernel.concurrent.AbortPolicy; import com.liferay.portal.kernel.concurrent.AsyncBroker; import com.liferay.portal.kernel.concurrent.FutureListener; import com.liferay.portal.kernel.concurrent.NoticeableFuture; import com.liferay.portal.kernel.concurrent.NoticeableFutureConverter; import com.liferay.portal.kernel.concurrent.ThreadPoolExecutor; import com.liferay.portal.kernel.concurrent.ThreadPoolHandlerAdapter; import com.liferay.portal.kernel.io.unsync.UnsyncBufferedInputStream; import com.liferay.portal.kernel.io.unsync.UnsyncByteArrayOutputStream; import com.liferay.portal.kernel.log.Log; import com.liferay.portal.kernel.log.LogFactoryUtil; import com.liferay.portal.kernel.process.ProcessCallable; import com.liferay.portal.kernel.process.ProcessChannel; import com.liferay.portal.kernel.process.ProcessConfig; import com.liferay.portal.kernel.process.ProcessException; import com.liferay.portal.kernel.process.ProcessExecutor; import com.liferay.portal.kernel.process.TerminationProcessException; import com.liferay.portal.kernel.util.ClassLoaderObjectInputStream; import com.liferay.portal.kernel.util.NamedThreadFactory; import com.liferay.portal.kernel.util.PortalClassLoaderUtil; import com.liferay.portal.kernel.util.StreamUtil; import java.io.EOFException; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.io.StreamCorruptedException; import java.io.WriteAbortedException; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Future; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; /** * @author Shuyang Zhou */ public class LocalProcessExecutor implements ProcessExecutor { public Set<Process> destroy() { if (_threadPoolExecutor == null) { return Collections.emptySet(); } Set<Process> processes = Collections.emptySet(); synchronized (this) { if (_threadPoolExecutor != null) { processes = new HashSet<>(); _threadPoolExecutor.shutdownNow(); // At this point, the thread pool will no longer take in any // more subprocess reactors, so we know the list of managed // processes is in a safe state. The worst case is that the // destroyer thread and the thread pool thread concurrently // destroy the same process, but this is JDK's job to ensure // that processes are destroyed in a thread safe manner. Set<Entry<Process, NoticeableFuture<?>>> set = _managedProcesses.entrySet(); Iterator<Entry<Process, NoticeableFuture<?>>> iterator = set.iterator(); while (iterator.hasNext()) { Entry<Process, NoticeableFuture<?>> entry = iterator.next(); processes.add(entry.getKey()); NoticeableFuture<?> noticeableFuture = entry.getValue(); noticeableFuture.cancel(true); iterator.remove(); } // The current thread has a more comprehensive view of the list // of managed processes than any thread pool thread. After the // previous iteration, we are safe to clear the list of managed // processes. _managedProcesses.clear(); _threadPoolExecutor = null; } } // Whip's instrument logic sees a label on a synchronized block exit and // asks for coverage, but it does not understand that this is actually // the same as exiting a method. To overcome this limitation, the code // logic has to explicitly leave the synchronized block before leaving // the method. This limitation will be removed in a future version of // Whip. return processes; } @Override public <T extends Serializable> ProcessChannel<T> execute( ProcessConfig processConfig, ProcessCallable<T> processCallable) throws ProcessException { try { List<String> arguments = processConfig.getArguments(); List<String> commands = new ArrayList<>(arguments.size() + 4); commands.add(processConfig.getJavaExecutable()); commands.add("-cp"); commands.add(processConfig.getBootstrapClassPath()); commands.addAll(arguments); commands.add(LocalProcessLauncher.class.getName()); ProcessBuilder processBuilder = new ProcessBuilder(commands); final Process process = processBuilder.start(); ObjectOutputStream bootstrapObjectOutputStream = new ObjectOutputStream(process.getOutputStream()); bootstrapObjectOutputStream.writeObject(processCallable.toString()); bootstrapObjectOutputStream.writeObject( processConfig.getRuntimeClassPath()); ObjectOutputStream objectOutputStream = new ObjectOutputStream( bootstrapObjectOutputStream); objectOutputStream.writeObject(processCallable); objectOutputStream.flush(); ThreadPoolExecutor threadPoolExecutor = _getThreadPoolExecutor(); AsyncBroker<Long, Serializable> asyncBroker = new AsyncBroker<>(); SubprocessReactor subprocessReactor = new SubprocessReactor( process, processConfig.getReactClassLoader(), asyncBroker); try { NoticeableFuture<ProcessCallable<? extends Serializable>> processCallableNoticeableFuture = threadPoolExecutor.submit( subprocessReactor); processCallableNoticeableFuture.addFutureListener( new FutureListener <ProcessCallable<? extends Serializable>>() { @Override public void complete( Future<ProcessCallable<? extends Serializable>> future) { if (future.isCancelled()) { process.destroy(); } } }); // Consider the newly created process as a managed process only // after the subprocess reactor is taken by the thread pool _managedProcesses.put(process, processCallableNoticeableFuture); NoticeableFuture<T> noticeableFuture = new NoticeableFutureConverter <T, ProcessCallable<? extends Serializable>>( processCallableNoticeableFuture) { @Override protected T convert( ProcessCallable<? extends Serializable> processCallable) throws ProcessException { if (processCallable instanceof ReturnProcessCallable<?>) { return (T)processCallable.call(); } ExceptionProcessCallable exceptionProcessCallable = (ExceptionProcessCallable)processCallable; throw exceptionProcessCallable.call(); } }; return new LocalProcessChannel<>( noticeableFuture, objectOutputStream, asyncBroker); } catch (RejectedExecutionException ree) { process.destroy(); throw new ProcessException( "Cancelled execution because of a concurrent destroy", ree); } } catch (IOException ioe) { throw new ProcessException(ioe); } } private ThreadPoolExecutor _getThreadPoolExecutor() { if (_threadPoolExecutor != null) { return _threadPoolExecutor; } synchronized (this) { if (_threadPoolExecutor == null) { _threadPoolExecutor = new ThreadPoolExecutor( 0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS, true, Integer.MAX_VALUE, new AbortPolicy(), new NamedThreadFactory( LocalProcessExecutor.class.getName(), Thread.MIN_PRIORITY, PortalClassLoaderUtil.getClassLoader()), new ThreadPoolHandlerAdapter()); } } return _threadPoolExecutor; } private static final Log _log = LogFactoryUtil.getLog( LocalProcessExecutor.class); private final Map<Process, NoticeableFuture<?>> _managedProcesses = new ConcurrentHashMap<>(); private volatile ThreadPoolExecutor _threadPoolExecutor; private class SubprocessReactor implements Callable<ProcessCallable<? extends Serializable>> { public SubprocessReactor( Process process, ClassLoader reactClassLoader, AsyncBroker<Long, Serializable> asyncBroker) { _process = process; _reactClassLoader = reactClassLoader; _asyncBroker = asyncBroker; } @Override public ProcessCallable<? extends Serializable> call() throws Exception { ProcessCallable<?> resultProcessCallable = null; AsyncBrokerThreadLocal.setAsyncBroker(_asyncBroker); UnsyncBufferedInputStream unsyncBufferedInputStream = new UnsyncBufferedInputStream(_process.getInputStream()); try { ObjectInputStream objectInputStream = null; UnsyncByteArrayOutputStream unsyncByteArrayOutputStream = new UnsyncByteArrayOutputStream(); while (true) { try { // Be ready for a bad header unsyncBufferedInputStream.mark(4); objectInputStream = new ClassLoaderObjectInputStream( unsyncBufferedInputStream, _reactClassLoader); // Found the beginning of the object input stream. Flush // out corrupted log if necessary. if (unsyncByteArrayOutputStream.size() > 0) { if (_log.isWarnEnabled()) { _log.warn( "Found corrupt leading log " + unsyncByteArrayOutputStream.toString()); } } unsyncByteArrayOutputStream = null; break; } catch (StreamCorruptedException sce) { // Collecting bad header as log information unsyncBufferedInputStream.reset(); unsyncByteArrayOutputStream.write( unsyncBufferedInputStream.read()); } } while (true) { Object obj = null; try { obj = objectInputStream.readObject(); } catch (WriteAbortedException wae) { if (_log.isWarnEnabled()) { _log.warn("Caught a write aborted exception", wae); } continue; } if (!(obj instanceof ProcessCallable)) { if (_log.isInfoEnabled()) { _log.info( "Received a nonprocess callable piping back " + obj); } continue; } ProcessCallable<?> processCallable = (ProcessCallable<?>)obj; if ((processCallable instanceof ExceptionProcessCallable) || (processCallable instanceof ReturnProcessCallable<?>)) { resultProcessCallable = processCallable; continue; } try { Serializable returnValue = processCallable.call(); if (_log.isDebugEnabled()) { _log.debug( "Invoked generic process callable " + processCallable + " with return value " + returnValue); } } catch (Throwable t) { _log.error( "Unable to invoke generic process callable", t); } } } catch (StreamCorruptedException sce) { File file = File.createTempFile( "corrupted-stream-dump-" + System.currentTimeMillis(), ".log"); _log.error( "Dumping content of corrupted object input stream to " + file.getAbsolutePath(), sce); FileOutputStream fileOutputStream = new FileOutputStream(file); StreamUtil.transfer( unsyncBufferedInputStream, fileOutputStream); throw new ProcessException( "Corrupted object input stream", sce); } catch (EOFException eofe) { throw new ProcessException( "Subprocess piping back ended prematurely", eofe); } catch (Throwable t) { _log.error("Abort subprocess piping", t); throw t; } finally { try { int exitCode = _process.waitFor(); if (exitCode != 0) { throw new TerminationProcessException(exitCode); } } catch (InterruptedException ie) { _process.destroy(); throw new ProcessException( "Forcibly killed subprocess on interruption", ie); } _managedProcesses.remove(_process); if (resultProcessCallable != null) { // Override previous process exception if there was one return resultProcessCallable; } AsyncBrokerThreadLocal.removeAsyncBroker(); } } private final AsyncBroker<Long, Serializable> _asyncBroker; private final Process _process; private final ClassLoader _reactClassLoader; } }