/FileExtraction/Java_unseen/dlitz_resin/modules/resin/src/com/caucho/boot/WatchdogChildProcess.java
/*
 * Copyright (c) 1998-2011 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.boot;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.caucho.bootjni.JniProcess;
import com.caucho.config.ConfigException;
import com.caucho.env.service.ResinSystem;
import com.caucho.env.shutdown.ExitCode;
import com.caucho.env.thread.ThreadPool;
import com.caucho.hmtp.HmtpLinkWorker;
import com.caucho.lifecycle.Lifecycle;
import com.caucho.loader.EnvironmentClassLoader;
import com.caucho.log.RotateLog;
import com.caucho.log.RotateStream;
import com.caucho.network.listen.TcpSocketLinkListener;
import com.caucho.server.util.CauchoSystem;
import com.caucho.util.Alarm;
import com.caucho.util.L10N;
import com.caucho.vfs.Path;
import com.caucho.vfs.QServerSocket;
import com.caucho.vfs.Vfs;
import com.caucho.vfs.WriteStream;

/**
 * Encapsulation of the process running resin.
 *
 * Each WatchdogProcess instance corresponds to a Resin JVM.  When the
 * JVM exits, the WatchdogProcess will finish.  It is not reused on a
 * restart.
 */
class WatchdogChildProcess
{
  private static final L10N L = new L10N(WatchdogChildProcess.class);
  private static final Logger log
    = Logger.getLogger(WatchdogChildProcess.class.getName());

  private static Boot _jniBoot;

  private final String _id;
  private final ResinSystem _system;
  private final WatchdogChild _watchdog;
  private final Lifecycle _lifecycle = new Lifecycle();

  private WatchdogActor _watchdogActor;

  private Socket _childSocket;
  private AtomicReference<Process> _processRef
    = new AtomicReference<Process>();
  private OutputStream _stdOs;
  private int _pid;

  private long _startTime;
  private int _status = -1;
  private String _exitMessage;
  private ExitCode _exitCode;

  WatchdogChildProcess(String id,
                       ResinSystem system,
                       WatchdogChild watchdog)
  {
    _id = id;
    _system = system;
    _watchdog = watchdog;
  }

  int getPid()
  {
    return _pid;
  }
  
  public String getId()
  {
    return _id;
  }
  
  /**
   * General queries of the Resin instance.
   */
  Serializable queryGet(Serializable payload)
  {
    if (_watchdogActor != null)
      return _watchdogActor.query(payload);
    else
      return null;
  }
  
  /**
   * General message to the Resin instance.
   */
  void message(Serializable payload)
  {
    if (_watchdogActor != null) {
      _watchdogActor.message(payload);
    }
  }

  public int getStatus()
  {
    return _status;
  }
  
  public String getExitMessage()
  {
    return _exitMessage;
  }
  
  public long getUptime()
  {
    long now = Alarm.getCurrentTime();
    long start = _startTime;
    
    if (_startTime > 0)
      return now - start;
    else
      return 0;
  }

