All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.netbeans.api.extexecution.base.BaseExecutionService Maven / Gradle / Ivy

The newest version!
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.netbeans.api.extexecution.base;

import java.io.BufferedInputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.nio.charset.Charset;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.extexecution.base.BaseExecutionDescriptor.InputProcessorFactory;
import org.netbeans.api.extexecution.base.input.InputProcessor;
import org.netbeans.api.extexecution.base.input.InputProcessors;
import org.netbeans.api.extexecution.base.input.InputReaderTask;
import org.netbeans.api.extexecution.base.input.InputReaders;
import org.netbeans.modules.extexecution.base.ProcessInputStream;
import org.openide.util.Cancellable;
import org.openide.util.RequestProcessor;

/**
 * Base Execution service provides the facility to execute a process.
 * 

* All processes launched by this class are terminated on VM exit (if * these are not finished or terminated earlier). * *

*

* Sample usage (ls command): *

 *     BaseExecutionDescriptor descriptor = new BaseExecutionDescriptor()
 *             .outProcessorFactory(new BaseExecutionDescriptor.InputProcessorFactory() {
 *
 *         @Override
 *         public InputProcessor newInputProcessor() {
 *             return InputProcessors.copying(new BufferedWriter(new OutputStreamWriter(System.out)));
 *         }
 *     });
 *
 *     ProcessBuilder processBuilder = ProcessBuilder.getLocal();
 *     processBuilder.setExecutable(ls);
 *
 *     BaseExecutionService service = BaseExecutionService.newService(processBuilder, descriptor);
 *     Future<Integer> task = service.run();
 * 
*

* Even simpler usage but without displaying output (ls command): *

 *     ProcessBuilder processBuilder = ProcessBuilder.getLocal();
 *     processBuilder.setExecutable(ls);
 *
 *     ExecutionService service = ExecutionService.newService(processBuilder, new BaseExecutionDescriptor());
 *     Future<Integer> task = service.run();
 * 
*
* * @author Petr Hejl * @see #newService(java.util.concurrent.Callable, org.netbeans.api.extexecution.base.BaseExecutionDescriptor) * @see BaseExecutionDescriptor */ public final class BaseExecutionService { private static final Logger LOGGER = Logger.getLogger(BaseExecutionService.class.getName()); private static final Set RUNNING_PROCESSES = new HashSet(); private static final int EXECUTOR_SHUTDOWN_SLICE = 1000; private static final ExecutorService EXECUTOR_SERVICE = new RequestProcessor(BaseExecutionService.class.getName(), Integer.MAX_VALUE); static { // shutdown hook Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { EXECUTOR_SERVICE.shutdown(); synchronized (RUNNING_PROCESSES) { for (Process process : RUNNING_PROCESSES) { process.destroy(); } } } }); } private final Callable processCreator; private final BaseExecutionDescriptor descriptor; private BaseExecutionService(Callable processCreator, BaseExecutionDescriptor descriptor) { this.processCreator = processCreator; this.descriptor = descriptor; } /** * Creates new execution service. Service will wrap up the processes * created by processCreator and will manage them. * * @param processCreator callable returning the process to wrap up * @param descriptor descriptor describing the configuration of service * @return new execution service */ @NonNull public static BaseExecutionService newService(@NonNull Callable processCreator, @NonNull BaseExecutionDescriptor descriptor) { return new BaseExecutionService(processCreator, descriptor); } /** * Infers the output encoding from the relevant system properties, if those should all be null * then this will fallback to Charset.defaultCharset() * * Since JDK 18 and JEP400 Console.charset() is used for the console's encoding instead of Charset.defaultCharset(). * Console.charset() is exposed via stdout.encoding/sun.stdout.encoding. * If ran with JDK<=16 stdout.encoding and native.encoding should be null and the old default of Charset.defaultCharset() will be used to match pre JEP400 behavior. * * The checking order for the encoding is stdout.encoding, sun.stdout.encoding, native.encoding, Charset.defaultCharset() * * @see org.netbeans.modules.maven.execute.CommandLineOutputHandler#getPreferredCharset * * @return inferred encoding as Charset */ private static Charset getInputOutputEncoding(){ String[] encodingSystemProperties = new String[]{"stdout.encoding", "sun.stdout.encoding", "native.encoding"}; Charset preferredCharset = null; for (String encodingProperty : encodingSystemProperties) { String encodingPropertyValue = System.getProperty(encodingProperty); if (encodingPropertyValue == null) { continue; } try { preferredCharset = Charset.forName(encodingPropertyValue); } catch (IllegalArgumentException ex) { LOGGER.log(java.util.logging.Level.WARNING, "Failed to get charset for '" + encodingProperty + "' value : '" + encodingPropertyValue + "'", ex); } if (preferredCharset != null) { return preferredCharset; } } return Charset.defaultCharset(); } /** * Runs the process described by this service. The call does not block * and the task is represented by the returned value. Integer returned * as a result of the {@link Future} is exit code of the process. *

* This method can be invoked multiple times returning the different and * unrelated {@link Future}s. On each call Callable<Process> * passed to {@link #newService(java.util.concurrent.Callable, org.netbeans.api.extexecution.base.BaseExecutionDescriptor)} * is invoked in order to create the process. If the process creation fails * (throwing an exception) returned Future will throw * {@link java.util.concurrent.ExecutionException} on {@link Future#get()} * request. *

* For details on execution control see {@link BaseExecutionDescriptor}. * * @return task representing the actual run, value representing result * of the {@link Future} is exit code of the process */ @NonNull public Future run() { final Reader in; BaseExecutionDescriptor.ReaderFactory factory = descriptor.getInReaderFactory(); if (factory != null) { in = factory.newReader(); } else { in = null; } final CountDownLatch finishedLatch = new CountDownLatch(1); Callable callable = new Callable() { @Override public Integer call() throws Exception { boolean interrupted = false; Process process = null; Integer ret = null; ExecutorService executor = null; ProcessInputStream outStream = null; ProcessInputStream errStream = null; List tasks = new ArrayList(); try { final Runnable pre = descriptor.getPreExecution(); if (pre != null) { pre.run(); } if (Thread.currentThread().isInterrupted()) { return null; } process = processCreator.call(); synchronized (RUNNING_PROCESSES) { RUNNING_PROCESSES.add(process); } if (Thread.currentThread().isInterrupted()) { return null; } outStream = new ProcessInputStream(process, process.getInputStream()); errStream = new ProcessInputStream(process, process.getErrorStream()); executor = Executors.newFixedThreadPool(in != null ? 3 : 2); Charset charset = descriptor.getCharset(); // The CommandLineOutputHandler used the default charset to convert // output from command line invocations to strings. That encoding is // derived from the system file.encoding. From JDK 18 onwards its // default value changed to UTF-8. // JDK 17+ exposes the native encoding as the new system property // native.encoding, prior versions don't have that property and will // report NULL for it. // To account for the behavior of JEP400 the following order is used to determine the encoding: // stdout.encoding, sun.stdout.encoding, native.encoding, Charset.defaultCharset() if (charset == null) { charset = BaseExecutionService.getInputOutputEncoding(); } tasks.add(InputReaderTask.newDrainingTask( InputReaders.forStream(new BufferedInputStream(outStream), charset), createOutProcessor())); tasks.add(InputReaderTask.newDrainingTask( InputReaders.forStream(new BufferedInputStream(errStream), charset), createErrProcessor())); if (in != null) { tasks.add(InputReaderTask.newTask( InputReaders.forReader(in), createInProcessor(process.getOutputStream(), charset))); } for (InputReaderTask task : tasks) { executor.submit(task); } process.waitFor(); } catch (InterruptedException ex) { LOGGER.log(Level.FINE, null, ex); interrupted = true; } catch (Throwable t) { LOGGER.log(Level.INFO, null, t); throw new WrappedException(t); } finally { try { // fully evaluated - we want to clear interrupted status in any case interrupted |= Thread.interrupted(); if (!interrupted) { if (outStream != null) { outStream.close(true); } if (errStream != null) { errStream.close(true); } } if (process != null) { process.destroy(); synchronized (RUNNING_PROCESSES) { RUNNING_PROCESSES.remove(process); } try { ret = process.exitValue(); } catch (IllegalThreadStateException ex) { LOGGER.log(Level.FINE, "Process not yet exited", ex); } } } catch (Throwable t) { LOGGER.log(Level.INFO, null, t); throw new WrappedException(t); } finally { try { cleanup(tasks, executor); final ParametrizedRunnable post = descriptor.getPostExecution(); if (post != null) { post.run(ret); } } finally { finishedLatch.countDown(); if (interrupted) { Thread.currentThread().interrupt(); } } } } return ret; } }; final FutureTask current = new FutureTask(callable) { @Override protected void setException(Throwable t) { if (t instanceof WrappedException) { super.setException(((WrappedException) t).getCause()); } else { super.setException(t); } } }; EXECUTOR_SERVICE.execute(current); return current; } private void cleanup(final List tasks, final ExecutorService processingExecutor) { boolean interrupted = false; if (processingExecutor != null) { try { AccessController.doPrivileged(new PrivilegedAction() { @Override public Void run() { processingExecutor.shutdown(); return null; } }); for (Cancellable cancellable : tasks) { cancellable.cancel(); } while (!processingExecutor.awaitTermination(EXECUTOR_SHUTDOWN_SLICE, TimeUnit.MILLISECONDS)) { LOGGER.log(Level.INFO, "Awaiting processing finish"); } } catch (InterruptedException ex) { interrupted = true; } } if (interrupted) { Thread.currentThread().interrupt(); } } private InputProcessor createOutProcessor() { InputProcessor outProcessor = null; InputProcessorFactory descriptorOutFactory = descriptor.getOutProcessorFactory(); if (descriptorOutFactory != null) { outProcessor = descriptorOutFactory.newInputProcessor(); } return outProcessor; } private InputProcessor createErrProcessor() { InputProcessor errProcessor = null; InputProcessorFactory descriptorErrFactory = descriptor.getErrProcessorFactory(); if (descriptorErrFactory != null) { errProcessor = descriptorErrFactory.newInputProcessor(); } return errProcessor; } private InputProcessor createInProcessor(OutputStream os, Charset charset) { return InputProcessors.copying(new OutputStreamWriter(os, charset)); } private static class WrappedException extends Exception { public WrappedException(Throwable cause) { super(cause); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy