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}