001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2008-2010 Sun Microsystems, Inc.
025 *      Portions Copyright 2011-2015 ForgeRock AS
026 */
027package org.opends.quicksetup.util;
028
029import java.io.BufferedReader;
030import java.io.IOException;
031import java.io.InputStreamReader;
032import java.util.ArrayList;
033import java.util.Map;
034
035import javax.naming.NamingException;
036import javax.naming.ldap.InitialLdapContext;
037
038import org.forgerock.i18n.LocalizableMessage;
039import org.forgerock.i18n.LocalizableMessageBuilder;
040import org.forgerock.i18n.slf4j.LocalizedLogger;
041import org.opends.quicksetup.*;
042import org.opends.quicksetup.installer.InstallerHelper;
043import org.opends.server.util.SetupUtils;
044import org.opends.server.util.StaticUtils;
045
046import com.forgerock.opendj.cli.CliConstants;
047
048import static com.forgerock.opendj.cli.ArgumentConstants.*;
049import static com.forgerock.opendj.cli.Utils.*;
050import static com.forgerock.opendj.util.OperatingSystem.*;
051
052import static org.opends.admin.ads.util.ConnectionUtils.*;
053import static org.opends.messages.QuickSetupMessages.*;
054
055/**
056 * Class used to manipulate an OpenDS server.
057 */
058public class ServerController {
059
060  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
061
062  private Application application;
063
064  private Installation installation;
065
066  /**
067   * Creates a new instance that will operate on <code>application</code>'s
068   * installation.
069   * @param application to use for notifications
070   */
071  public ServerController(Application application) {
072    this(application, application.getInstallation());
073  }
074
075  /**
076   * Creates a new instance that will operate on <code>application</code>'s
077   * installation.
078   * @param installation representing the server instance to control
079   */
080  public ServerController(Installation installation) {
081    this(null, installation);
082  }
083
084  /**
085   * Creates a new instance that will operate on <code>installation</code>
086   * and use <code>application</code> for notifications.
087   * @param application to use for notifications
088   * @param installation representing the server instance to control
089   */
090  public ServerController(Application application, Installation installation) {
091    if (installation == null) {
092      throw new NullPointerException("installation cannot be null");
093    }
094    this.application = application;
095    this.installation = installation;
096  }
097
098  /**
099   * This methods stops the server.
100   *
101   * @throws org.opends.quicksetup.ApplicationException if something goes wrong.
102   */
103  public void stopServer() throws ApplicationException {
104    stopServer(false);
105  }
106
107  /**
108   * This methods stops the server.
109   *
110   * @param suppressOutput boolean indicating that ouput to standard output
111   *                       streams from the server should be suppressed.
112   * @throws org.opends.quicksetup.ApplicationException
113   *          if something goes wrong.
114   */
115  public void stopServer(boolean suppressOutput) throws ApplicationException {
116    stopServer(suppressOutput,false);
117  }
118  /**
119   * This methods stops the server.
120   *
121   * @param suppressOutput boolean indicating that ouput to standard output
122   *                       streams from the server should be suppressed.
123   * @param noPropertiesFile boolean indicating if the stopServer should
124   *                       be called without taking into account the
125   *                       properties file.
126   * @throws org.opends.quicksetup.ApplicationException
127   *          if something goes wrong.
128   */
129  public void stopServer(boolean suppressOutput,boolean noPropertiesFile)
130  throws ApplicationException {
131
132    if (suppressOutput && !StandardOutputSuppressor.isSuppressed()) {
133      StandardOutputSuppressor.suppress();
134    }
135
136    if (suppressOutput && application != null)
137    {
138      application.setNotifyListeners(false);
139    }
140
141    try {
142      if (application != null) {
143        LocalizableMessageBuilder mb = new LocalizableMessageBuilder();
144        mb.append(application.getFormattedProgress(
145                        INFO_PROGRESS_STOPPING.get()));
146        mb.append(application.getLineBreak());
147        application.notifyListeners(mb.toMessage());
148      }
149      logger.info(LocalizableMessage.raw("stopping server"));
150
151      ArrayList<String> argList = new ArrayList<>();
152      argList.add(Utils.getScriptPath(
153          Utils.getPath(installation.getServerStopCommandFile())));
154      int size = argList.size();
155      if (noPropertiesFile)
156      {
157        size++;
158      }
159      String[] args = new String[size];
160      argList.toArray(args);
161      if (noPropertiesFile)
162      {
163        args[argList.size()] = "--" + OPTION_LONG_NO_PROP_FILE;
164      }
165      ProcessBuilder pb = new ProcessBuilder(args);
166      Map<String, String> env = pb.environment();
167      env.put(SetupUtils.OPENDJ_JAVA_HOME, System.getProperty("java.home"));
168      env.remove(SetupUtils.OPENDJ_JAVA_ARGS);
169      env.remove("CLASSPATH");
170
171      logger.info(LocalizableMessage.raw("Before calling stop-ds.  Is server running? "+
172          installation.getStatus().isServerRunning()));
173
174      int stopTries = 3;
175      while (stopTries > 0)
176      {
177        stopTries --;
178        logger.info(LocalizableMessage.raw("Launching stop command, stopTries left: "+
179            stopTries));
180
181        try
182        {
183          logger.info(LocalizableMessage.raw("Launching stop command, argList: "+argList));
184          Process process = pb.start();
185
186          BufferedReader err =
187            new BufferedReader(
188                new InputStreamReader(process.getErrorStream()));
189          BufferedReader out =
190            new BufferedReader(
191                new InputStreamReader(process.getInputStream()));
192
193          /* Create these objects to resend the stop process output to the
194           * details area.
195           */
196          new StopReader(err, true);
197          new StopReader(out, false);
198
199          int returnValue = process.waitFor();
200
201          int clientSideError =
202            org.opends.server.protocols.ldap.
203            LDAPResultCode.CLIENT_SIDE_CONNECT_ERROR;
204          if (isWindows()
205              && (returnValue == clientSideError || returnValue == 0)) {
206            /*
207             * Sometimes the server keeps some locks on the files.
208             * TODO: remove this code once stop-ds returns properly when
209             * server is stopped.
210             */
211            int nTries = 10;
212            boolean stopped = false;
213            for (int i = 0; i < nTries && !stopped; i++) {
214              logger.trace("waiting for server to stop");
215              try {
216                Thread.sleep(5000);
217              }
218              catch (Exception ex)
219              {
220                // do nothing
221              }
222              stopped = !installation.getStatus().isServerRunning();
223              logger.info(LocalizableMessage.raw(
224                  "After calling stop-ds.  Is server running? " + !stopped));
225              if (stopped) {
226                break;
227              }
228              if (application != null) {
229                LocalizableMessageBuilder mb = new LocalizableMessageBuilder();
230                mb.append(application.getFormattedLog(
231                    INFO_PROGRESS_SERVER_WAITING_TO_STOP.get()));
232                mb.append(application.getLineBreak());
233                application.notifyListeners(mb.toMessage());
234              }
235            }
236            if (!stopped) {
237              returnValue = -1;
238            }
239          }
240
241          if (returnValue == clientSideError) {
242            if (application != null) {
243              LocalizableMessageBuilder mb = new LocalizableMessageBuilder();
244              mb.append(application.getLineBreak());
245              mb.append(application.getFormattedLog(
246                  INFO_PROGRESS_SERVER_ALREADY_STOPPED.get()));
247              mb.append(application.getLineBreak());
248              application.notifyListeners(mb.toMessage());
249            }
250            logger.info(LocalizableMessage.raw("server already stopped"));
251            break;
252          } else if (returnValue != 0) {
253            if (stopTries <= 0)
254            {
255              /*
256               * The return code is not the one expected, assume the server
257               * could not be stopped.
258               */
259              throw new ApplicationException(
260                  ReturnCode.STOP_ERROR,
261                  INFO_ERROR_STOPPING_SERVER_CODE.get(returnValue),
262                  null);
263            }
264          } else {
265            if (application != null) {
266              application.notifyListeners(application.getFormattedLog(
267                  INFO_PROGRESS_SERVER_STOPPED.get()));
268            }
269            logger.info(LocalizableMessage.raw("server stopped"));
270            break;
271          }
272
273        } catch (Exception e) {
274          throw new ApplicationException(
275              ReturnCode.STOP_ERROR, getThrowableMsg(
276                  INFO_ERROR_STOPPING_SERVER.get(), e), e);
277        }
278      }
279    }
280    finally {
281      if (suppressOutput)
282      {
283        if (StandardOutputSuppressor.isSuppressed())
284        {
285          StandardOutputSuppressor.unsuppress();
286        }
287        if (application != null)
288        {
289          application.setNotifyListeners(true);
290        }
291      }
292    }
293  }
294
295  /**
296   * This methods starts the server.
297   *
298   *@throws org.opends.quicksetup.ApplicationException if something goes wrong.
299   */
300  public void startServer() throws ApplicationException {
301    startServer(true, false);
302  }
303
304  /**
305   * This methods starts the server.
306   * @param suppressOutput boolean indicating that ouput to standard output
307   * streams from the server should be suppressed.
308   * @throws org.opends.quicksetup.ApplicationException if something goes wrong.
309   */
310  public void startServer(boolean suppressOutput)
311          throws ApplicationException
312  {
313    startServer(true, suppressOutput);
314  }
315
316  /**
317   * This methods starts the server.
318   * @param verify boolean indicating whether this method will attempt to
319   * connect to the server after starting to verify that it is listening.
320   * @param suppressOutput indicating that ouput to standard output streams
321   * from the server should be suppressed.
322   * @throws org.opends.quicksetup.ApplicationException if something goes wrong.
323   */
324  private void startServer(boolean verify, boolean suppressOutput)
325  throws ApplicationException
326  {
327    if (suppressOutput && !StandardOutputSuppressor.isSuppressed()) {
328      StandardOutputSuppressor.suppress();
329    }
330
331    if (suppressOutput && application != null)
332    {
333      application.setNotifyListeners(false);
334    }
335
336    try {
337      if (application != null) {
338        LocalizableMessageBuilder mb = new LocalizableMessageBuilder();
339        mb.append(application.getFormattedProgress(
340            INFO_PROGRESS_STARTING.get()));
341        mb.append(application.getLineBreak());
342        application.notifyListeners(mb.toMessage());
343      }
344      logger.info(LocalizableMessage.raw("starting server"));
345
346      ArrayList<String> argList = new ArrayList<>();
347      argList.add(Utils.getScriptPath(
348          Utils.getPath(installation.getServerStartCommandFile())));
349      argList.add("--timeout");
350      argList.add("0");
351      String[] args = new String[argList.size()];
352      argList.toArray(args);
353      ProcessBuilder pb = new ProcessBuilder(args);
354      pb.directory(installation.getBinariesDirectory());
355      Map<String, String> env = pb.environment();
356      env.put(SetupUtils.OPENDJ_JAVA_HOME, System.getProperty("java.home"));
357      env.remove(SetupUtils.OPENDJ_JAVA_ARGS);
358
359      // Upgrader's classpath contains jars located in the temporary
360      // directory that we don't want locked by the directory server
361      // when it starts.  Since we're just calling the start-ds script
362      // it will figure out the correct classpath for the server.
363      env.remove("CLASSPATH");
364      try
365      {
366        String startedId = getStartedId();
367        Process process = pb.start();
368
369        BufferedReader err =
370          new BufferedReader(new InputStreamReader(process.getErrorStream()));
371        BufferedReader out =
372          new BufferedReader(new InputStreamReader(process.getInputStream()));
373
374        StartReader errReader = new StartReader(err, startedId, true);
375        StartReader outputReader = new StartReader(out, startedId, false);
376
377        int returnValue = process.waitFor();
378
379        logger.info(LocalizableMessage.raw("start-ds return value: "+returnValue));
380
381        if (returnValue != 0)
382        {
383          throw new ApplicationException(ReturnCode.START_ERROR,
384              INFO_ERROR_STARTING_SERVER_CODE.get(returnValue),
385              null);
386        }
387        if (outputReader.isFinished())
388        {
389          logger.info(LocalizableMessage.raw("Output reader finished."));
390        }
391        if (errReader.isFinished())
392        {
393          logger.info(LocalizableMessage.raw("Error reader finished."));
394        }
395        if (!outputReader.startedIdFound() && !errReader.startedIdFound())
396        {
397          logger.warn(LocalizableMessage.raw("Started ID could not be found"));
398        }
399
400        // Check if something wrong occurred reading the starting of the server
401        ApplicationException ex = errReader.getException();
402        if (ex == null)
403        {
404          ex = outputReader.getException();
405        }
406        if (ex != null)
407        {
408          // This is meaningless right now since we throw
409          // the exception below, but in case we change out
410          // minds later or add the ability to return exceptions
411          // in the output only instead of throwing...
412          throw ex;
413        } else if (verify)
414        {
415          /*
416           * There are no exceptions from the readers and they are marked as
417           * finished. So it seems that everything went fine.
418           *
419           * However we can have issues with the firewalls or do not have rights
420           * to connect or since the startup process is asynchronous we will
421           * have to wait for the databases and the listeners to initialize.
422           * Just check if we can connect to the server.
423           * Try 30 times with an interval of 3 seconds between try.
424           */
425          boolean connected = false;
426          Configuration config = installation.getCurrentConfiguration();
427          int port = config.getAdminConnectorPort();
428
429          // See if the application has prompted for credentials.  If
430          // not we'll just try to connect anonymously.
431          String userDn = null;
432          String userPw = null;
433          if (application != null) {
434            userDn = application.getUserData().getDirectoryManagerDn();
435            userPw = application.getUserData().getDirectoryManagerPwd();
436          }
437          if (userDn == null || userPw == null) {
438            userDn = null;
439            userPw = null;
440          }
441
442          InitialLdapContext ctx = null;
443          for (int i=0; i<50 && !connected; i++)
444          {
445            String hostName = null;
446            if (application != null)
447            {
448              hostName = application.getUserData().getHostName();
449            }
450            if (hostName == null)
451            {
452              hostName = "localhost";
453            }
454
455            int dig = i % 10;
456
457            if ((dig == 3 || dig == 4) && !"localhost".equals(hostName))
458            {
459              // Try with local host. This might be necessary in certain
460              // network configurations.
461              hostName = "localhost";
462            }
463
464            if (dig == 5 || dig == 6)
465            {
466              // Try with 0.0.0.0. This might be necessary in certain
467              // network configurations.
468              hostName = "0.0.0.0";
469            }
470
471            hostName = getHostNameForLdapUrl(hostName);
472            String ldapUrl = "ldaps://"+hostName+":" + port;
473            try
474            {
475              int timeout = CliConstants.DEFAULT_LDAP_CONNECT_TIMEOUT;
476              if (application != null && application.getUserData() != null)
477              {
478                timeout = application.getUserData().getConnectTimeout();
479              }
480              ctx = createLdapsContext(ldapUrl, userDn, userPw, timeout,
481                  null, null, null);
482              connected = true;
483            }
484            catch (NamingException ne)
485            {
486              logger.warn(LocalizableMessage.raw("Could not connect to server: "+ne, ne));
487            }
488            finally
489            {
490              StaticUtils.close(ctx);
491            }
492            if (!connected)
493            {
494              try
495              {
496                Thread.sleep(3000);
497              }
498              catch (Throwable t)
499              {
500                 // do nothing
501              }
502            }
503          }
504          if (!connected)
505          {
506            final LocalizableMessage msg = isWindows()
507                ? INFO_ERROR_STARTING_SERVER_IN_WINDOWS.get(port)
508                : INFO_ERROR_STARTING_SERVER_IN_UNIX.get(port);
509            throw new ApplicationException(ReturnCode.START_ERROR, msg, null);
510          }
511        }
512      } catch (IOException | InterruptedException ioe)
513      {
514        throw new ApplicationException(
515            ReturnCode.START_ERROR,
516            getThrowableMsg(INFO_ERROR_STARTING_SERVER.get(), ioe), ioe);
517      }
518    } finally {
519      if (suppressOutput)
520      {
521        if (StandardOutputSuppressor.isSuppressed())
522        {
523          StandardOutputSuppressor.unsuppress();
524        }
525        if (application != null)
526        {
527          application.setNotifyListeners(true);
528        }
529      }
530    }
531  }
532
533  /**
534   * This class is used to read the standard error and standard output of the
535   * Stop process.
536   * <p/>
537   * When a new log message is found notifies the
538   * UninstallProgressUpdateListeners of it. If an error occurs it also
539   * notifies the listeners.
540   */
541  private class StopReader {
542    private boolean isFirstLine;
543
544    /**
545     * The protected constructor.
546     *
547     * @param reader  the BufferedReader of the stop process.
548     * @param isError a boolean indicating whether the BufferedReader
549     *        corresponds to the standard error or to the standard output.
550     */
551    public StopReader(final BufferedReader reader,
552                                      final boolean isError) {
553      final LocalizableMessage errorTag =
554              isError ?
555                      INFO_ERROR_READING_ERROROUTPUT.get() :
556                      INFO_ERROR_READING_OUTPUT.get();
557
558      isFirstLine = true;
559      Thread t = new Thread(new Runnable() {
560        @Override
561        public void run() {
562          try {
563            String line = reader.readLine();
564            while (line != null) {
565              if (application != null) {
566                LocalizableMessageBuilder buf = new LocalizableMessageBuilder();
567                if (!isFirstLine) {
568                  buf.append(application.getProgressMessageFormatter().
569                          getLineBreak());
570                }
571                if (isError) {
572                  buf.append(application.getFormattedLogError(
573                          LocalizableMessage.raw(line)));
574                } else {
575                  buf.append(application.getFormattedLog(
576                          LocalizableMessage.raw(line)));
577                }
578                application.notifyListeners(buf.toMessage());
579                isFirstLine = false;
580              }
581              logger.info(LocalizableMessage.raw("server: " + line));
582              line = reader.readLine();
583            }
584          } catch (Throwable t) {
585            if (application != null) {
586              LocalizableMessage errorMsg = getThrowableMsg(errorTag, t);
587              application.notifyListeners(errorMsg);
588            }
589            logger.info(LocalizableMessage.raw("error reading server messages",t));
590          }
591        }
592      });
593      t.start();
594    }
595  }
596
597  /**
598   * Returns the LocalizableMessage ID indicating that the server has started.
599   * @return the LocalizableMessage ID indicating that the server has started.
600   */
601  private String getStartedId()
602  {
603    InstallerHelper helper = new InstallerHelper();
604    return helper.getStartedId();
605  }
606
607  /**
608   * This class is used to read the standard error and standard output of the
609   * Start process.
610   *
611   * When a new log message is found notifies the ProgressUpdateListeners
612   * of it. If an error occurs it also notifies the listeners.
613   *
614   */
615  private class StartReader
616  {
617    private ApplicationException ex;
618
619    private boolean isFinished;
620
621    private boolean startedIdFound;
622
623    private boolean isFirstLine;
624
625    /**
626     * The protected constructor.
627     * @param reader the BufferedReader of the start process.
628     * @param startedId the message ID that this class can use to know whether
629     * the start is over or not.
630     * @param isError a boolean indicating whether the BufferedReader
631     * corresponds to the standard error or to the standard output.
632     */
633    public StartReader(final BufferedReader reader, final String startedId,
634        final boolean isError)
635    {
636      final LocalizableMessage errorTag =
637              isError ?
638                      INFO_ERROR_READING_ERROROUTPUT.get() :
639                      INFO_ERROR_READING_OUTPUT.get();
640
641      isFirstLine = true;
642
643      Thread t = new Thread(new Runnable()
644      {
645        @Override
646        public void run()
647        {
648          try
649          {
650            String line = reader.readLine();
651            while (line != null)
652            {
653              if (application != null) {
654                LocalizableMessageBuilder buf = new LocalizableMessageBuilder();
655                if (!isFirstLine)
656                {
657                  buf.append(application.getProgressMessageFormatter().
658                          getLineBreak());
659                }
660                if (isError)
661                {
662                  buf.append(application.getFormattedLogError(
663                          LocalizableMessage.raw(line)));
664                } else
665                {
666                  buf.append(application.getFormattedLog(
667                          LocalizableMessage.raw(line)));
668                }
669                application.notifyListeners(buf.toMessage());
670                isFirstLine = false;
671              }
672              logger.info(LocalizableMessage.raw("server: " + line));
673              if (line.toLowerCase().contains("=" + startedId))
674              {
675                isFinished = true;
676                startedIdFound = true;
677              }
678              line = reader.readLine();
679            }
680          } catch (Throwable t)
681          {
682            logger.warn(LocalizableMessage.raw("Error reading output: "+t, t));
683            ex = new ApplicationException(
684                ReturnCode.START_ERROR,
685                getThrowableMsg(errorTag, t), t);
686
687          }
688          isFinished = true;
689        }
690      });
691      t.start();
692    }
693
694    /**
695     * Returns the ApplicationException that occurred reading the Start error
696     * and output or <CODE>null</CODE> if no exception occurred.
697     * @return the exception that occurred reading or <CODE>null</CODE> if
698     * no exception occurred.
699     */
700    public ApplicationException getException()
701    {
702      return ex;
703    }
704
705    /**
706     * Returns <CODE>true</CODE> if the server starting process finished
707     * (successfully or not) and <CODE>false</CODE> otherwise.
708     * @return <CODE>true</CODE> if the server starting process finished
709     * (successfully or not) and <CODE>false</CODE> otherwise.
710     */
711    public boolean isFinished()
712    {
713      return isFinished;
714    }
715
716    /**
717     * Returns <CODE>true</CODE> if the server start Id was found and
718     * <CODE>false</CODE> otherwise.
719     * @return <CODE>true</CODE> if the server start Id was found and
720     * <CODE>false</CODE> otherwise.
721     */
722    public boolean startedIdFound()
723    {
724      return startedIdFound;
725    }
726  }
727
728}