  public void run()
  {
    if (! _lifecycle.toActive())
      return;

    EnvironmentClassLoader envLoader
      = EnvironmentClassLoader.create(_system.getClassLoader());
    
    Thread thread = Thread.currentThread();
    
    WriteStream jvmOut = null;
    ServerSocket ss = null;
    Socket s = null;

    try {
      _startTime = Alarm.getCurrentTime();
      
      thread.setContextClassLoader(envLoader);
      envLoader.start();

      ss = new ServerSocket(0, 5, InetAddress.getByName("127.0.0.1"));

      int port = ss.getLocalPort();

      log.warning("Watchdog starting Resin[" + _watchdog.getId() + "]");

      jvmOut = createJvmOut();
      
      Process process = createProcess(port, jvmOut); 
      
      if (process != null) {
        _processRef.compareAndSet(null, process);
        
        if (process instanceof JniProcess)
          _pid = ((JniProcess) process).getPid();
        else
          _pid = 0;

        InputStream stdIs = process.getInputStream();
        _stdOs = process.getOutputStream();

        WatchdogProcessLogThread logThread
          = new WatchdogProcessLogThread(stdIs, jvmOut);

        ThreadPool.getCurrent().start(logThread);

        s = connectToChild(ss);
        
        message(new StartInfoMessage(_watchdog.isRestart(),
                                     _watchdog.getRestartMessage(),
                                     _watchdog.getPreviousExitCode()));

        _status = process.waitFor();

        logStatus(_status);
      }
    } catch (Exception e) {
      log.log(Level.WARNING, e.toString(), e);

      try {
        Thread.sleep(5000);
      } catch (Exception e1) {
      }
    } catch (Throwable e) {
      log.log(Level.WARNING, e.toString(), e);
    } finally {
      _startTime = 0;
      
      if (ss != null) {
        try {
          ss.close();
        } catch (Throwable e) {
        }
      }

      try {
        if (s != null)
          s.close();
      } catch (Throwable e) {
        log.log(Level.FINER, e.toString(), e);
      }

      kill();

      if (jvmOut != null && ! _watchdog.isConsole()) {
        try {
          synchronized (jvmOut) {
            jvmOut.close();
          }
        } catch (Exception e) {
        }
      }

      synchronized (this) {
        if (_status < 0)
          _status = 666;

        notifyAll();
      }

      thread.setContextClassLoader(_system.getClassLoader());
    }
  }
  
  private void logStatus(int status)
  {
    String type = "unknown";
    String code = " (exit code=" + status + ")";
    
    _exitCode = ExitCode.UNKNOWN;
    
    if (status == 0) {
      type = "normal exit";
      
      _exitCode = ExitCode.OK;
    }
    else if (status >= 0 && status < ExitCode.values().length) {
      _exitCode = ExitCode.values()[status];
      
      type = _exitCode.toString();
    }
    else if (status >= 128 && status < 128 + 32) {
      _exitCode = ExitCode.SIGNAL;
      
      switch (status - 128) {
      case 1:
        type = "SIGHUP";
        break;
      case 2:
        type = "SIGINT";
        break;
      case 3:
        type = "SIGQUIT";
        break;
      case 7:
        type = "SIGBUS";
        break;
      case 9:
        type = "SIGKILL";
        break;
      case 11:
        type = "SIGSEGV";
        break;
      case 14:
        type = "SIGALRM";
        break;
      case 15:
        type = "SIGTERM";
        break;
      case 19:
        type = "SIGSTOP";
        break;
      default:
        type = "signal=" + (status - 128);
        break;
      }
      
      code = " (signal=" + (status - 128) + ")";
    }

    String msg = ("Watchdog detected close of "
                  + "Resin[" + _watchdog.getId() + ",pid=" + _pid + "]"
                  + "\n  exit reason: " + type + code);
    
    log.warning(msg);
    
    _exitMessage = msg;
  }

  /**
   * Stops the instance, waiting for the completion.
   */
  void stop()
  {
    _lifecycle.toDestroy();

    if (_watchdogActor != null)
      _watchdogActor.sendShutdown();
  }

  void kill()
  {
    _lifecycle.toDestroy();

    Process process = _processRef.getAndSet(null);

    if (process != null) {
      try {
        process.destroy();
      } catch (Exception e) {
        log.log(Level.FINE, e.toString(), e);
      }
    }

    OutputStream stdOs = _stdOs;
    _stdOs = null;

    if (stdOs != null) {
      try {
        stdOs.close();
      } catch (Throwable e) {
        log.log(Level.FINE, e.toString(), e);
      }
    }

    Socket childSocket = _childSocket;
    _childSocket = null;

    if (childSocket != null) {
      try {
        childSocket.close();
      } catch (Throwable e) {
        log.log(Level.FINE, e.toString(), e);
      }
    }
    
    if (process != null) {
      try {
        process.waitFor();
      } catch (Exception e) {
        log.log(Level.INFO, e.toString(), e);
      }
    }
  }

