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}