package er.extensions.foundation; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.sql.SQLException; import java.util.Enumeration; import java.util.Timer; import java.util.TimerTask; import java.util.UUID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.webobjects.appserver.WOContext; import com.webobjects.eoaccess.EOAdaptorChannel; import com.webobjects.eoaccess.EODatabaseContext; import com.webobjects.eoaccess.EOGeneralAdaptorException; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSBundle; import com.webobjects.foundation.NSDictionary; import com.webobjects.foundation.NSForwardException; import com.webobjects.foundation.NSMutableDictionary; import com.webobjects.foundation.NSSelector; import com.webobjects.jdbcadaptor.JDBCAdaptorException; import er.extensions.appserver.ERXWOContext; /** * Collection of utilities dealing with threads and processes. * * * @author ak * @author david */ public class ERXRuntimeUtilities { private static final Logger log = LoggerFactory.getLogger(ERXRuntimeUtilities.class); /** * Hack to create a bundle after the app is loaded. Useful for the insistence of EOF on JavaXXXAdaptor bundles. * @param name * @return a new bundle under the system temp directory */ public static NSBundle createBundleIfNeeded(String name) { File sysTempDir = new File(System.getProperty("java.io.tmpdir", "/tmp")); File newTempDir; final int maxAttempts = 5; int attemptCount = 0; do { attemptCount++; if(attemptCount > maxAttempts) { throw NSForwardException._runtimeExceptionForThrowable(new IOException( "The highly improbable has occurred! Failed to " + "create a unique temporary directory after " + maxAttempts + " attempts.")); } // create unique dir in tmp to work with String dirName = name + UUID.randomUUID().toString(); newTempDir = new File(sysTempDir, dirName); } while (newTempDir.exists()); if (newTempDir.mkdirs()) { // create basic framework bundle structure File fwkResourcesDir = new File(new File(newTempDir , name + ".framework"), "Resources"); File fwkJavaDir = new File(fwkResourcesDir, "Java"); fwkJavaDir.mkdirs(); try { ERXFileUtilities.stringToFile("{Has_WOComponents=NO;}", new File(fwkResourcesDir, "Info.plist")); } catch (IOException e) { throw NSForwardException._runtimeExceptionForThrowable(e); } return loadBundleIfNeeded(fwkJavaDir); } throw NSForwardException._runtimeExceptionForThrowable(new IOException("Failed to create temp dir named " + newTempDir.getAbsolutePath())); } /** * Load an application, framework or jar bundle if not already loaded. * * @param bundleFile - the directory or archive (e.g., jar, war) of the bundle to load * @return the bundle found at the given uri. * @throws NSForwardException if bundle loading fails */ public static NSBundle loadBundleIfNeeded(File bundleFile) { try { String canonicalPath = bundleFile.getCanonicalPath(); boolean isJar = bundleFile.isFile() && canonicalPath.endsWith(".jar"); return NSBundle._bundleWithPathShouldCreateIsJar(canonicalPath, true, isJar); } catch (IOException e) { throw NSForwardException._runtimeExceptionForThrowable(e); } } /** * Returns a dictionary with useful stuff. * @param e */ public static NSMutableDictionary<String, Object> informationForException(Exception e) { NSMutableDictionary<String, Object> extraInfo = new NSMutableDictionary<>(); if (e instanceof EOGeneralAdaptorException) { // AK NOTE: you might have sensitive info in your failed ops... NSDictionary dict = ((EOGeneralAdaptorException) e).userInfo(); if (dict != null) { Object value; // this one is a little bit heavyweight... // value = NSPropertyListSerialization.stringFromPropertyList(dict); value = dict.objectForKey(EODatabaseContext.FailedDatabaseOperationKey); if (value != null) { extraInfo.setObjectForKey(value.toString(), EODatabaseContext.FailedDatabaseOperationKey); } value = dict.objectForKey(EOAdaptorChannel.AdaptorFailureKey); if (value != null) { extraInfo.setObjectForKey(value.toString(), EOAdaptorChannel.AdaptorFailureKey); } value = dict.objectForKey(EOAdaptorChannel.FailedAdaptorOperationKey); if (value != null) { extraInfo.setObjectForKey(value.toString(), EOAdaptorChannel.FailedAdaptorOperationKey); } if (e instanceof JDBCAdaptorException) { value = ((JDBCAdaptorException) e).sqlException(); if (value != null) { extraInfo.setObjectForKey(value.toString(), "SQLException"); } } } } return extraInfo; } public static NSMutableDictionary<String, Object> informationForBundles() { NSMutableDictionary<String, Object> extraInfo = new NSMutableDictionary<>(); NSMutableDictionary<String, Object> bundleVersions = new NSMutableDictionary<String, Object>(); for (Enumeration bundles = NSBundle._allBundlesReally().objectEnumerator(); bundles.hasMoreElements();) { NSBundle bundle = (NSBundle) bundles.nextElement(); String version = ERXProperties.versionStringForFrameworkNamed(bundle.name()); if(version == null) { version = "No version provided"; } bundleVersions.setObjectForKey(version, bundle.name()); } extraInfo.setObjectForKey(bundleVersions, "Bundles"); return extraInfo; } public static NSMutableDictionary<String, Object> informationForContext(WOContext context) { NSMutableDictionary<String, Object> extraInfo = new NSMutableDictionary<>(); if (context != null) { if(context.page() != null) { extraInfo.setObjectForKey(context.page().name(), "CurrentPage"); if (context.component() != null) { extraInfo.setObjectForKey(context.component().name(), "CurrentComponent"); if (context.component().parent() != null) { extraInfo.setObjectForKey(ERXWOContext.componentPath(context), "CurrentComponentHierarchy"); } } // If this is a D2W component, get its D2W-related information from ERDirectToWeb. NSSelector d2wSelector = new NSSelector("d2wContext"); if (d2wSelector.implementedByObject(context.page())) { try { Class erDirectToWebClazz = Class.forName("er.directtoweb.ERDirectToWeb"); NSSelector infoSelector = new NSSelector("informationForContext", new Class [] {WOContext.class}); NSDictionary d2wExtraInfo = (NSDictionary)infoSelector.invoke(erDirectToWebClazz, context); extraInfo.addEntriesFromDictionary(d2wExtraInfo); } catch (Exception e) { } } } if(context.request() != null) { extraInfo.setObjectForKey(context.request().uri(), "URL"); if(context.request().headers() != null) { NSMutableDictionary<String, Object> headers = new NSMutableDictionary<>(); for (Object key : context.request().headerKeys()) { String value = context.request().headerForKey(key); if(value != null) { headers.setObjectForKey(value, key.toString()); } } extraInfo.setObjectForKey(headers, "Headers"); } } if (context.hasSession()) { if(context.session().statistics() != null) { extraInfo.setObjectForKey(context.session().statistics(), "PreviousPageList"); } extraInfo.setObjectForKey(context.session(), "Session"); } } return extraInfo; } /** * Retrieves the actual cause of an error by unwrapping them as far as possible, * i.e. NSForwardException.originalThrowable(), InvocationTargetException.getTargetException() * or Exception.getCause() are regarded as actual causes. */ public static Throwable originalThrowable(Throwable t) { if (t instanceof InvocationTargetException) { return originalThrowable(((InvocationTargetException)t).getTargetException()); } if (t instanceof NSForwardException) { return originalThrowable(((NSForwardException)t).originalException()); } if (t instanceof JDBCAdaptorException) { JDBCAdaptorException ex = (JDBCAdaptorException)t; if(ex.sqlException() != null) { return originalThrowable(ex.sqlException()); } } if (t instanceof SQLException) { SQLException ex = (SQLException)t; if(ex.getNextException() != null) { return originalThrowable(ex.getNextException()); } } if (t instanceof Exception) { Exception ex = (Exception)t; if(ex.getCause() != null) { return originalThrowable(ex.getCause()); } } return t; } /** * Excecutes the specified command line commands. If envp is not null the * environment variables are set before executing the command. * * @param command * the commands to execute like "ls -la" or "cp /tmp/file1 * /tmp/file2" or "open /Applications/*.app" new String[]{"ls", * "-la"} new String[]{"cp", "/tmp/file1", "/tmp/file2"} new * String[]{"open", "/Applications/*.app"} * * @param envp * a <code>String</code> array which represents the environment * variables like * <code>String[] envp = new String[]{"PATH=/usr/bin:/bin", "CVS_RSH=ssh"}</code>, * can be null * @param dir * a <code>File</code> object representing the working * directory, can be null * * @return the results from the processes that were executed * * @exception IOException * if something went wrong */ public final static Result executeCommandLineCommandWithArgumentsWithEnvVarsInWorkingDir( String[] command, String[] envp, File dir) throws IOException { try { return execute(command, envp, dir, 0); } catch (TimeoutException e) { // this will never happen so we can return null here. return null; } } /** * Excecutes the specified command line commands. If envp is not null the * environment variables are set before executing the command. * * @param commands * the commands to execute, this is an String array with two * dimensions the following commands <br> * "ls -la" or "cp /tmp/file1 /tmp/file2" or "open * /Applications/*.app"<br> * would be as String arrays<br> * * <pre> * new String[] { * new String[] { "ls", "-la" }, * new String[] { "cp", "/tmp/file1", "/tmp/file2" }, * new String[] { "open", "/Applications/*.app" } * } * </pre> * * @param envp * a <code>String</code> array which represents the environment * variables like * <code>String[] envp = new String[]{"PATH=/usr/bin:/bin", "CVS_RSH=ssh"}</code>, * can be null * @param dir * a <code>File</code> object representing the working * directory, can be null * * @return the results from the processes that were executed * * @exception IOException * if something went wrong */ public final static Result[] executeCommandLineCommandsWithEnvVarsInWorkingDir( String[][] commands, String[] envp, File dir) throws IOException { Result[] results = new Result[commands.length]; for (int i = 0; i < commands.length; i++) { try { results[i] = execute(commands[i], envp, dir, 0); } catch (TimeoutException e) { // will never happen return null; } } return results; } /** * Excecutes the specified command line commands. If envp is not null the * environment variables are set before executing the command. This method * supports timeout's. This is quite important because its -always- possible * that a UNIX or WINDOWS process does not return, even with simple shell * scripts. This is due to whatever bugs and hence every invocation of * <code>Process.waitFor()</code> should be observed and stopped if a * certain amount of time is over. * * @param command * the commands to execute, this is an String array with two * dimensions the following commands <br> * "ls -la" or "cp /tmp/file1 /tmp/file2" or "open * /Applications/*.app"<br> * would be as String arrays<br> * * <pre> * new String[] { new String[] { "ls", "-la" }, * new String[] { "cp", "/tmp/file1", "/tmp/file2" }, * new String[] { "open", "/Applications/*.app" } } * </pre> * * @param envp * a <code>String</code> array which represents the environment * variables like * <code>String[] envp = new String[]{"PATH=/usr/bin:/bin", "CVS_RSH=ssh"}</code>, * can be null * @param dir * a <code>File</code> object representing the working * directory, can be null * * @param timeout * a <code>long</code> which can be either <code>0</code> * indicating this method call waits until the process exits or * any <code>long</code> number larger than <code>0</code> * which means if the process does not exit after * <code>timeout</code> milliseconds then this method throws an * ERXTimeoutException * * * @return the results from the processes that were executed * * @exception IOException * if something went wrong */ public final static Result execute(String[] command, String[] envp, File dir, long timeout) throws IOException, TimeoutException { int exitValue = -1; Runtime rt = Runtime.getRuntime(); Process p = null; StreamReader isr = null; StreamReader esr = null; Result result; try { if (log.isDebugEnabled()) { log.debug("Will execute command {}", new NSArray<>(command).componentsJoinedByString(" ")); } if (dir == null && envp == null) { p = rt.exec(command); } else if (dir == null) { p = rt.exec(command, envp); } else if (envp == null) { throw new IllegalArgumentException( "if dir != null then envp must also be != null"); } else { p = rt.exec(command, envp, dir); } // DT: we must read from input and error stream in separate threads // because if the buffer from these streams are full the process // will block! isr = new StreamReader(p.getInputStream()); esr = new StreamReader(p.getErrorStream()); if (timeout > 0) { TimeoutTimerTask task = new TimeoutTimerTask(p); Timer timer = new Timer(); timer.schedule(task, timeout); boolean wasStopped = false; try { p.waitFor(); exitValue = p.exitValue(); } catch (InterruptedException ex) { wasStopped = true; } timer.cancel(); if (task.didTimeout() || wasStopped) { throw new TimeoutException("process didn't exit after " + timeout + " milliseconds"); } } else { // wait for the result of the process try { p.waitFor(); exitValue = p.exitValue(); } catch (InterruptedException ex) { } } } finally { // Getting stream results before freeing process resources to prevent a case // when fast process is destroyed before stream readers read from buffers. if (isr != null) { if (esr != null) { result = new Result(exitValue, isr.getResult(), esr.getResult()); } else { result = new Result(exitValue, isr.getResult(), null); } } else if (esr != null) { result = new Result(exitValue, null, esr.getResult()); } else { result = new Result(exitValue, null, null); } // Checking exceptions after getting results to ensure that stream readers // had already read their buffers by the time of check. if (isr != null && isr.getException() != null) { log.error("input stream reader got exception,\n\tcommand = {}\n\tresult = {}", ERXStringUtilities.toString(command, " "), isr.getResultAsString(), isr.getException()); } if (esr != null && esr.getException() != null) { log.error("error stream reader got exception,\n\tcommand = {}\n\tresult = {}", ERXStringUtilities.toString(command, " "), esr.getResultAsString(), esr.getException()); } freeProcessResources(p); } return result; } /** * Frees all of a resources associated with a given process and then * destroys it. * * @param p * process to destroy */ public static void freeProcessResources(Process p) { if (p != null) { try { if (p.getInputStream() != null) p.getInputStream().close(); } catch (IOException e) { // do nothing here } if (p.getOutputStream() != null) try { p.getOutputStream().close(); } catch (IOException e) { // do nothing here } if (p.getErrorStream() != null) try { p.getErrorStream().close(); } catch (IOException e) { // do nothing here } p.destroy(); } } public static class StreamReader { private byte[] _result = null; private boolean _finished = false; private IOException _iox; public StreamReader(final InputStream is) { Runnable r = new Runnable() { public void run() { ByteArrayOutputStream bout = new ByteArrayOutputStream(); try { int read = -1; byte[] buf = new byte[1024 * 50]; while ((read = is.read(buf)) != -1) { bout.write(buf, 0, read); } _result = bout.toByteArray(); } catch (IOException e) { _iox = e; _result = bout.toByteArray(); } finally { synchronized (StreamReader.this) { _finished = true; StreamReader.this.notifyAll(); } } } }; Thread t = new Thread(r); t.start(); } public byte[] getResult() { synchronized (this) { if(!_finished) { try { StreamReader.this.wait(); } catch (InterruptedException e) { throw NSForwardException._runtimeExceptionForThrowable(e); } } } return _result; } public boolean isFinished() { return _finished; } public IOException getException() { return _iox; } public String getResultAsString() { return getResult() == null ? null : new String(getResult()); } } public static class Result { private byte[] _response, _error; private int _exitValue; public Result(int exitValue, byte[] response, byte[] error) { _exitValue = exitValue; _response = response; _error = error; } public byte[] getResponse() { return _response; } public byte[] getError() { return _error; } public int getExitValue() { return _exitValue; } public String getResponseAsString() { return getResponse() == null ? null : new String(getResponse()); } public String getErrorAsString() { return getError() == null ? null : new String(getError()); } } public static class TimeoutException extends Exception { /** * Do I need to update serialVersionUID? * See section 5.6 <cite>Type Changes Affecting Serialization</cite> on page 51 of the * <a href="http://java.sun.com/j2se/1.4/pdf/serial-spec.pdf">Java Object Serialization Spec</a> */ private static final long serialVersionUID = 1L; public TimeoutException(String string) { super(string); } } public static class TimeoutTimerTask extends TimerTask { private Process _p; private boolean _didTimeout = false; public TimeoutTimerTask(Process p) { _p = p; } public boolean didTimeout() { return _didTimeout; } @Override public void run() { try { _p.exitValue(); } catch (IllegalThreadStateException e) { _didTimeout = true; _p.destroy(); } } } private static NSMutableDictionary<Thread, String> flags; /** * When you have an inner loop and you want to be able to bail out on a stop * request, call this method and you will get interrupted when another thread wants you to. */ public static void checkThreadInterrupt() { if(flags == null) { return; } synchronized (flags) { Thread currentThread = Thread.currentThread(); if (flags.containsKey(currentThread)) { String message = clearThreadInterrupt(currentThread); throw NSForwardException._runtimeExceptionForThrowable(new InterruptedException(message)); } } } /** * Call this to get the thread in question interrupted on the next call to checkThreadInterrupt(). * @param thread * @param message */ public static synchronized void addThreadInterrupt(Thread thread, String message) { if(flags == null) { flags = new NSMutableDictionary<>(); } synchronized (flags) { if (!flags.containsKey(thread)) { log.debug("Adding thread interrupt request: {}", message, new RuntimeException()); flags.setObjectForKey(message, thread); } } } /** * Clear the interrupt flag for the thread. * @param thread */ public static synchronized String clearThreadInterrupt(Thread thread) { if(flags == null) { return null; } synchronized (flags) { return flags.removeObjectForKey(thread); } } }