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