blob: f1c99d89cd82502d0c7bd409225acfc759723973 [file] [log] [blame]
/*
* 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
}