  void waitForExit()
  {
    synchronized (this) {
      if (_status < 0) {
        try {
          wait(60000);
        } catch (Exception e) {
        }
      }
    }
  }

  /**
   * Waits for a socket connection from the child, returning the socket
   *
   * @param ss TCP ServerSocket from the watchdog for the child to connect to
   */
  private Socket connectToChild(ServerSocket ss)
    throws IOException
  {
    Socket s = null;

    try {
      ss.setSoTimeout(60000);

      for (int i = 0; _lifecycle.isActive() && i < 120 && s == null; i++) {
        try {
          s = ss.accept();
        } catch (SocketTimeoutException e) {
        }
      }

      if (s != null) {
        _childSocket = s;

        startWatchdogActor(s);
      }
    } catch (Exception e) {
      log.log(Level.WARNING, e.toString(), e);
    } finally {
      ss.close();
    }

    return s;
  }

  /**
   * Starts the BAM actor to communicate with the child.
   *
   * @param s the socket to the child.
   */
  private void startWatchdogActor(Socket s)
    throws IOException
  {
    InputStream watchdogIs = s.getInputStream();
    OutputStream watchdogOs = s.getOutputStream();

    _watchdogActor = new WatchdogActor(this);

    HmtpLinkWorker link = new HmtpLinkWorker(_watchdogActor, watchdogIs, watchdogOs);

    try {
      ThreadPool.getCurrent().schedule(link);
    } catch (Exception e) {
      log.log(Level.WARNING, e.toString(), e);
    }
  }

  /**
   * Creates a new Process for the Resin JVM, initializing the environment
   * and passing value to the new process.
   *
   * @param socketPort the watchdog socket port
   * @param out the debug log jvm-default.log
   */
  private Process createProcess(int socketPort,
                                WriteStream out)
    throws IOException
  {
    // watchdog/0210
    // Path pwd = rootDirectory;
    Path chroot = _watchdog.getChroot();
    Path processPwd = _watchdog.getPwd();

    HashMap<String,String> env = buildEnv();

    ArrayList<String> jvmArgs = buildJvmArgs();

    ArrayList<String> resinArgs = buildResinArgs(socketPort);

    addCommandLineArguments(jvmArgs, resinArgs);

    jvmArgs.add("com.caucho.server.resin.Resin");

    jvmArgs.addAll(resinArgs);

    if (_watchdog.isVerbose()) {
      logVerboseArguments(out, jvmArgs);

      logVerboseEnvironment(out, env);
    }

    Boot boot = getJniBoot();
    if (boot != null) {
      boot.clearSaveOnExec();

      ArrayList<QServerSocket> boundSockets = new ArrayList<QServerSocket>();

      try {
        if (_watchdog.getUserName() != null) {
          for (TcpSocketLinkListener port : _watchdog.getPorts()) {
            QServerSocket ss = port.bindForWatchdog();

            if (ss == null)
              continue;

            boundSockets.add(ss);

            if (ss.setSaveOnExec()) {
              jvmArgs.add("-port");
              jvmArgs.add(String.valueOf(ss.getSystemFD()));
              jvmArgs.add(String.valueOf(port.getAddress()));
              jvmArgs.add(String.valueOf(port.getPort()));
            }
            else {
              ss.close();
            }
          }
        }

        String chrootPath = null;

        if (chroot != null)
          chrootPath = chroot.getNativePath();

        Process process = boot.exec(jvmArgs, env,
                                    chrootPath,
                                    processPwd.getNativePath(),
                                    _watchdog.getUserName(),
                                    _watchdog.getGroupName());

        if (process != null) {
          return process;
        }
      } catch (ConfigException e) {
        log.warning(e.getMessage());
      } catch (Throwable e) {
        log.log(Level.WARNING, e.toString(), e);
      } finally {
        for (int i = 0; i < boundSockets.size(); i++) {
          try {
            boundSockets.get(i).close();
          } catch (Throwable e) {
          }
        }
      }
    }

    if (_watchdog.getUserName() != null) {
      if (_watchdog.isConsole())
        throw new ConfigException(L.l("<user-name> requires compiled JNI started with 'start'.  Resin cannot use <user-name> when started as a console process."));
      else
        throw new ConfigException(L.l("<user-name> requires compiled JNI."));
    }

    if (_watchdog.getGroupName() != null) {
      if (_watchdog.isConsole())
        throw new ConfigException(L.l("<group-name> requires compiled JNI started with 'start'.  Resin cannot use <group-name> when started as a console process."));
      else
        throw new ConfigException(L.l("<group-name> requires compiled JNI."));
    }

    ProcessBuilder builder = new ProcessBuilder();

    builder.directory(new File(processPwd.getNativePath()));

    builder.environment().putAll(env);

    builder = builder.command(jvmArgs);

    builder.redirectErrorStream(true);

    return builder.start();
  }

