Permalink
Cannot retrieve contributors at this time
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
1415 lines (1271 sloc)
53 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * 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.read(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.read(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 | |
| } |