001/**
002* Licensed to the Apache Software Foundation (ASF) under one
003* or more contributor license agreements.  See the NOTICE file
004* distributed with this work for additional information
005* regarding copyright ownership.  The ASF licenses this file
006* to you under the Apache License, Version 2.0 (the
007* "License"); you may not use this file except in compliance
008* with the License.  You may obtain a copy of the License at
009*
010*     http://www.apache.org/licenses/LICENSE-2.0
011*
012* Unless required by applicable law or agreed to in writing, software
013* distributed under the License is distributed on an "AS IS" BASIS,
014* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015* See the License for the specific language governing permissions and
016* limitations under the License.
017*/
018
019package org.apache.hadoop.yarn.client.cli;
020
021import java.io.IOException;
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.Collections;
025import java.util.List;
026
027import javax.ws.rs.core.MediaType;
028
029import org.apache.commons.cli.CommandLine;
030import org.apache.commons.cli.CommandLineParser;
031import org.apache.commons.cli.GnuParser;
032import org.apache.commons.cli.HelpFormatter;
033import org.apache.commons.cli.Option;
034import org.apache.commons.cli.Options;
035import org.apache.commons.cli.ParseException;
036import org.apache.commons.lang.StringUtils;
037import org.apache.hadoop.classification.InterfaceAudience.Public;
038import org.apache.hadoop.classification.InterfaceStability.Evolving;
039import org.apache.hadoop.conf.Configuration;
040import org.apache.hadoop.conf.Configured;
041import org.apache.hadoop.security.UserGroupInformation;
042import org.apache.hadoop.util.Tool;
043import org.apache.hadoop.yarn.api.records.ApplicationId;
044import org.apache.hadoop.yarn.api.records.ApplicationReport;
045import org.apache.hadoop.yarn.api.records.ContainerReport;
046import org.apache.hadoop.yarn.api.records.YarnApplicationState;
047import org.apache.hadoop.yarn.client.api.YarnClient;
048import org.apache.hadoop.yarn.conf.YarnConfiguration;
049import org.apache.hadoop.yarn.exceptions.YarnException;
050import org.apache.hadoop.yarn.logaggregation.LogCLIHelpers;
051import org.apache.hadoop.yarn.util.ConverterUtils;
052import org.apache.hadoop.yarn.util.Times;
053import org.apache.hadoop.yarn.webapp.util.WebAppUtils;
054import org.codehaus.jettison.json.JSONArray;
055import org.codehaus.jettison.json.JSONException;
056import org.codehaus.jettison.json.JSONObject;
057
058import com.google.common.annotations.VisibleForTesting;
059import com.sun.jersey.api.client.Client;
060import com.sun.jersey.api.client.ClientHandlerException;
061import com.sun.jersey.api.client.ClientResponse;
062import com.sun.jersey.api.client.UniformInterfaceException;
063import com.sun.jersey.api.client.WebResource;
064
065@Public
066@Evolving
067public class LogsCLI extends Configured implements Tool {
068
069  private static final String CONTAINER_ID_OPTION = "containerId";
070  private static final String APPLICATION_ID_OPTION = "applicationId";
071  private static final String NODE_ADDRESS_OPTION = "nodeAddress";
072  private static final String APP_OWNER_OPTION = "appOwner";
073  private static final String AM_CONTAINER_OPTION = "am";
074  private static final String CONTAINER_LOG_FILES = "logFiles";
075  public static final String HELP_CMD = "help";
076
077  @Override
078  public int run(String[] args) throws Exception {
079
080    Options opts = new Options();
081    opts.addOption(HELP_CMD, false, "Displays help for all commands.");
082    Option appIdOpt =
083        new Option(APPLICATION_ID_OPTION, true, "ApplicationId (required)");
084    appIdOpt.setRequired(true);
085    opts.addOption(appIdOpt);
086    opts.addOption(CONTAINER_ID_OPTION, true, "ContainerId. "
087        + "By default, it will only print syslog if the application is runing."
088        + " Work with -logFiles to get other logs.");
089    opts.addOption(NODE_ADDRESS_OPTION, true, "NodeAddress in the format "
090      + "nodename:port");
091    opts.addOption(APP_OWNER_OPTION, true,
092      "AppOwner (assumed to be current user if not specified)");
093    Option amOption = new Option(AM_CONTAINER_OPTION, true, 
094      "Prints the AM Container logs for this application. "
095      + "Specify comma-separated value to get logs for related AM Container. "
096      + "For example, If we specify -am 1,2, we will get the logs for "
097      + "the first AM Container as well as the second AM Container. "
098      + "To get logs for all AM Containers, use -am ALL. "
099      + "To get logs for the latest AM Container, use -am -1. "
100      + "By default, it will only print out syslog. Work with -logFiles "
101      + "to get other logs");
102    amOption.setValueSeparator(',');
103    amOption.setArgs(Option.UNLIMITED_VALUES);
104    amOption.setArgName("AM Containers");
105    opts.addOption(amOption);
106    Option logFileOpt = new Option(CONTAINER_LOG_FILES, true,
107      "Work with -am/-containerId and specify comma-separated value "
108      + "to get specified Container log files");
109    logFileOpt.setValueSeparator(',');
110    logFileOpt.setArgs(Option.UNLIMITED_VALUES);
111    logFileOpt.setArgName("Log File Name");
112    opts.addOption(logFileOpt);
113
114    opts.getOption(APPLICATION_ID_OPTION).setArgName("Application ID");
115    opts.getOption(CONTAINER_ID_OPTION).setArgName("Container ID");
116    opts.getOption(NODE_ADDRESS_OPTION).setArgName("Node Address");
117    opts.getOption(APP_OWNER_OPTION).setArgName("Application Owner");
118    opts.getOption(AM_CONTAINER_OPTION).setArgName("AM Containers");
119
120    Options printOpts = new Options();
121    printOpts.addOption(opts.getOption(HELP_CMD));
122    printOpts.addOption(opts.getOption(CONTAINER_ID_OPTION));
123    printOpts.addOption(opts.getOption(NODE_ADDRESS_OPTION));
124    printOpts.addOption(opts.getOption(APP_OWNER_OPTION));
125    printOpts.addOption(opts.getOption(AM_CONTAINER_OPTION));
126    printOpts.addOption(opts.getOption(CONTAINER_LOG_FILES));
127
128    if (args.length < 1) {
129      printHelpMessage(printOpts);
130      return -1;
131    }
132    if (args[0].equals("-help")) {
133      printHelpMessage(printOpts);
134      return 0;
135    }
136    CommandLineParser parser = new GnuParser();
137    String appIdStr = null;
138    String containerIdStr = null;
139    String nodeAddress = null;
140    String appOwner = null;
141    boolean getAMContainerLogs = false;
142    String[] logFiles = null;
143    List<String> amContainersList = new ArrayList<String>();
144    try {
145      CommandLine commandLine = parser.parse(opts, args, true);
146      appIdStr = commandLine.getOptionValue(APPLICATION_ID_OPTION);
147      containerIdStr = commandLine.getOptionValue(CONTAINER_ID_OPTION);
148      nodeAddress = commandLine.getOptionValue(NODE_ADDRESS_OPTION);
149      appOwner = commandLine.getOptionValue(APP_OWNER_OPTION);
150      getAMContainerLogs = commandLine.hasOption(AM_CONTAINER_OPTION);
151      if (getAMContainerLogs) {
152        String[] amContainers = commandLine.getOptionValues(AM_CONTAINER_OPTION);
153        for (String am : amContainers) {
154          boolean errorInput = false;
155          if (!am.trim().equalsIgnoreCase("ALL")) {
156            try {
157              int id = Integer.parseInt(am.trim());
158              if (id != -1 && id <= 0) {
159                errorInput = true;
160              }
161            } catch (NumberFormatException ex) {
162              errorInput = true;
163            }
164            if (errorInput) {
165              System.err.println(
166                "Invalid input for option -am. Valid inputs are 'ALL', -1 "
167                + "and any other integer which is larger than 0.");
168              printHelpMessage(printOpts);
169              return -1;
170            }
171            amContainersList.add(am.trim());
172          } else {
173            amContainersList.add("ALL");
174            break;
175          }
176        }
177      }
178      if (commandLine.hasOption(CONTAINER_LOG_FILES)) {
179        logFiles = commandLine.getOptionValues(CONTAINER_LOG_FILES);
180      }
181    } catch (ParseException e) {
182      System.err.println("options parsing failed: " + e.getMessage());
183      printHelpMessage(printOpts);
184      return -1;
185    }
186
187    if (appIdStr == null) {
188      System.err.println("ApplicationId cannot be null!");
189      printHelpMessage(printOpts);
190      return -1;
191    }
192
193    ApplicationId appId = null;
194    try {
195      appId = ConverterUtils.toApplicationId(appIdStr);
196    } catch (Exception e) {
197      System.err.println("Invalid ApplicationId specified");
198      return -1;
199    }
200
201    LogCLIHelpers logCliHelper = new LogCLIHelpers();
202    logCliHelper.setConf(getConf());
203
204    if (appOwner == null || appOwner.isEmpty()) {
205      appOwner = UserGroupInformation.getCurrentUser().getShortUserName();
206    }
207
208    YarnApplicationState appState = YarnApplicationState.NEW;
209    try {
210      appState = getApplicationState(appId);
211      if (appState == YarnApplicationState.NEW
212          || appState == YarnApplicationState.NEW_SAVING
213          || appState == YarnApplicationState.SUBMITTED) {
214        System.out.println("Logs are not avaiable right now.");
215        return -1;
216      }
217    } catch (IOException | YarnException e) {
218      System.err.println("Unable to get ApplicationState."
219          + " Attempting to fetch logs directly from the filesystem.");
220    }
221
222    // To get am logs
223    if (getAMContainerLogs) {
224      // if we do not specify the value for CONTAINER_LOG_FILES option,
225      // we will only output syslog
226      if (logFiles == null || logFiles.length == 0) {
227        logFiles = new String[] { "syslog" };
228      }
229      // If the application is running, we will call the RM WebService
230      // to get the AppAttempts which includes the nodeHttpAddress
231      // and containerId for all the AM Containers.
232      // After that, we will call NodeManager webService to get the
233      // related logs
234      if (appState == YarnApplicationState.ACCEPTED
235          || appState == YarnApplicationState.RUNNING) {
236        return printAMContainerLogs(getConf(), appIdStr, amContainersList,
237          logFiles, logCliHelper, appOwner, false);
238      } else {
239        // If the application is in the final state, we will call RM webservice
240        // to get all AppAttempts information first. If we get nothing,
241        // we will try to call AHS webservice to get related AppAttempts
242        // which includes nodeAddress for the AM Containers.
243        // After that, we will use nodeAddress and containerId
244        // to get logs from HDFS directly.
245        if (getConf().getBoolean(YarnConfiguration.APPLICATION_HISTORY_ENABLED,
246          YarnConfiguration.DEFAULT_APPLICATION_HISTORY_ENABLED)) {
247          return printAMContainerLogs(getConf(), appIdStr, amContainersList,
248            logFiles, logCliHelper, appOwner, true);
249        } else {
250          System.out
251            .println("Can not get AMContainers logs for the application:"
252                + appId);
253          System.out.println("This application:" + appId + " is finished."
254              + " Please enable the application history service. Or Using "
255              + "yarn logs -applicationId <appId> -containerId <containerId> "
256              + "--nodeAddress <nodeHttpAddress> to get the container logs");
257          return -1;
258        }
259      }
260    }
261
262    int resultCode = 0;
263    if (containerIdStr != null) {
264      // if we provide the node address and the application is in the final
265      // state, we could directly get logs from HDFS.
266      if (nodeAddress != null && isApplicationFinished(appState)) {
267        return logCliHelper.dumpAContainersLogsForALogType(appIdStr,
268            containerIdStr, nodeAddress, appOwner, logFiles == null ? null
269                : Arrays.asList(logFiles));
270      }
271      try {
272        // If the nodeAddress is not provided, we will try to get
273        // the ContainerReport. In the containerReport, we could get
274        // nodeAddress and nodeHttpAddress
275        ContainerReport report = getContainerReport(containerIdStr);
276        String nodeHttpAddress =
277            report.getNodeHttpAddress().replaceFirst(
278              WebAppUtils.getHttpSchemePrefix(getConf()), "");
279        String nodeId = report.getAssignedNode().toString();
280        // If the application is not in the final state,
281        // we will provide the NodeHttpAddress and get the container logs
282        // by calling NodeManager webservice.
283        if (!isApplicationFinished(appState)) {
284          if (logFiles == null || logFiles.length == 0) {
285            logFiles = new String[] { "syslog" };
286          }
287          printContainerLogsFromRunningApplication(getConf(), appIdStr,
288            containerIdStr, nodeHttpAddress, nodeId, logFiles, logCliHelper,
289            appOwner);
290        } else {
291          // If the application is in the final state, we will directly
292          // get the container logs from HDFS.
293          printContainerLogsForFinishedApplication(appIdStr, containerIdStr,
294            nodeId, logFiles, logCliHelper, appOwner);
295        }
296        return resultCode;
297      } catch (IOException | YarnException ex) {
298        System.err.println("Unable to get logs for this container:"
299            + containerIdStr + "for the application:" + appId);
300        if (!getConf().getBoolean(YarnConfiguration.APPLICATION_HISTORY_ENABLED,
301          YarnConfiguration.DEFAULT_APPLICATION_HISTORY_ENABLED)) {
302          System.out.println("Please enable the application history service. Or ");
303        }
304        System.out.println("Using "
305            + "yarn logs -applicationId <appId> -containerId <containerId> "
306            + "--nodeAddress <nodeHttpAddress> to get the container logs");
307        return -1;
308      }
309    } else {
310      if (nodeAddress == null) {
311        resultCode =
312            logCliHelper.dumpAllContainersLogs(appId, appOwner, System.out);
313      } else {
314        System.out.println("Should at least provide ContainerId!");
315        printHelpMessage(printOpts);
316        resultCode = -1;
317      }
318    }
319    return resultCode;
320  }
321
322  private YarnApplicationState getApplicationState(ApplicationId appId)
323      throws IOException, YarnException {
324    YarnClient yarnClient = createYarnClient();
325
326    try {
327      ApplicationReport appReport = yarnClient.getApplicationReport(appId);
328      return appReport.getYarnApplicationState();
329    } finally {
330      yarnClient.close();
331    }
332  }
333  
334  @VisibleForTesting
335  protected YarnClient createYarnClient() {
336    YarnClient yarnClient = YarnClient.createYarnClient();
337    yarnClient.init(getConf());
338    yarnClient.start();
339    return yarnClient;
340  }
341
342  public static void main(String[] args) throws Exception {
343    Configuration conf = new YarnConfiguration();
344    LogsCLI logDumper = new LogsCLI();
345    logDumper.setConf(conf);
346    int exitCode = logDumper.run(args);
347    System.exit(exitCode);
348  }
349
350  private void printHelpMessage(Options options) {
351    System.out.println("Retrieve logs for completed YARN applications.");
352    HelpFormatter formatter = new HelpFormatter();
353    formatter.printHelp("yarn logs -applicationId <application ID> [OPTIONS]", new Options());
354    formatter.setSyntaxPrefix("");
355    formatter.printHelp("general options are:", options);
356  }
357
358  private List<JSONObject> getAMContainerInfoForRMWebService(
359      Configuration conf, String appId) throws ClientHandlerException,
360      UniformInterfaceException, JSONException {
361    Client webServiceClient = Client.create();
362    String webAppAddress =
363        WebAppUtils.getWebAppBindURL(conf, YarnConfiguration.RM_BIND_HOST,
364          WebAppUtils.getRMWebAppURLWithScheme(conf));
365    WebResource webResource = webServiceClient.resource(webAppAddress);
366
367    ClientResponse response =
368        webResource.path("ws").path("v1").path("cluster").path("apps")
369          .path(appId).path("appattempts").accept(MediaType.APPLICATION_JSON)
370          .get(ClientResponse.class);
371    JSONObject json =
372        response.getEntity(JSONObject.class).getJSONObject("appAttempts");
373    JSONArray requests = json.getJSONArray("appAttempt");
374    List<JSONObject> amContainersList = new ArrayList<JSONObject>();
375    for (int i = 0; i < requests.length(); i++) {
376      amContainersList.add(requests.getJSONObject(i));
377    }
378    return amContainersList;
379  }
380
381  private List<JSONObject> getAMContainerInfoForAHSWebService(Configuration conf,
382      String appId) throws ClientHandlerException, UniformInterfaceException,
383      JSONException {
384    Client webServiceClient = Client.create();
385    String webAppAddress =
386        WebAppUtils.getHttpSchemePrefix(conf)
387            + WebAppUtils.getAHSWebAppURLWithoutScheme(conf);
388    WebResource webResource = webServiceClient.resource(webAppAddress);
389
390    ClientResponse response =
391        webResource.path("ws").path("v1").path("applicationhistory").path("apps")
392          .path(appId).path("appattempts").accept(MediaType.APPLICATION_JSON)
393          .get(ClientResponse.class);
394    JSONObject json = response.getEntity(JSONObject.class);
395    JSONArray requests = json.getJSONArray("appAttempt");
396    List<JSONObject> amContainersList = new ArrayList<JSONObject>();
397    for (int i = 0; i < requests.length(); i++) {
398      amContainersList.add(requests.getJSONObject(i));
399    }
400    Collections.reverse(amContainersList);
401    return amContainersList;
402  }
403
404  private void printContainerLogsFromRunningApplication(Configuration conf,
405      String appId, String containerIdStr, String nodeHttpAddress,
406      String nodeId, String[] logFiles, LogCLIHelpers logCliHelper,
407      String appOwner) throws IOException {
408    Client webServiceClient = Client.create();
409    String containerString = "\n\nContainer: " + containerIdStr;
410    System.out.println(containerString);
411    System.out.println(StringUtils.repeat("=", containerString.length()));
412    for (String logFile : logFiles) {
413      System.out.println("LogType:" + logFile);
414      System.out.println("Log Upload Time:"
415          + Times.format(System.currentTimeMillis()));
416      System.out.println("Log Contents:");
417      try {
418        WebResource webResource =
419            webServiceClient.resource(WebAppUtils.getHttpSchemePrefix(conf)
420                + nodeHttpAddress);
421        ClientResponse response =
422            webResource.path("ws").path("v1").path("node")
423              .path("containerlogs").path(containerIdStr).path(logFile)
424              .accept(MediaType.TEXT_PLAIN).get(ClientResponse.class);
425        System.out.println(response.getEntity(String.class));
426        System.out.println("End of LogType:" + logFile);
427      } catch (ClientHandlerException | UniformInterfaceException ex) {
428        System.out.println("Can not find the log file:" + logFile
429            + " for the container:" + containerIdStr + " in NodeManager:"
430            + nodeId);
431      }
432    }
433    // for the case, we have already uploaded partial logs in HDFS
434    logCliHelper.dumpAContainersLogsForALogType(appId, containerIdStr, nodeId,
435      appOwner, Arrays.asList(logFiles));
436  }
437
438  private void printContainerLogsForFinishedApplication(String appId,
439      String containerId, String nodeAddress, String[] logFiles,
440      LogCLIHelpers logCliHelper, String appOwner) throws IOException {
441    String containerString = "\n\nContainer: " + containerId;
442    System.out.println(containerString);
443    System.out.println(StringUtils.repeat("=", containerString.length()));
444    logCliHelper.dumpAContainersLogsForALogType(appId, containerId,
445      nodeAddress, appOwner, logFiles != null ? Arrays.asList(logFiles) : null);
446  }
447
448  private ContainerReport getContainerReport(String containerIdStr)
449      throws YarnException, IOException {
450    YarnClient yarnClient = createYarnClient();
451    try {
452      return yarnClient.getContainerReport(ConverterUtils
453        .toContainerId(containerIdStr));
454    } finally {
455      yarnClient.close();
456    }
457  }
458
459  private boolean isApplicationFinished(YarnApplicationState appState) {
460    return appState == YarnApplicationState.FINISHED
461        || appState == YarnApplicationState.FAILED
462        || appState == YarnApplicationState.KILLED; 
463  }
464
465  private int printAMContainerLogs(Configuration conf, String appId,
466      List<String> amContainers, String[] logFiles, LogCLIHelpers logCliHelper,
467      String appOwner, boolean applicationFinished) throws Exception {
468    List<JSONObject> amContainersList = null;
469    List<AMLogsRequest> requests = new ArrayList<AMLogsRequest>();
470    boolean getAMContainerLists = false;
471    String errorMessage = "";
472    try {
473      amContainersList = getAMContainerInfoForRMWebService(conf, appId);
474      if (amContainersList != null && !amContainersList.isEmpty()) {
475        getAMContainerLists = true;
476        for (JSONObject amContainer : amContainersList) {
477          AMLogsRequest request = new AMLogsRequest(applicationFinished);
478          request.setAmContainerId(amContainer.getString("containerId"));
479          request.setNodeHttpAddress(amContainer.getString("nodeHttpAddress"));
480          request.setNodeId(amContainer.getString("nodeId"));
481          requests.add(request);
482        }
483      }
484    } catch (Exception ex) {
485      errorMessage = ex.getMessage();
486      if (applicationFinished) {
487        try {
488          amContainersList = getAMContainerInfoForAHSWebService(conf, appId);
489          if (amContainersList != null && !amContainersList.isEmpty()) {
490            getAMContainerLists = true;
491            for (JSONObject amContainer : amContainersList) {
492              AMLogsRequest request = new AMLogsRequest(applicationFinished);
493              request.setAmContainerId(amContainer.getString("amContainerId"));
494              requests.add(request);
495            }
496          }
497        } catch (Exception e) {
498          errorMessage = e.getMessage();
499        }
500      }
501    }
502
503    if (!getAMContainerLists) {
504      System.err.println("Unable to get AM container informations "
505          + "for the application:" + appId);
506      System.err.println(errorMessage);
507      return -1;
508    }
509
510    if (amContainers.contains("ALL")) {
511      for (AMLogsRequest request : requests) {
512        outputAMContainerLogs(request, conf, appId, logFiles, logCliHelper,
513          appOwner);
514      }
515      System.out.println();      
516      System.out.println("Specified ALL for -am option. "
517          + "Printed logs for all am containers.");
518    } else {
519      for (String amContainer : amContainers) {
520        int amContainerId = Integer.parseInt(amContainer.trim());
521        if (amContainerId == -1) {
522          outputAMContainerLogs(requests.get(requests.size() - 1), conf, appId,
523            logFiles, logCliHelper, appOwner);
524        } else {
525          if (amContainerId <= requests.size()) {
526            outputAMContainerLogs(requests.get(amContainerId - 1), conf, appId,
527              logFiles, logCliHelper, appOwner);
528          }
529        }
530      }
531    }
532    return 0;
533  }
534
535  private void outputAMContainerLogs(AMLogsRequest request, Configuration conf,
536      String appId, String[] logFiles, LogCLIHelpers logCliHelper,
537      String appOwner) throws Exception {
538    String nodeHttpAddress = request.getNodeHttpAddress();
539    String containerId = request.getAmContainerId();
540    String nodeId = request.getNodeId();
541
542    if (request.isAppFinished()) {
543      if (containerId != null && !containerId.isEmpty()) {
544        if (nodeId == null || nodeId.isEmpty()) {
545          try {
546            nodeId =
547                getContainerReport(containerId).getAssignedNode().toString();
548          } catch (Exception ex) {
549            System.err.println(ex);
550            nodeId = null;
551          }
552        }
553        if (nodeId != null && !nodeId.isEmpty()) {
554          printContainerLogsForFinishedApplication(appId, containerId, nodeId,
555            logFiles, logCliHelper, appOwner);
556        }
557      }
558    } else {
559      if (nodeHttpAddress != null && containerId != null
560          && !nodeHttpAddress.isEmpty() && !containerId.isEmpty()) {
561        printContainerLogsFromRunningApplication(conf, appId, containerId,
562          nodeHttpAddress, nodeId, logFiles, logCliHelper, appOwner);
563      }
564    }
565  }
566
567  private static class AMLogsRequest {
568    private String amContainerId;
569    private String nodeId;
570    private String nodeHttpAddress;
571    private final boolean isAppFinished;
572
573    AMLogsRequest(boolean isAppFinished) {
574      this.isAppFinished = isAppFinished;
575      this.setAmContainerId("");
576      this.setNodeId("");
577      this.setNodeHttpAddress("");
578    }
579
580    public String getAmContainerId() {
581      return amContainerId;
582    }
583
584    public void setAmContainerId(String amContainerId) {
585      this.amContainerId = amContainerId;
586    }
587
588    public String getNodeId() {
589      return nodeId;
590    }
591
592    public void setNodeId(String nodeId) {
593      this.nodeId = nodeId;
594    }
595
596    public String getNodeHttpAddress() {
597      return nodeHttpAddress;
598    }
599
600    public void setNodeHttpAddress(String nodeHttpAddress) {
601      this.nodeHttpAddress = nodeHttpAddress;
602    }
603
604    public boolean isAppFinished() {
605      return isAppFinished;
606    }
607  }
608}