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