001    /*
002     * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003     *
004     * Copyright (c) 1997-2011 Oracle and/or its affiliates. All rights reserved.
005     *
006     * The contents of this file are subject to the terms of either the GNU
007     * General Public License Version 2 only ("GPL") or the Common Development
008     * and Distribution License("CDDL") (collectively, the "License").  You
009     * may not use this file except in compliance with the License.  You can
010     * obtain a copy of the License at
011     * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
012     * or packager/legal/LICENSE.txt.  See the License for the specific
013     * language governing permissions and limitations under the License.
014     *
015     * When distributing the software, include this License Header Notice in each
016     * file and include the License file at packager/legal/LICENSE.txt.
017     *
018     * GPL Classpath Exception:
019     * Oracle designates this particular file as subject to the "Classpath"
020     * exception as provided by Oracle in the GPL Version 2 section of the License
021     * file that accompanied this code.
022     *
023     * Modifications:
024     * If applicable, add the following below the License Header, with the fields
025     * enclosed by brackets [] replaced by your own identifying information:
026     * "Portions Copyright [year] [name of copyright owner]"
027     *
028     * Contributor(s):
029     * If you wish your version of this file to be governed by only the CDDL or
030     * only the GPL Version 2, indicate your decision by adding "[Contributor]
031     * elects to include this software in this distribution under the [CDDL or GPL
032     * Version 2] license."  If you don't indicate a single choice of license, a
033     * recipient has the option to distribute your version of this file under
034     * either the CDDL, the GPL Version 2 or to extend the choice of license to
035     * its licensees as provided above.  However, if you add GPL Version 2 code
036     * and therefore, elected the GPL Version 2 license, then the option applies
037     * only if the new code is made subject to such option by the copyright
038     * holder.
039     */
040    package com.sun.enterprise.admin.cli;
041    
042    import com.sun.enterprise.util.OS;
043    import java.io.File;
044    import java.io.IOException;
045    import java.util.*;
046    import java.util.logging.Level;
047    import java.util.logging.Logger;
048    import com.sun.enterprise.admin.launcher.GFLauncher;
049    import com.sun.enterprise.admin.launcher.GFLauncherException;
050    import com.sun.enterprise.admin.launcher.GFLauncherInfo;
051    import com.sun.enterprise.universal.i18n.LocalStringsImpl;
052    import com.sun.enterprise.universal.process.ProcessStreamDrainer;
053    import com.sun.enterprise.universal.process.ProcessUtils;
054    import com.sun.enterprise.util.HostAndPort;
055    import com.sun.enterprise.util.io.ServerDirs;
056    import com.sun.enterprise.util.net.NetUtils;
057    import org.glassfish.api.admin.CommandException;
058    import static com.sun.enterprise.util.StringUtils.ok;
059    import static com.sun.enterprise.admin.cli.CLIConstants.WAIT_FOR_DAS_TIME_MS;
060    
061    /**
062     * Java does not allow multiple inheritance.  Both StartDomainCommand and
063     * StartInstanceCommand have common code but they are already in a different
064     * hierarchy of classes.  The first common baseclass is too far away -- e.g.
065     * no "launcher" variable, etc.
066     *
067     * Instead -- put common code in here and call it as common utilities
068     * This class is designed to be thread-safe and IMMUTABLE
069     * @author bnevins
070     */
071    public class StartServerHelper {
072        public StartServerHelper(Logger logger0, boolean terse0,
073                ServerDirs serverDirs0, GFLauncher launcher0,
074                String masterPassword0, boolean debug0) {
075            logger = logger0;
076            terse = terse0;
077            launcher = launcher0;
078            info = launcher.getInfo();
079    
080            if (info.isDomain())
081                serverOrDomainName = info.getDomainName();
082            else
083                serverOrDomainName = info.getInstanceName();
084    
085            addresses = info.getAdminAddresses();
086            serverDirs = serverDirs0;
087            pidFile = serverDirs.getPidFile();
088            masterPassword = masterPassword0;
089            debug = debug0;
090            // it will be < 0 if both --debug is false and debug-enabled=false in jvm-config
091            debugPort = launcher.getDebugPort();
092            isDebugSuspend = launcher.isDebugSuspend();
093    
094            if (isDebugSuspend && debugPort >= 0) {
095                logger.info(strings.get("ServerStart.DebuggerSuspendedMessage", "" + debugPort));
096            }
097        }
098    
099        // TODO check the i18n messages
100        public void waitForServer() throws CommandException {
101            long startWait = System.currentTimeMillis();
102            if (!terse) {
103                // use stdout because logger always appends a newline
104                System.out.print(strings.get("WaitServer", serverOrDomainName) + " ");
105            }
106    
107            boolean alive = false;
108            int count = 0;
109    
110            pinged:
111            while (!timedOut(startWait)) {
112                if (pidFile != null) {
113                    logger.finer("Check for pid file: " + pidFile);
114                    if (pidFile.exists()) {
115                        alive = true;
116                        break pinged;
117                    }
118                }
119                else {
120                    // first, see if the admin port is responding
121                    // if it is, the DAS is up
122                    for (HostAndPort addr : addresses) {
123                        if (NetUtils.isRunning(addr.getHost(), addr.getPort())) {
124                            alive = true;
125                            break pinged;
126                        }
127                    }
128                }
129    
130                // check to make sure the DAS process is still running
131                // if it isn't, startup failed
132                try {
133                    Process p = launcher.getProcess();
134                    int exitCode = p.exitValue();
135                    // uh oh, DAS died
136                    String sname;
137    
138                    if (info.isDomain())
139                        sname = "domain " + info.getDomainName();
140                    else
141                        sname = "instance " + info.getInstanceName();
142    
143                    ProcessStreamDrainer psd = launcher.getProcessStreamDrainer();
144                    String output = psd.getOutErrString();
145                    if (ok(output))
146                        throw new CommandException(strings.get("serverDiedOutput",
147                                sname, exitCode, output));
148                    else
149                        throw new CommandException(strings.get("serverDied",
150                                sname, exitCode));
151                }
152                catch (GFLauncherException ex) {
153                    // should never happen
154                }
155                catch (IllegalThreadStateException ex) {
156                    // process is still alive
157                }
158    
159                // wait before checking again
160                try {
161                    Thread.sleep(100);
162                    if (!terse && count++ % 10 == 0)
163                        System.out.print(".");
164                }
165                catch (InterruptedException ex) {
166                    // don't care
167                }
168            }
169    
170            if (!terse)
171                System.out.println();
172    
173            if (!alive) {
174                String msg;
175                String time = "" + (WAIT_FOR_DAS_TIME_MS / 1000);
176                if (info.isDomain())
177                    msg = strings.get("serverNoStart", strings.get("DAS"),
178                            info.getDomainName(), time);
179                else
180                    msg = strings.get("serverNoStart", strings.get("INSTANCE"),
181                            info.getInstanceName(), time);
182    
183                throw new CommandException(msg);
184            }
185        }
186    
187        /**
188         * Run a series of commands to prepare for a launch.
189         * @return false if there was a problem.
190         */
191        public boolean prepareForLaunch() throws CommandException {
192    
193            waitForParentToDie();
194            setSecurity();
195    
196            if (checkPorts() == false)
197                return false;
198    
199            deletePidFile();
200    
201            return true;
202        }
203    
204        public void report() {
205            String logfile;
206    
207            try {
208                logfile = launcher.getLogFilename();
209            }
210            catch (GFLauncherException ex) {
211                logfile = "UNKNOWN";        // should never happen
212            }
213    
214            int adminPort = -1;
215            String adminPortString = "-1";
216    
217            try {
218                if (addresses != null && addresses.size() > 0)
219                    adminPort = addresses.get(0).getPort();
220                // To avoid having the logger do this: port = 4,848
221                // so we do the conversion to a string ourselves
222                adminPortString = "" + adminPort;
223            }
224            catch (Exception e) {
225                //ignore
226            }
227    
228            logger.info(strings.get(
229                    "ServerStart.SuccessMessage",
230                    info.isDomain() ? "domain " : "instance",
231                    serverDirs.getServerName(),
232                    serverDirs.getServerDir(),
233                    logfile,
234                    adminPortString));
235    
236            if (debugPort >= 0) {
237                logger.info(strings.get("ServerStart.DebuggerMessage", "" + debugPort));
238            }
239        }
240    
241        /**
242         * If the parent is a GF server -- then wait for it to die.  This is part
243         * of the Client-Server Restart Dance!
244         * THe dying server called us with the system property AS_RESTART set to its pid
245         * @throws CommandException if we timeout waiting for the parent to die or
246         *  if the admin ports never free up
247         */
248        private void waitForParentToDie() throws CommandException {
249            // we also come here with just a regular start in which case there is
250            // no parent, and the System Property is NOT set to anything...
251            String pids = System.getProperty(CLIConstants.RESTART_FLAG_PARENT_PID);
252    
253            if (!ok(pids))
254                return;
255    
256            int pid = -1;
257    
258            try {
259                pid = Integer.parseInt(pids);
260            }
261            catch (Exception e) {
262                pid = -1;
263            }
264            waitForParentDeath(pid);
265        }
266    
267        private boolean checkPorts() {
268            String err = adminPortInUse();
269    
270            if (err != null) {
271                logger.warning(err);
272                return false;
273            }
274    
275            return true;
276        }
277    
278        private void deletePidFile() {
279            String msg = serverDirs.deletePidFile();
280    
281            if (msg != null)
282                logger.finer(msg);
283        }
284    
285        private void setSecurity() {
286            info.addSecurityToken(CLIConstants.MASTER_PASSWORD, masterPassword);
287        }
288    
289        private String adminPortInUse() {
290            return adminPortInUse(info.getAdminAddresses());
291        }
292    
293        private String adminPortInUse(List<HostAndPort> adminAddresses) {
294            // it returns a String for logging --- if desired
295            for (HostAndPort addr : adminAddresses) {
296                if (!NetUtils.isPortFree(addr.getHost(), addr.getPort()))
297                    return strings.get("ServerRunning",
298                            Integer.toString(addr.getPort()));
299            }
300    
301            return null;
302        }
303    
304        // use the pid we received from the parent server and platform specific tools
305        // to see FOR SURE when the entire JVM process is gone.  This solves
306        // potential niggling bugs.
307        private void waitForParentDeath(int pid) throws CommandException {
308            if (pid < 0) {
309                // can not happen.  (Famous Last Words!)
310                new ParentDeathWaiterPureJava();
311                return;
312            }
313    
314            long start = System.currentTimeMillis();
315            try {
316                do {
317                    Boolean b = ProcessUtils.isProcessRunning(pid);
318                    if (b == null) {
319                        // this means we were unable to find out from the OS if the process
320                        // is running or not
321                        debugMessage("ProcessUtils.isProcessRunning(" + pid + ") "
322                                + "returned null which means we can't get process "
323                                + "info on this platform.");
324    
325                        new ParentDeathWaiterPureJava();
326                        return;
327                    }
328                    if (b.booleanValue() == false) {
329                        debugMessage("Parent process (" + pid + ") is dead.");
330                        return;
331                    }
332                    // else parent is still breathing...
333                    debugMessage("Wait one more second for parent to die...");
334                    Thread.sleep(1000);
335                }while (!timedOut(start, CLIConstants.DEATH_TIMEOUT_MS));
336    
337            }
338            catch (Exception e) {
339                // fall through.  Normal returns are in the block above
340            }
341    
342            // abnormal return path
343            throw new CommandException(
344                    strings.get("deathwait_timeout", CLIConstants.DEATH_TIMEOUT_MS));
345        }
346    
347        private static boolean timedOut(long startTime) {
348            return timedOut(startTime, WAIT_FOR_DAS_TIME_MS);
349        }
350    
351        private static boolean timedOut(long startTime, long span) {
352            return (System.currentTimeMillis() - startTime) > span;
353        }
354    
355        private static void debugMessage(String s) {
356            // very difficult to see output from this process when part of restart-domain.
357            // Normally there is no console.
358            // There are **three** JVMs in a restart -- old server, new server, cli
359            // we will not even see AS_DEBUG!
360            if (DEBUG_MESSAGES_ON)
361                CLIUtil.writeCommandToDebugLog(new String[]{"DEBUG MESSAGE FROM RESTART JVM", s}, 99999);
362        }
363        private final boolean terse;
364        private final GFLauncher launcher;
365        private final Logger logger;
366        private final File pidFile;
367        private final GFLauncherInfo info;
368        private final List<HostAndPort> addresses;
369        private final ServerDirs serverDirs;
370        private final String masterPassword;
371        private final String serverOrDomainName;
372        private final boolean debug;
373        private final int debugPort;
374        private final boolean isDebugSuspend;
375        // only set when actively trouble-shooting or investigating...
376        private final static boolean DEBUG_MESSAGES_ON = false;
377        private static final LocalStringsImpl strings =
378                new LocalStringsImpl(StartServerHelper.class);
379    
380        /**
381         * bnevins
382         * the restart flag is set by the RestartDomain command in the local
383         * server.  The dying server has started a new JVM process and is
384         * running this code.  Our official parent process is the dying server.
385         * The ParentDeathWaiterPureJava waits for the parent process to disappear.
386         * see RestartDomainCommand in core/kernel for more details
387         */
388        private class ParentDeathWaiterPureJava implements Runnable {
389            @Override
390            @SuppressWarnings("empty-statement")
391            public void run() {
392                try {
393                    // When parent process is almost dead, in.read returns -1 (EOF)
394                    // as the pipe breaks.
395    
396                    while (System.in.read() >= 0);
397                }
398                catch (IOException ex) {
399                    // ignore
400                }
401    
402                // The port may take some time to become free after the pipe breaks
403                while (adminPortInUse(addresses) != null)
404                    ;
405                success = true;
406            }
407    
408            private ParentDeathWaiterPureJava() throws CommandException {
409                try {
410                    Thread t = new Thread(this);
411                    t.start();
412                    t.join(CLIConstants.DEATH_TIMEOUT_MS);
413                }
414                catch (Exception e) {
415                    // ignore!
416                }
417    
418                if (!success)
419                    throw new CommandException(
420                            strings.get("deathwait_timeout", CLIConstants.DEATH_TIMEOUT_MS));
421            }
422            boolean success = false;
423        }
424    }