  private HashMap<String,String> buildEnv()
    throws IOException
  {
    HashMap<String,String> env = new HashMap<String,String>();

    env.putAll(System.getenv());
    
    Path resinHome = _watchdog.getResinHome();

    ArrayList<String> classPathList = new ArrayList<String>();
    classPathList.addAll(_watchdog.getJvmClasspath());
    String classPath
      = WatchdogArgs.calculateClassPath(classPathList, resinHome);

    env.put("CLASSPATH", classPath);

    if (_watchdog.is64bit()) {
      WatchdogClient.appendEnvPath(env,
                                   "LD_LIBRARY_PATH",
                                   resinHome.lookup("libexec64").getNativePath());
      WatchdogClient.appendEnvPath(env,
                                   "LD_LIBRARY_PATH_64",
                                   resinHome.lookup("libexec64").getNativePath());
      WatchdogClient.appendEnvPath(env,
                    "DYLD_LIBRARY_PATH",
                    resinHome.lookup("libexec64").getNativePath());
      if (CauchoSystem.isWindows())
        WatchdogClient.appendEnvPath(env,
                      "Path",
                      resinHome.lookup("win64").getNativePath());
    }
    else {
      WatchdogClient.appendEnvPath(env,
                    "LD_LIBRARY_PATH",
                    resinHome.lookup("libexec").getNativePath());
      WatchdogClient.appendEnvPath(env,
                    "DYLD_LIBRARY_PATH",
                    resinHome.lookup("libexec").getNativePath());

      if (CauchoSystem.isWindows())
        WatchdogClient.appendEnvPath(env,
                      "Path",
                      resinHome.lookup("win32").getNativePath());
    }

    return env;
  }

