| /* |
| * 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; |
| |
| import java.io.Closeable; |
| import java.io.DataInput; |
| import java.io.DataInputStream; |
| import java.io.DataOutputStream; |
| import java.io.EOFException; |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InterruptedIOException; |
| import java.io.OutputStream; |
| import java.io.PrintWriter; |
| import java.io.RandomAccessFile; |
| import java.net.InetAddress; |
| import java.net.ServerSocket; |
| import java.net.Socket; |
| import java.net.SocketException; |
| import java.net.UnknownHostException; |
| import java.nio.channels.FileLock; |
| import java.nio.channels.OverlappingFileLockException; |
| import java.security.NoSuchAlgorithmException; |
| import java.security.SecureRandom; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Random; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| import org.openide.util.RequestProcessor; |
| import org.openide.util.Task; |
| |
| /** |
| * Command Line Interface and User Directory Locker support class. |
| * Subclasses may be registered into the system to handle special command-line options. |
| * To be registered, use {@link org.openide.util.lookup.ServiceProvider} |
| * in a JAR file in the startup or dynamic class path (e.g. <samp>lib/ext/</samp> |
| * or <samp>lib/</samp>). |
| * @author Jaroslav Tulach |
| * @since org.netbeans.core/1 1.18 |
| * @see "#32054" |
| * @see <a href="http://openide.netbeans.org/proposals/arch/cli.html">Specification</a> |
| */ |
| public abstract class CLIHandler extends Object { |
| /** lenght of the key used for connecting */ |
| private static final int KEY_LENGTH = 10; |
| private static final byte[] VERSION = { |
| 'N', 'B', 'C', 'L', 'I', 0, 0, 0, 0, 1 |
| }; |
| /** ok reply */ |
| private static final int REPLY_OK = 1; |
| /** sends exit code */ |
| private static final int REPLY_EXIT = 2; |
| /** fail reply */ |
| private static final int REPLY_FAIL = 0; |
| /** the server is active, but cannot compute the value now */ |
| private static final int REPLY_DELAY = 3; |
| |
| /** request to read from input stream */ |
| private static final int REPLY_READ = 10; |
| /** request to write */ |
| private static final int REPLY_WRITE = 11; |
| /** request to find out how much data is available */ |
| private static final int REPLY_AVAILABLE = 12; |
| /** request to write to stderr */ |
| private static final int REPLY_ERROR = 13; |
| /** returns version of the protocol */ |
| private static final int REPLY_VERSION = 14; |
| |
| /** |
| * Used during bootstrap sequence. Should only be used by core, not modules. |
| */ |
| public static final int WHEN_BOOT = 1; |
| /** |
| * Used during later initialization or while NetBeans is up and running. |
| */ |
| public static final int WHEN_INIT = 2; |
| /** Extra set of inits. |
| */ |
| public static final int WHEN_EXTRA = 3; |
| private static final RequestProcessor secureCLIPort = new RequestProcessor("Secure CLI Port"); |
| |
| /** reference to our server. |
| */ |
| private static Server server; |
| |
| /** Testing output of the threads. |
| */ |
| private static final Logger OUTPUT = Logger.getLogger(CLIHandler.class.getName()); |
| |
| private int when; |
| |
| /** |
| * Create a CLI handler and indicate its preferred timing. |
| * @param when when to run the handler: {@link #WHEN_BOOT} or {@link #WHEN_INIT} |
| */ |
| protected CLIHandler(int when) { |
| this.when = when; |
| } |
| |
| /** |
| * Process some set of command-line arguments. |
| * Unrecognized or null arguments should be ignored. |
| * Recognized arguments should be nulled out. |
| * @param args arguments |
| * @return error value or 0 if everything is all right |
| */ |
| protected abstract int cli(Args args); |
| |
| protected static void showHelp(PrintWriter w, Collection<? extends CLIHandler> handlers, int when) { |
| for (CLIHandler h : handlers) { |
| if (when != -1 && when != h.when) { |
| continue; |
| } |
| |
| h.usage(w); |
| } |
| } |
| |
| /** |
| * Print usage information for this handler. |
| * @param w a writer to print to |
| */ |
| protected abstract void usage(PrintWriter w); |
| |
| /** For testing purposes we can block the |
| * algorithm in any place in the initialize method. |
| */ |
| private static void enterState(int state, Integer block) { |
| if (OUTPUT.isLoggable(Level.FINEST)) { |
| synchronized (OUTPUT) { |
| // for easier debugging of CLIHandlerTest |
| OUTPUT.finest("state: " + state + " thread: " + Thread.currentThread()); // NOI18N |
| } |
| } |
| |
| if (block == null) return; |
| |
| |
| synchronized (block) { |
| if (state == block.intValue()) { |
| if (OUTPUT.isLoggable(Level.FINEST)) { |
| OUTPUT.finest(state + " blocked"); // NOI18N |
| } |
| block.notifyAll(); |
| try { |
| block.wait(); |
| } catch (InterruptedException ex) { |
| throw new IllegalStateException(); |
| } |
| } else { |
| if (OUTPUT.isLoggable(Level.FINEST)) { |
| OUTPUT.finest(state + " not blocked"); // NOI18N |
| } |
| } |
| } |
| } |
| |
| private static boolean checkHelp(Args args, Collection<? extends CLIHandler> handlers) { |
| String[] argv = args.getArguments(); |
| for (int i = 0; i < argv.length; i++) { |
| if (argv[i] == null) { |
| continue; |
| } |
| |
| if (argv[i].equals("-?") || argv[i].equals("--help") || argv[i].equals ("-help")) { // NOI18N |
| // disable all logging from standard logger (which prints to stdout) to prevent help mesage disruption |
| Logger.getLogger("").setLevel(Level.OFF); // NOI18N |
| PrintWriter w = new PrintWriter(args.getOutputStream()); |
| showHelp(w, handlers, -1); |
| w.flush(); |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| /** Notification of available handlers. |
| * @return non-zero if one of the handlers fails |
| */ |
| protected static int notifyHandlers(Args args, Collection<? extends CLIHandler> handlers, int when, boolean failOnUnknownOptions, boolean consume) { |
| try { |
| int r = 0; |
| for (CLIHandler h : handlers) { |
| if (h.when != when) continue; |
| |
| r = h.cli(args); |
| //System.err.println("notifyHandlers: exit code " + r + " from " + h); |
| if (r != 0) { |
| return r; |
| } |
| } |
| String[] argv = args.getArguments(); |
| if (failOnUnknownOptions) { |
| argv = args.getArguments(); |
| for (int i = 0; i < argv.length; i++) { |
| if (argv[i] != null) { |
| // Unhandled option. |
| PrintWriter w = new PrintWriter(args.getOutputStream()); |
| w.println("Ignored unknown option: " + argv[i]); // NOI18N |
| |
| // XXX(-ttran) not good, this doesn't show the help for |
| // switches handled by the launcher |
| // |
| //showHelp(w, handlers); |
| |
| w.flush(); |
| return 2; |
| } |
| } |
| } |
| return 0; |
| } finally { |
| args.reset(consume); |
| } |
| } |
| private static int processInitLevelCLI(final Args args, final Collection<? extends CLIHandler> handlers, final boolean failOnUnknownOptions) { |
| return registerFinishInstallation(new Execute() { |
| @Override |
| public int exec() { |
| return notifyHandlers(args, handlers, WHEN_INIT, failOnUnknownOptions, failOnUnknownOptions); |
| } |
| |
| public @Override |
| String toString() { |
| return handlers.toString(); |
| } |
| }); |
| } |
| /** |
| * Represents result of initialization. |
| * @see #initialize(String[], ClassLoader) |
| * @see #initialize(Args, Integer, List) |
| */ |
| static final class Status { |
| public static final int CANNOT_CONNECT = -255; |
| public static final int CANNOT_WRITE = -254; |
| public static final int ALREADY_RUNNING = -253; |
| |
| private final File lockFile; |
| private final int port; |
| private int exitCode; |
| private Task parael; |
| /** |
| * General failure. |
| */ |
| Status() { |
| this(0); |
| } |
| /** |
| * Failure due to a parse problem. |
| * @param c bad status code (not 0) |
| * @see #cli(Args) |
| */ |
| Status(int c) { |
| this(null, 0, c, null); |
| } |
| /** |
| * Some measure of success. |
| * @param l the lock file (not null) |
| * @param p the server port (not 0) |
| * @param c a status code (0 or not) |
| */ |
| Status(File l, int p, int c, Task parael) { |
| lockFile = l; |
| port = p; |
| exitCode = c; |
| this.parael = parael; |
| } |
| |
| private void waitFinished() { |
| if (parael != null) { |
| parael.waitFinished(); |
| } |
| } |
| |
| /** |
| * Get the lock file, if available. |
| * @return the lock file, or null if there is none |
| */ |
| public File getLockFile() { |
| waitFinished(); |
| return lockFile; |
| } |
| /** |
| * Get the server port, if available. |
| * @return a port number for the server, or 0 if there is no port open |
| */ |
| public int getServerPort() { |
| return port; |
| } |
| /** |
| * Get the CLI parse status. |
| * @return 0 for success, some other value for error conditions |
| */ |
| public int getExitCode() { |
| return exitCode; |
| } |
| } |
| |
| private static FileLock tryLock(RandomAccessFile raf) throws IOException { |
| try { |
| return raf.getChannel().tryLock(333L, 1L, false); |
| } catch (OverlappingFileLockException ex) { |
| OUTPUT.log(Level.INFO, "tryLock fails in the same VM", ex); |
| // happens in CLIHandlerTest as it simulates running multiple |
| // instances of the application in the same VM |
| return null; |
| } |
| } |
| |
| |
| /** Initializes the system by creating lock file. |
| * |
| * @param args the command line arguments to recognize |
| * @param classloader to find command CLIHandlers in |
| * @param failOnUnknownOptions if true, fail (status 2) if some options are not recognized (also checks for -? and -help) |
| * @param cleanLockFile removes lock file if it appears to be dead |
| * @return the file to be used as lock file or null parsing of args failed |
| */ |
| static Status initialize( |
| String[] args, |
| InputStream is, |
| OutputStream os, |
| java.io.OutputStream err, |
| MainImpl.BootClassLoader loader, |
| boolean failOnUnknownOptions, |
| boolean cleanLockFile, |
| Runnable runWhenHome |
| ) { |
| String userDir = System.getProperty("netbeans.user.dir"); |
| //System.err.println("nud: " + userDir); |
| if (userDir == null) { |
| userDir = System.getProperty ("user.dir"); |
| } |
| //System.err.println(" ud: " + userDir); |
| return initialize( |
| new Args(args, is, os, err, userDir), |
| (Integer)null, |
| loader.allCLIs(), |
| failOnUnknownOptions, |
| cleanLockFile, |
| runWhenHome |
| ); |
| } |
| |
| /** |
| * What to do later when {@link #finishInitialization} is called. |
| * May remain null, otherwise contains list of Execute |
| */ |
| private static List<Execute> doLater = new ArrayList<Execute> (); |
| static interface Execute { |
| /** @return returns exit code */ |
| public int exec (); |
| } |
| |
| /** Execute this runnable when finishInitialization method is called. |
| */ |
| private static int registerFinishInstallation (Execute run) { |
| boolean runNow; |
| |
| synchronized (CLIHandler.class) { |
| if (doLater != null) { |
| doLater.add (run); |
| runNow = false; |
| } else { |
| runNow = true; |
| } |
| } |
| |
| if (runNow) { |
| return run.exec (); |
| } |
| |
| return 0; |
| } |
| |
| /** |
| * Run any {@link #WHEN_INIT} handlers that were passed to the original command line. |
| * Should be called when the system is up and ready. |
| * Cancels any existing actions, in case it is called twice. |
| * @return the result of executing the handlers |
| */ |
| static int finishInitialization (boolean recreate) { |
| OUTPUT.log(Level.FINER, "finishInitialization {0}", recreate); |
| List<Execute> toRun; |
| synchronized (CLIHandler.class) { |
| toRun = doLater; |
| doLater = recreate ? new ArrayList<Execute> () : null; |
| if (OUTPUT.isLoggable(Level.FINER)) { |
| OUTPUT.finer("Notify: " + toRun); |
| } |
| if (!recreate) { |
| CLIHandler.class.notifyAll (); |
| } |
| } |
| |
| if (toRun != null) { |
| for (Execute r : toRun) { |
| int result = r.exec (); |
| if (result != 0) { |
| return result; |
| } |
| } |
| } |
| return 0; |
| } |
| |
| /** Blocks for a while and waits if the finishInitialization method |
| * was called. |
| * @param timeout ms to wait |
| * @return true if finishInitialization is over |
| */ |
| private static synchronized boolean waitFinishInstallationIsOver (int timeout) { |
| if (doLater != null) { |
| try { |
| CLIHandler.class.wait (timeout); |
| } catch (InterruptedException ex) { |
| // go on, never mind |
| } |
| } |
| return doLater == null; |
| } |
| |
| static void waitSecureCLIOver() { |
| secureCLIPort.post(Task.EMPTY).waitFinished(); |
| } |
| |
| /** Stops the server. |
| */ |
| public static synchronized void stopServer () { |
| Server s = server; |
| if (s != null) { |
| s.stopServer (); |
| } |
| } |
| |
| /** Enhanced search for localhost address that works also behind VPN |
| */ |
| private static InetAddress localHostAddress () throws IOException { |
| java.net.NetworkInterface net = java.net.NetworkInterface.getByName ("lo"); |
| if (net == null || !net.getInetAddresses().hasMoreElements()) { |
| net = java.net.NetworkInterface.getByInetAddress(InetAddress.getByAddress(new byte[] { 127, 0, 0, 1 })); |
| } |
| if (net == null || !net.getInetAddresses().hasMoreElements()) { |
| return InetAddress.getLocalHost(); |
| } |
| else { |
| return net.getInetAddresses().nextElement(); |
| } |
| } |
| |
| /** Initializes the system by creating lock file. |
| * |
| * @param args the command line arguments to recognize |
| * @param block the state we want to block in |
| * @param handlers all handlers to use |
| * @param failOnUnknownOptions if true, fail (status 2) if some options are not recognized (also checks for -? and -help) |
| * @param cleanLockFile removes lock file if it appears to be dead |
| * @param runWhenHome runnable to be executed when netbeans.user property is set |
| * @return a status summary |
| */ |
| static Status initialize( |
| final Args args, final Integer block, |
| final Collection<? extends CLIHandler> handlers, |
| final boolean failOnUnknownOptions, |
| boolean cleanLockFile, |
| Runnable runWhenHome |
| ) { |
| // initial parsing of args |
| { |
| int r = notifyHandlers(args, handlers, WHEN_BOOT, false, failOnUnknownOptions); |
| if (r != 0) { |
| return new Status(r); |
| } |
| } |
| |
| // get the value |
| String home = System.getProperty("netbeans.user"); // NOI18N |
| if (home == null) { |
| home = System.getProperty("user.home"); // NOI18N |
| System.setProperty("netbeans.user", home); // NOI18N |
| } |
| |
| if (/*Places.MEMORY*/"memory".equals(home)) { // NOI18N |
| int execCode = processInitLevelCLI(args, handlers, failOnUnknownOptions); |
| return new Status(execCode); |
| } |
| |
| File lockFile = new File(home, "lock"); // NOI18N |
| |
| for (int i = 0; i < 5; i++) { |
| // try few times to succeed |
| RandomAccessFile raf = null; |
| FileLock lock = null; |
| try { |
| NO_LOCK: if (lockFile.isFile()) { |
| if (!cleanLockFile && !lockFile.canWrite()) { |
| return new Status(Status.CANNOT_WRITE); |
| } |
| raf = new RandomAccessFile(lockFile, "rw"); |
| enterState(2, block); |
| lock = tryLock(raf); |
| if (OUTPUT.isLoggable(Level.FINER)) { |
| OUTPUT.log(Level.FINER, "tryLock on {0} result: {1}", new Object[] { lockFile, lock }); |
| } |
| if (lock != null && lock.isValid()) { |
| // not locked by anyone, |
| enterState(4, block); |
| break NO_LOCK; |
| } |
| enterState(5, block); |
| if (runWhenHome != null) { |
| // notify that we have successfully identified the home property |
| runWhenHome.run(); |
| runWhenHome = null; |
| } |
| throw new IOException("EXISTS"); // NOI18N |
| } |
| |
| if (i == 0 && checkHelp(args, handlers)) { |
| if (runWhenHome != null) { |
| // notify that we have successfully identified the home property |
| runWhenHome.run(); |
| runWhenHome = null; |
| } |
| return new Status(2); |
| } |
| |
| if (lock == null) { |
| lockFile.getParentFile().mkdirs(); |
| try { |
| raf = new RandomAccessFile(lockFile, "rw"); |
| lock = tryLock(raf); |
| OUTPUT.log(Level.FINER, "tryLock when null on {0} result: {1}", new Object[] { lockFile, lock }); |
| if (!cleanLockFile && !lockFile.canWrite()) { |
| return new Status(Status.CANNOT_WRITE); |
| } |
| } catch (IOException ex) { |
| if (!cleanLockFile && !lockFile.getParentFile().canWrite()) { |
| return new Status(Status.CANNOT_WRITE); |
| } |
| throw ex; |
| } |
| } |
| lockFile.deleteOnExit(); |
| assert lock != null : "Null lock on " + lockFile; |
| |
| if (runWhenHome != null) { |
| // notify that we have successfully identified the home property |
| runWhenHome.run (); |
| runWhenHome = null; |
| } |
| |
| secureAccess(lockFile); |
| |
| enterState(10, block); |
| |
| final byte[] arr = new byte[KEY_LENGTH]; |
| new Random().nextBytes(arr); |
| |
| |
| final RandomAccessFile os = raf; |
| raf.seek(0L); |
| |
| int p; |
| if ("false".equals(System.getProperty("org.netbeans.CLIHandler.server"))) { // NOI18N |
| server = null; |
| p = 0; |
| } else { |
| server = new Server(new FileAndLock(os, lock), arr, block, handlers, failOnUnknownOptions); |
| p = server.getLocalPort(); |
| } |
| os.writeInt(p); |
| os.getChannel().force(true); |
| |
| enterState(20, block); |
| |
| Task parael; |
| if (server != null) { |
| parael = secureCLIPort.post(new Runnable() { // NOI18N |
| @Override |
| public void run() { |
| SecureRandom random = null; |
| enterState(95, block); |
| try { |
| random = SecureRandom.getInstance("SHA1PRNG"); // NOI18N |
| } catch (NoSuchAlgorithmException e) { |
| // #36966: IBM JDK doesn't have it. |
| try { |
| random = SecureRandom.getInstance("IBMSecureRandom"); // NOI18N |
| } catch (NoSuchAlgorithmException e2) { |
| // OK, disable server... |
| server.stopServer(); |
| } |
| } |
| |
| enterState(96, block); |
| |
| if (random != null) { |
| random.nextBytes(arr); |
| } |
| |
| enterState(97, block); |
| |
| try { |
| os.write(arr); |
| os.getChannel().force(true); |
| |
| enterState(27,block); |
| // if this turns to be slow due to lookup of getLocalHost |
| // address, it can be done asynchronously as nobody needs |
| // the address in the stream if the server is listening |
| byte[] host; |
| try { |
| if (block != null && block.intValue() == 667) { |
| // this is here to emulate #64004 |
| throw new UnknownHostException("dhcppc0"); // NOI18N |
| } |
| host = InetAddress.getLocalHost().getAddress(); |
| } catch (UnknownHostException unknownHost) { |
| if (!"dhcppc0".equals(unknownHost.getMessage())) { // NOI18N, see above |
| // if we just cannot get the address, we can go on |
| unknownHost.printStackTrace(); |
| } |
| host = new byte[] {127, 0, 0, 1}; |
| } |
| os.write(host); |
| } catch (IOException ex) { |
| ex.printStackTrace(); |
| } |
| try { |
| os.getChannel().force(true); |
| } catch (IOException ex) { |
| // ignore |
| } |
| } |
| }); |
| } else { |
| parael = Task.EMPTY; |
| } |
| |
| int execCode = processInitLevelCLI (args, handlers, failOnUnknownOptions); |
| |
| enterState(0, block); |
| return new Status(lockFile, p, execCode, parael); |
| } catch (IOException ex) { |
| if (!"EXISTS".equals(ex.getMessage())) { // NOI18N |
| ex.printStackTrace(); |
| } |
| // already exists, try to read |
| byte[] key = null; |
| byte[] serverAddress = null; |
| int port = -1; |
| DataInput is = null; |
| try { |
| enterState(21, block); |
| if (OUTPUT.isLoggable(Level.FINER)) { |
| OUTPUT.log(Level.FINER, "Reading lock file {0}", lockFile); // NOI18N |
| } |
| is = raf; |
| port = is.readInt(); |
| if (port == 0) { |
| return new Status(lockFile, 0, Status.ALREADY_RUNNING, Task.EMPTY); |
| } |
| enterState(22, block); |
| key = new byte[KEY_LENGTH]; |
| is.readFully(key); |
| enterState(23, block); |
| byte[] x = new byte[4]; |
| is.readFully(x); |
| enterState(24, block); |
| serverAddress = x; |
| } catch (EOFException eof) { |
| // not yet fully written down |
| if (port != -1) { |
| try { |
| enterState(94, block); |
| try { |
| Socket socket = new Socket(localHostAddress (), port); |
| socket.close(); |
| } catch (Exception ex3) { |
| // socket is not open, remove the file and try once more |
| lockFile.delete(); |
| continue; |
| } |
| // just wait a while |
| Thread.sleep(2000); |
| } catch (InterruptedException inter) { |
| inter.printStackTrace(); |
| } |
| continue; |
| } |
| } catch (IOException ex2) { |
| // ok, try to read it once more |
| enterState(26, block); |
| } finally { |
| if (is instanceof Closeable) { |
| try { |
| ((Closeable)is).close(); |
| } catch (IOException ex3) { |
| // ignore here |
| } |
| } |
| enterState(25, block); |
| } |
| |
| if (key != null && port != -1) { |
| int version = -1; |
| RESTART: for (;;) try { |
| // ok, try to connect |
| enterState(28, block); |
| Socket socket = new Socket(localHostAddress (), port); |
| // wait max of 1s for reply |
| socket.setSoTimeout(5000); |
| DataOutputStream os = new DataOutputStream(socket.getOutputStream()); |
| if (version == -1) { |
| os.write(VERSION); |
| } else { |
| os.write(key); |
| } |
| assert VERSION.length == key.length; |
| os.flush(); |
| |
| enterState(30, block); |
| |
| DataInputStream replyStream = new DataInputStream(socket.getInputStream()); |
| byte[] outputArr = new byte[4096]; |
| |
| COMMUNICATION: for (;;) { |
| enterState(32, block); |
| int reply = replyStream.read(); |
| //System.err.println("reply=" + reply); |
| enterState(34, block); |
| |
| switch (reply) { |
| case REPLY_VERSION: |
| version = replyStream.readInt(); |
| os.write(key); |
| os.flush(); |
| break; |
| case REPLY_FAIL: |
| if (version == -1) { |
| os.close(); |
| replyStream.close(); |
| socket.close(); |
| version = 0; |
| continue RESTART; |
| } |
| enterState(36, block); |
| break COMMUNICATION; |
| case REPLY_OK: |
| enterState(38, block); |
| // write the arguments |
| String[] arr = args.getArguments(); |
| os.writeInt(arr.length); |
| for (int a = 0; a < arr.length; a++) { |
| os.writeUTF(arr[a]); |
| } |
| os.writeUTF (args.getCurrentDirectory().toString()); |
| os.flush(); |
| break; |
| case REPLY_EXIT: |
| int exitCode = replyStream.readInt(); |
| if (exitCode == 0) { |
| // to signal end of the world |
| exitCode = -1; |
| } |
| |
| os.close(); |
| replyStream.close(); |
| |
| enterState(0, block); |
| return new Status(lockFile, port, exitCode, null); |
| case REPLY_READ: { |
| enterState(42, block); |
| int howMuch = replyStream.readInt(); |
| if (howMuch > outputArr.length) { |
| outputArr = new byte[howMuch]; |
| } |
| int really = args.getInputStream().read(outputArr, 0, howMuch); |
| if (version >= 1) { |
| os.writeInt(really); |
| } else { |
| os.write(really); |
| } |
| if (really > 0) { |
| os.write(outputArr, 0, really); |
| } |
| os.flush(); |
| break; |
| } |
| case REPLY_WRITE: { |
| enterState(44, block); |
| int howMuch = replyStream.readInt(); |
| if (howMuch > outputArr.length) { |
| outputArr = new byte[howMuch]; |
| } |
| replyStream.readFully(outputArr, 0, howMuch); |
| args.getOutputStream().write(outputArr, 0, howMuch); |
| break; |
| } |
| case REPLY_ERROR: { |
| enterState(45, block); |
| int howMuch = replyStream.readInt(); |
| if (howMuch > outputArr.length) { |
| outputArr = new byte[howMuch]; |
| } |
| replyStream.readFully(outputArr, 0, howMuch); |
| args.getErrorStream().write(outputArr, 0, howMuch); |
| break; |
| } |
| case REPLY_AVAILABLE: |
| enterState(46, block); |
| os.writeInt(args.getInputStream().available()); |
| os.flush(); |
| break; |
| case REPLY_DELAY: |
| enterState(47, block); |
| // ok, try once more |
| break; |
| case -1: |
| enterState(48, block); |
| // EOF. Why does this happen? |
| break COMMUNICATION; |
| default: |
| enterState(49, block); |
| assert false : reply; |
| } |
| } |
| |
| // connection ok, butlockFile secret key not recognized |
| // delete the lock file |
| break RESTART; |
| } catch (java.net.SocketTimeoutException ex2) { |
| // connection failed, the port is dead |
| enterState(33, block); |
| break RESTART; |
| } catch (java.net.ConnectException ex2) { |
| // connection failed, the port is dead |
| enterState(33, block); |
| break RESTART; |
| } catch (IOException ex2) { |
| // some strange exception |
| ex2.printStackTrace(); |
| enterState(33, block); |
| break RESTART; |
| } |
| |
| boolean isSameHost = true; |
| if (serverAddress != null) { |
| try { |
| isSameHost = Arrays.equals(InetAddress.getLocalHost().getAddress(), serverAddress); |
| } catch (UnknownHostException ex5) { |
| // ok, we will not try to connect |
| enterState(999, block); |
| } |
| } |
| |
| if (lock == null) { |
| // process exists (we cannot lock the file), but does not respond |
| return new Status (Status.CANNOT_CONNECT); |
| } |
| |
| if (cleanLockFile || isSameHost) { |
| // remove the file and try once more |
| lockFile.delete(); |
| } else { |
| return new Status (Status.CANNOT_CONNECT); |
| } |
| } |
| } |
| |
| try { |
| enterState(83, block); |
| Thread.sleep((int)(Math.random() * 1000.00)); |
| enterState(85, block); |
| } catch (InterruptedException ex) { |
| // means nothing |
| } |
| } |
| |
| // failure |
| return new Status(); |
| } |
| |
| /** Make the file readable just to its owner. |
| */ |
| private static void secureAccess(final File file) throws IOException { |
| file.setReadable(false, false); |
| file.setReadable(true, true); |
| } |
| |
| /** Class that represents available arguments to the CLI |
| * handlers. |
| */ |
| public static final class Args extends Object { |
| private String[] args; |
| private final String[] argsBackup; |
| private InputStream is; |
| private OutputStream os; |
| private OutputStream err; |
| private File currentDir; |
| private boolean closed; |
| |
| Args(String[] args, InputStream is, OutputStream os, java.io.OutputStream err, String currentDir) { |
| argsBackup = args; |
| reset(false); |
| this.is = is; |
| this.os = os; |
| this.err = err; |
| this.currentDir = new File (currentDir); |
| } |
| |
| /** |
| * Restore the arguments list to a clean state. |
| * If not consuming arguments, it is just set to the original list. |
| * If consuming arguments, any nulled-out arguments are removed from the list. |
| */ |
| void reset(boolean consume) { |
| if (consume) { |
| String[] a = args; |
| if (a == null) { |
| a = argsBackup; |
| } |
| List<String> l = new ArrayList<String>(Arrays.asList(a)); |
| l.removeAll(Collections.singleton(null)); |
| args = l.toArray(new String[l.size()]); |
| } else { |
| args = argsBackup.clone(); |
| } |
| } |
| |
| /** Closes the connection. |
| */ |
| final void close() { |
| closed = true; |
| } |
| |
| /** |
| * Get the command-line arguments. |
| * You may not modify the returned array except to set some elements |
| * to null as you recognize them. |
| * @return array of string arguments, may contain nulls |
| */ |
| public String[] getArguments() { |
| return args; |
| } |
| |
| /** |
| * Get an output stream to which data may be sent. |
| * @return stream to write to |
| */ |
| public OutputStream getOutputStream() { |
| return os; |
| } |
| |
| /** Access to error stream. |
| * @return the stream to write error messages to |
| */ |
| public OutputStream getErrorStream() { |
| return err; |
| } |
| |
| public File getCurrentDirectory () { |
| return currentDir; |
| } |
| |
| /** |
| * Get an input stream that may supply additional data. |
| * @return stream to read from |
| */ |
| public InputStream getInputStream() { |
| return is; |
| } |
| |
| /** Is open? True if the connection is still alive. Can be |
| * used with long running computations to find out if the |
| * consumer of the output has not been interrupted. |
| * |
| * @return true if the connection is still alive |
| */ |
| public boolean isOpen() { |
| return !closed; |
| } |
| |
| } // end of Args |
| |
| /** Server that creates local socket and communicates with it. |
| */ |
| private static final class Server extends Thread { |
| private Closeable unlock; |
| private byte[] key; |
| private volatile ServerSocket socket; |
| private Integer block; |
| private Collection<? extends CLIHandler> handlers; |
| private Socket work; |
| private static volatile int counter; |
| private final boolean failOnUnknownOptions; |
| |
| private static long lastReply; |
| /** by default wait 100ms before sending a REPLY_FAIL message */ |
| private static long failDelay = 100; |
| |
| public Server(Closeable lock, byte[] key, Integer block, Collection<? extends CLIHandler> handlers, boolean failOnUnknownOptions) throws IOException { |
| super("CLI Requests Server"); // NOI18N |
| this.unlock = lock; |
| this.key = key; |
| this.setDaemon(true); |
| this.block = block; |
| this.handlers = handlers; |
| this.failOnUnknownOptions = failOnUnknownOptions; |
| |
| socket = new ServerSocket(0, 50, localHostAddress()); |
| start(); |
| } |
| |
| public Server(Socket request, byte[] key, Integer block, Collection<? extends CLIHandler> handlers, boolean failOnUnknownOptions) throws IOException { |
| super("CLI Handler Thread Handler: " + ++counter); // NOI18N |
| this.key = key; |
| this.setDaemon(true); |
| this.block = block; |
| this.handlers = handlers; |
| this.work = request; |
| this.failOnUnknownOptions = failOnUnknownOptions; |
| |
| start(); |
| } |
| |
| public int getLocalPort() { |
| return socket.getLocalPort(); |
| } |
| |
| public @Override void run() { |
| if (work != null) { |
| // I am a worker not listener server |
| try { |
| handleConnect(work); |
| } catch (IOException ex) { |
| OUTPUT.log(Level.INFO, null, ex); |
| } |
| return; |
| } |
| |
| ServerSocket toClose = socket; |
| if (toClose == null) { |
| return; |
| } |
| |
| // by default wait 100ms after exception from socket.accept() |
| long acceptFailDelay = 100; |
| |
| while (socket != null) { |
| try { |
| enterState(65, block); |
| Socket s = socket.accept(); |
| if (socket == null) { |
| enterState(66, block); |
| s.getOutputStream().write(REPLY_FAIL); |
| enterState(67, block); |
| s.close(); |
| continue; |
| } |
| acceptFailDelay = 100; |
| |
| // spans new request handler |
| new Server(s, key, block, handlers, failOnUnknownOptions); |
| // and re-run the while loop |
| continue; |
| } catch (InterruptedIOException ex) { |
| if (socket != null) { |
| ex.printStackTrace(); |
| } |
| // otherwise ignore, we've just been asked by the stopServer |
| // to stop |
| } catch (java.net.SocketException ex) { |
| if (socket != null) { |
| ex.printStackTrace(); |
| } |
| } catch (IOException ex) { |
| ex.printStackTrace(); |
| } |
| // common error handling below |
| // socket.accept() failed with exception, wait for some time |
| // to prevent messages.log and memory overflow caused by a large |
| // number of ex.printStackTrace() invocations |
| if (socket != null) { |
| try { |
| Thread.sleep(acceptFailDelay); |
| } catch (InterruptedException ex) { |
| ex.printStackTrace(); |
| } |
| acceptFailDelay *= 2; |
| } |
| } |
| |
| try { |
| toClose.close(); |
| } catch (IOException ex) { |
| ex.printStackTrace(); |
| } |
| } |
| |
| final void stopServer () { |
| OUTPUT.log(Level.FINER, "stopServer, unlock: {0}", unlock); |
| if (unlock != null) { |
| try { |
| unlock.close(); |
| } catch (IOException ex) { |
| OUTPUT.log(Level.WARNING, "Cannot unlock {0}", ex.getMessage()); |
| } |
| } |
| socket = null; |
| // interrupts the listening server |
| interrupt(); |
| } |
| |
| private void handleConnect(Socket s) throws IOException { |
| int requestedVersion; |
| byte[] check = new byte[key.length]; |
| DataInputStream is = new DataInputStream(s.getInputStream()); |
| |
| enterState(70, block); |
| |
| is.readFully(check); |
| |
| final DataOutputStream os = new DataOutputStream(s.getOutputStream()); |
| |
| boolean match = true; |
| for (int i = 0; i < VERSION.length - 1; i++) { |
| if (VERSION[i] != check[i]) { |
| match = false; |
| } |
| } |
| if (match) { |
| requestedVersion = check[VERSION.length - 1]; |
| os.write(REPLY_VERSION); |
| os.writeInt(VERSION[VERSION.length - 1]); |
| os.flush(); |
| is.readFully(check); |
| } else { |
| requestedVersion = 0; |
| } |
| |
| enterState(90, block); |
| |
| if (Arrays.equals(check, key)) { |
| while (!waitFinishInstallationIsOver (2000)) { |
| os.write (REPLY_DELAY); |
| os.flush (); |
| } |
| |
| enterState(93, block); |
| os.write(REPLY_OK); |
| os.flush(); |
| |
| // continue with arguments |
| int numberOfArguments = is.readInt(); |
| String[] args = new String[numberOfArguments]; |
| for (int i = 0; i < args.length; i++) { |
| args[i] = is.readUTF(); |
| } |
| final String currentDir = is.readUTF (); |
| |
| final Args arguments = new Args( |
| args, |
| new IS(is, os, requestedVersion), |
| new OS(os, REPLY_WRITE), |
| new OS(os, REPLY_ERROR), |
| currentDir |
| ); |
| |
| class ComputingAndNotifying extends Thread { |
| public int res; |
| public boolean finished; |
| |
| public ComputingAndNotifying () { |
| super ("Computes values in handlers"); |
| } |
| |
| public @Override void run() { |
| try { |
| if (checkHelp(arguments, handlers)) { |
| res = 2; |
| } else { |
| res = notifyHandlers (arguments, handlers, WHEN_INIT, failOnUnknownOptions, false); |
| } |
| |
| if (res == 0) { |
| enterState (98, block); |
| } else { |
| enterState (99, block); |
| } |
| } finally { |
| synchronized (this) { |
| finished = true; |
| notifyAll (); |
| } |
| } |
| } |
| |
| public synchronized void waitForResultAndNotifyOthers () { |
| // execute the handlers in another thread |
| start (); |
| while (!finished) { |
| try { |
| wait (1000); |
| os.write (REPLY_DELAY); |
| os.flush (); |
| } catch (SocketException ex) { |
| if (isClosedSocket(ex)) { // NOI18N |
| // mark the arguments killed |
| arguments.close(); |
| // interrupt this thread |
| interrupt(); |
| } else { |
| ex.printStackTrace(); |
| } |
| } catch (InterruptedException ex) { |
| ex.printStackTrace(); |
| } catch (IOException ex) { |
| ex.printStackTrace(); |
| } |
| } |
| } |
| } |
| ComputingAndNotifying r = new ComputingAndNotifying (); |
| r.waitForResultAndNotifyOthers (); |
| try { |
| os.write(REPLY_EXIT); |
| os.writeInt(r.res); |
| } catch (SocketException ex) { |
| if (isClosedSocket(ex)) { // NOI18N |
| // mark the arguments killed |
| arguments.close(); |
| // interrupt r thread |
| r.interrupt(); |
| } else { |
| throw ex; |
| } |
| } |
| } else { |
| enterState(103, block); |
| long toWait = lastReply + failDelay - System.currentTimeMillis(); |
| if (toWait > 0) { |
| try { |
| Thread.sleep(toWait); |
| } catch (InterruptedException ex) { |
| ex.printStackTrace(); |
| } |
| failDelay *= 2; |
| } else { |
| failDelay = 100; |
| } |
| lastReply = System.currentTimeMillis(); |
| os.write(REPLY_FAIL); |
| } |
| |
| |
| enterState(120, block); |
| |
| os.close(); |
| is.close(); |
| } |
| |
| /** A method to find out on various systems whether an exception is |
| * a signal of closed socket, especially if the peer is killed or exited. |
| * @param ex the exception to investigate |
| */ |
| static final boolean isClosedSocket(SocketException ex) { |
| if (ex.getMessage().equals("Broken pipe")) { // NOI18N |
| return true; |
| } |
| if (ex.getMessage().startsWith("Connection reset by peer")) { // NOI18N |
| return true; |
| } |
| |
| return false; |
| } |
| |
| private static final class IS extends InputStream { |
| private final DataInputStream is; |
| private final DataOutputStream os; |
| private final int requestedVersion; |
| |
| public IS(DataInputStream is, DataOutputStream os, int version) { |
| this.is = is; |
| this.os = os; |
| this.requestedVersion = version; |
| } |
| |
| public int read() throws IOException { |
| byte[] arr = new byte[1]; |
| if (read(arr) == 1) { |
| return arr[0]; |
| } else { |
| return -1; |
| } |
| } |
| |
| public @Override void close() throws IOException { |
| super.close(); |
| } |
| |
| public @Override int available() throws IOException { |
| // ask for data |
| os.write(REPLY_AVAILABLE); |
| os.flush(); |
| // read provided data |
| return is.readInt(); |
| } |
| |
| public @Override int read(byte[] b) throws IOException { |
| return read(b, 0, b.length); |
| } |
| |
| public @Override int read(byte[] b, int off, int len) throws IOException { |
| for (;;) { |
| // ask for data |
| os.write(REPLY_READ); |
| os.writeInt(len); |
| os.flush(); |
| // read provided data |
| int really = requestedVersion >= 1 ? is.readInt() : is.read (); |
| if (really > 0) { |
| return is.read(b, off, really); |
| } else { |
| if (really < 0) { |
| return really; |
| } |
| // can't return zero read bytes, need another round |
| } |
| } |
| } |
| |
| } // end of IS |
| |
| private static final class OS extends OutputStream { |
| private DataOutputStream os; |
| private int type; |
| |
| public OS(DataOutputStream os, int type) { |
| this.os = os; |
| this.type = type; |
| } |
| |
| public void write(int b) throws IOException { |
| byte[] arr = { (byte)b }; |
| write(arr); |
| } |
| |
| public @Override void write(byte[] b) throws IOException { |
| write(b, 0, b.length); |
| } |
| |
| public @Override void close() throws IOException { |
| super.close(); |
| } |
| |
| public @Override void flush() throws IOException { |
| os.flush(); |
| } |
| |
| public @Override void write(byte[] b, int off, int len) throws IOException { |
| os.write(type); |
| os.writeInt(len); |
| os.write(b, off, len); |
| } |
| |
| } // end of OS |
| |
| } // end of Server |
| |
| private static final class FileAndLock implements Closeable { |
| private final Closeable file; |
| private final FileLock lock; |
| |
| public FileAndLock(Closeable file, FileLock lock) { |
| this.file = file; |
| this.lock = lock; |
| } |
| |
| @Override |
| public void close() throws IOException { |
| if (lock != null) { |
| lock.release(); |
| } |
| if (file != null) { |
| file.close(); |
| } |
| } |
| } // end of FileAndLock |
| } |