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