  private ArrayList<String> buildJvmArgs()
  {
    ArrayList<String> jvmArgs = new ArrayList<String>();

    jvmArgs.add(_watchdog.getJavaExe());
    
    boolean isEndorsed = false;

    // user args are first so they're displayed by ps
    for (String arg : _watchdog.getJvmArgs()) {
      if (! arg.startsWith("-Djava.class.path"))
        jvmArgs.add(arg);
      
      if (arg.startsWith("-Djava.endorsed.dirs"))
        isEndorsed = true;
    }
    
    jvmArgs.add("-Dresin.server=" + _id);

    jvmArgs.add("-Djava.util.logging.manager=com.caucho.log.LogManagerImpl");

    // This is needed for JMX to work correctly.
    String systemClassLoader = _watchdog.getSystemClassLoader();
    if (systemClassLoader != null && ! "".equals(systemClassLoader)) {
      jvmArgs.add("-Djava.system.class.loader=" + systemClassLoader);
    }
    
    Path resinHome = _watchdog.getResinHome();
    
    if (! isEndorsed) {
      String endorsed = System.getProperty("java.endorsed.dirs");
      String resinEndorsed = resinHome.getNativePath() + File.separator + "endorsed";
      
      
      if (endorsed != null)
        endorsed = endorsed + File.pathSeparator + resinEndorsed;
      else
        endorsed = resinEndorsed;
      
      jvmArgs.add("-Djava.endorsed.dirs=" + endorsed);
    }
    
    // #2567
    jvmArgs.add("-Djavax.management.builder.initial=com.caucho.jmx.MBeanServerBuilderImpl");
    jvmArgs.add("-Djava.awt.headless=true");

    jvmArgs.add("-Dresin.home=" + resinHome.getFullPath());

    if (! _watchdog.hasXss())
      jvmArgs.add("-Xss1m");

    if (! _watchdog.hasXmx())
      jvmArgs.add("-Xmx256m");
    
    // #4308, #4585
    if (CauchoSystem.isWindows())
      jvmArgs.add("-Xrs");

    String[] argv = _watchdog.getArgv();

    for (int i = 0; i < argv.length; i++) {
      String arg = argv[i];

      if (arg.startsWith("-D") || arg.startsWith("-X")) {
        jvmArgs.add(arg);
      }
      else if (arg.startsWith("-J")) {
        jvmArgs.add(arg.substring(2));
      }
      else if (arg.equals("-d64") || arg.equals("-d32")) {
        jvmArgs.add(arg);
      }
      else if ("--debug-port".equals(arg) || "-debug-port".equals(arg)) {
        jvmArgs.add("-Xdebug");
        jvmArgs.add("-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address="
                    + argv[i + 1]);
        i++;
      }
      else if ("--jmx-port".equals(arg) || "-jmx-port".equals(arg)) {
        jvmArgs.add("-Dcom.sun.management.jmxremote.port=" + argv[i + 1]);
        jvmArgs.add("-Dcom.sun.management.jmxremote.authenticate=false");
        jvmArgs.add("-Dcom.sun.management.jmxremote.ssl=false");
        i++;
      }
    }

    if (! jvmArgs.contains("-d32") && ! jvmArgs.contains("-d64")
        && _watchdog.is64bit() && ! CauchoSystem.isWindows()) {
      jvmArgs.add("-d64");
    }

    if (! jvmArgs.contains("-server")
        && ! jvmArgs.contains("-client")
        && ! CauchoSystem.isWindows()) {
      // #3331, windows can't add -server automatically
      jvmArgs.add("-server");
    }
    
    return jvmArgs;
  }

  private ArrayList<String> buildResinArgs(int socketPort)
  {
    ArrayList<String> resinArgs = new ArrayList<String>();

    Path resinRoot = _watchdog.getResinRoot();

    if (resinRoot != null) {
      resinArgs.add("--root-directory");
      resinArgs.add(resinRoot.getFullPath());
    }
    
    if (_watchdog.getResinConf() != null) {
      resinArgs.add("-conf");
      resinArgs.add(_watchdog.getResinConf().getNativePath());
    }

    resinArgs.add("-socketwait");
    resinArgs.add(String.valueOf(socketPort));

    return resinArgs;
  }

  private void addCommandLineArguments(ArrayList<String> jvmArgs,
                                       ArrayList<String> resinArgs)
  {
    String []argv = _watchdog.getArgv();
    for (int i = 0; i < argv.length; i++) {
      if (argv[i].equals("-conf")) {
        // resin conf handled below
        i++;
      }
      else if (argv[i].startsWith("-Djava.class.path=")) {
        // IBM JDK startup issues
      }
      else if (argv[i].startsWith("-J")) {
        jvmArgs.add(argv[i].substring(2));
      }
      else if (argv[i].startsWith("-Djava.class.path")) {
      }
      else if (argv[i].startsWith("-D") || argv[i].startsWith("-X")) {
      }
      else if (argv[i].equals("-d64") || argv[i].startsWith("-d32")) {
      }
      else
        resinArgs.add(argv[i]);
    }
  }

  private void logVerboseArguments(WriteStream out, ArrayList<String> list)
    throws IOException
  {
    for (int i = 0; i < list.size(); i++) {
      if (i > 0)
        out.print("  ");

      out.print(list.get(i));

      if (i + 1 < list.size())
        out.println(" \\");
      else
        out.println();
    }
  }

  private void logVerboseEnvironment(WriteStream out, Map<String,String> env)
    throws IOException
  {
    for (Map.Entry<String, String> envEntry : env.entrySet()) {
      String key = envEntry.getKey();
      String value = envEntry.getValue();

      if ("CLASSPATH".equals(key)
          || "LD_LIBRARY_PATH".equals(key)
          || "DYLD_LIBRARY_PATH".equals(key)) {
        out.println(key + ": ");

        int len = (key + ": ").length();

        for (String v : value.split("[" + File.pathSeparatorChar + "]")) {
          for (int i = 0; i < len; i++)
            out.print(" ");

          out.println(v);
        }
      }
      else
        out.println("" + key + ": " + value);
    }
  }

  Boot getJniBoot()
  {
    if (_jniBoot != null)
      return _jniBoot.isValid() ? _jniBoot : null;

    try {
      ClassLoader loader = Thread.currentThread().getContextClassLoader();

      Class<?> cl = Class.forName("com.caucho.bootjni.JniBoot", false, loader);
      _jniBoot = (Boot) cl.newInstance();
    } catch (ClassNotFoundException e) {
      log.fine(e.toString());
    } catch (IllegalStateException e) {
      log.fine(e.toString());
    } catch (Throwable e) {
      log.log(Level.FINE, e.toString(), e);
    }

    return _jniBoot != null && _jniBoot.isValid() ? _jniBoot : null;
  }

  //
  // logging utilities
  //

  /**
   * Creates the log/jvm-default.log file where all the Resin log messages
   * go.
   */
  private WriteStream createJvmOut()
    throws IOException
  {
    if (_watchdog.isConsole()) {
      return Vfs.openWrite(System.out);
    }

    Path jvmPath = _watchdog.getLogPath();

    try {
      Path dir = jvmPath.getParent();

      if (! dir.exists()) {
        dir.mkdirs();

        String userName = _watchdog.getUserName();
        if (userName != null)
          dir.changeOwner(userName);

        String groupName = _watchdog.getGroupName();
        if (groupName != null)
          dir.changeGroup(groupName);
      }
    } catch (Exception e) {
      log.log(Level.WARNING, e.toString(), e);
    }

    RotateLog log = new RotateLog();
    log.setPath(jvmPath);
    log.setRolloverSizeBytes(64L * 1024 * 1024);
    
    if (_watchdog.getStdoutLog() != null)
      _watchdog.getStdoutLog().configure(log);

    log.init();
    
    RotateStream rotateStream = log.getRotateStream();

    // _watchdog.getConfig().logInit(rotateStream);
    
    // rotateStream.init();
    return rotateStream.getStream();
  }

  @Override
  public String toString()
  {
    return getClass().getSimpleName() + "[" + _watchdog + "," + _id + "]";
  }

  /**
   * Watchdog thread responsible for writing jvm-default.log by reading the
   * JVM's stdout and copying it to the log.
   */
  class WatchdogProcessLogThread implements Runnable {
    private InputStream _is;
    private WriteStream _out;

    /**
     * @param is the stdout stream from the Resin
     * @param out stream to the log/jvm-default.log file
     */
    WatchdogProcessLogThread(InputStream is, WriteStream out)
    {
      _is = is;
      _out = out;
    }

    @Override
    public void run()
    {
      Thread thread = Thread.currentThread();
      thread.setName("watchdog-process-log-" + _pid + "-" + _id);

      thread.setContextClassLoader(ClassLoader.getSystemClassLoader());
      
      WriteStream out = _out;

      try {
        int len;

        byte []data = new byte[4096];

        while ((len = _is.read(data, 0, data.length)) > 0) {
          out.write(data, 0, len);
          out.flush();
        }
      } catch (Throwable e) {
        log.log(Level.WARNING, e.toString(), e);
      } finally {
        try {
          if (! _watchdog.isConsole()) {
            synchronized (out) {
              out.close();
            }
          }
        } catch (IOException e) {
        }

        kill();
      }
    }
  }
}
