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 */
018package org.apache.hadoop.mapreduce.tools;
019
020import java.io.IOException;
021import java.io.OutputStreamWriter;
022import java.io.PrintWriter;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.HashSet;
026import java.util.List;
027import java.util.Set;
028
029import org.apache.commons.lang.StringUtils;
030import org.apache.commons.logging.Log;
031import org.apache.commons.logging.LogFactory;
032import org.apache.hadoop.classification.InterfaceAudience;
033import org.apache.hadoop.classification.InterfaceStability;
034import org.apache.hadoop.classification.InterfaceAudience.Private;
035import org.apache.hadoop.conf.Configuration;
036import org.apache.hadoop.conf.Configured;
037import org.apache.hadoop.ipc.RemoteException;
038import org.apache.hadoop.mapred.JobConf;
039import org.apache.hadoop.mapred.TIPStatus;
040import org.apache.hadoop.mapreduce.Cluster;
041import org.apache.hadoop.mapreduce.Counters;
042import org.apache.hadoop.mapreduce.Job;
043import org.apache.hadoop.mapreduce.JobID;
044import org.apache.hadoop.mapreduce.JobPriority;
045import org.apache.hadoop.mapreduce.JobStatus;
046import org.apache.hadoop.mapreduce.TaskAttemptID;
047import org.apache.hadoop.mapreduce.TaskCompletionEvent;
048import org.apache.hadoop.mapreduce.TaskReport;
049import org.apache.hadoop.mapreduce.TaskTrackerInfo;
050import org.apache.hadoop.mapreduce.TaskType;
051import org.apache.hadoop.mapreduce.jobhistory.HistoryViewer;
052import org.apache.hadoop.mapreduce.v2.LogParams;
053import org.apache.hadoop.security.AccessControlException;
054import org.apache.hadoop.util.ExitUtil;
055import org.apache.hadoop.util.Tool;
056import org.apache.hadoop.util.ToolRunner;
057import org.apache.hadoop.yarn.logaggregation.LogCLIHelpers;
058
059import com.google.common.base.Charsets;
060
061/**
062 * Interprets the map reduce cli options 
063 */
064@InterfaceAudience.Public
065@InterfaceStability.Stable
066public class CLI extends Configured implements Tool {
067  private static final Log LOG = LogFactory.getLog(CLI.class);
068  protected Cluster cluster;
069  private static final Set<String> taskTypes = new HashSet<String>(
070      Arrays.asList("MAP", "REDUCE"));
071  private final Set<String> taskStates = new HashSet<String>(Arrays.asList(
072      "running", "completed", "pending", "failed", "killed"));
073 
074  public CLI() {
075  }
076  
077  public CLI(Configuration conf) {
078    setConf(conf);
079  }
080  
081  public int run(String[] argv) throws Exception {
082    int exitCode = -1;
083    if (argv.length < 1) {
084      displayUsage("");
085      return exitCode;
086    }    
087    // process arguments
088    String cmd = argv[0];
089    String submitJobFile = null;
090    String jobid = null;
091    String taskid = null;
092    String historyFile = null;
093    String counterGroupName = null;
094    String counterName = null;
095    JobPriority jp = null;
096    String taskType = null;
097    String taskState = null;
098    int fromEvent = 0;
099    int nEvents = 0;
100    boolean getStatus = false;
101    boolean getCounter = false;
102    boolean killJob = false;
103    boolean listEvents = false;
104    boolean viewHistory = false;
105    boolean viewAllHistory = false;
106    boolean listJobs = false;
107    boolean listAllJobs = false;
108    boolean listActiveTrackers = false;
109    boolean listBlacklistedTrackers = false;
110    boolean displayTasks = false;
111    boolean killTask = false;
112    boolean failTask = false;
113    boolean setJobPriority = false;
114    boolean logs = false;
115
116    if ("-submit".equals(cmd)) {
117      if (argv.length != 2) {
118        displayUsage(cmd);
119        return exitCode;
120      }
121      submitJobFile = argv[1];
122    } else if ("-status".equals(cmd)) {
123      if (argv.length != 2) {
124        displayUsage(cmd);
125        return exitCode;
126      }
127      jobid = argv[1];
128      getStatus = true;
129    } else if("-counter".equals(cmd)) {
130      if (argv.length != 4) {
131        displayUsage(cmd);
132        return exitCode;
133      }
134      getCounter = true;
135      jobid = argv[1];
136      counterGroupName = argv[2];
137      counterName = argv[3];
138    } else if ("-kill".equals(cmd)) {
139      if (argv.length != 2) {
140        displayUsage(cmd);
141        return exitCode;
142      }
143      jobid = argv[1];
144      killJob = true;
145    } else if ("-set-priority".equals(cmd)) {
146      if (argv.length != 3) {
147        displayUsage(cmd);
148        return exitCode;
149      }
150      jobid = argv[1];
151      try {
152        jp = JobPriority.valueOf(argv[2]); 
153      } catch (IllegalArgumentException iae) {
154        LOG.info(iae);
155        displayUsage(cmd);
156        return exitCode;
157      }
158      setJobPriority = true; 
159    } else if ("-events".equals(cmd)) {
160      if (argv.length != 4) {
161        displayUsage(cmd);
162        return exitCode;
163      }
164      jobid = argv[1];
165      fromEvent = Integer.parseInt(argv[2]);
166      nEvents = Integer.parseInt(argv[3]);
167      listEvents = true;
168    } else if ("-history".equals(cmd)) {
169      if (argv.length != 2 && !(argv.length == 3 && "all".equals(argv[1]))) {
170         displayUsage(cmd);
171         return exitCode;
172      }
173      viewHistory = true;
174      if (argv.length == 3 && "all".equals(argv[1])) {
175        viewAllHistory = true;
176        historyFile = argv[2];
177      } else {
178        historyFile = argv[1];
179      }
180    } else if ("-list".equals(cmd)) {
181      if (argv.length != 1 && !(argv.length == 2 && "all".equals(argv[1]))) {
182        displayUsage(cmd);
183        return exitCode;
184      }
185      if (argv.length == 2 && "all".equals(argv[1])) {
186        listAllJobs = true;
187      } else {
188        listJobs = true;
189      }
190    } else if("-kill-task".equals(cmd)) {
191      if (argv.length != 2) {
192        displayUsage(cmd);
193        return exitCode;
194      }
195      killTask = true;
196      taskid = argv[1];
197    } else if("-fail-task".equals(cmd)) {
198      if (argv.length != 2) {
199        displayUsage(cmd);
200        return exitCode;
201      }
202      failTask = true;
203      taskid = argv[1];
204    } else if ("-list-active-trackers".equals(cmd)) {
205      if (argv.length != 1) {
206        displayUsage(cmd);
207        return exitCode;
208      }
209      listActiveTrackers = true;
210    } else if ("-list-blacklisted-trackers".equals(cmd)) {
211      if (argv.length != 1) {
212        displayUsage(cmd);
213        return exitCode;
214      }
215      listBlacklistedTrackers = true;
216    } else if ("-list-attempt-ids".equals(cmd)) {
217      if (argv.length != 4) {
218        displayUsage(cmd);
219        return exitCode;
220      }
221      jobid = argv[1];
222      taskType = argv[2];
223      taskState = argv[3];
224      displayTasks = true;
225      if (!taskTypes.contains(
226          org.apache.hadoop.util.StringUtils.toUpperCase(taskType))) {
227        System.out.println("Error: Invalid task-type: " + taskType);
228        displayUsage(cmd);
229        return exitCode;
230      }
231      if (!taskStates.contains(
232          org.apache.hadoop.util.StringUtils.toLowerCase(taskState))) {
233        System.out.println("Error: Invalid task-state: " + taskState);
234        displayUsage(cmd);
235        return exitCode;
236      }
237    } else if ("-logs".equals(cmd)) {
238      if (argv.length == 2 || argv.length ==3) {
239        logs = true;
240        jobid = argv[1];
241        if (argv.length == 3) {
242          taskid = argv[2];
243        }  else {
244          taskid = null;
245        }
246      } else {
247        displayUsage(cmd);
248        return exitCode;
249      }
250    } else {
251      displayUsage(cmd);
252      return exitCode;
253    }
254
255    // initialize cluster
256    cluster = createCluster();
257        
258    // Submit the request
259    try {
260      if (submitJobFile != null) {
261        Job job = Job.getInstance(new JobConf(submitJobFile));
262        job.submit();
263        System.out.println("Created job " + job.getJobID());
264        exitCode = 0;
265      } else if (getStatus) {
266        Job job = cluster.getJob(JobID.forName(jobid));
267        if (job == null) {
268          System.out.println("Could not find job " + jobid);
269        } else {
270          Counters counters = job.getCounters();
271          System.out.println();
272          System.out.println(job);
273          if (counters != null) {
274            System.out.println(counters);
275          } else {
276            System.out.println("Counters not available. Job is retired.");
277          }
278          exitCode = 0;
279        }
280      } else if (getCounter) {
281        Job job = cluster.getJob(JobID.forName(jobid));
282        if (job == null) {
283          System.out.println("Could not find job " + jobid);
284        } else {
285          Counters counters = job.getCounters();
286          if (counters == null) {
287            System.out.println("Counters not available for retired job " + 
288            jobid);
289            exitCode = -1;
290          } else {
291            System.out.println(getCounter(counters,
292              counterGroupName, counterName));
293            exitCode = 0;
294          }
295        }
296      } else if (killJob) {
297        Job job = cluster.getJob(JobID.forName(jobid));
298        if (job == null) {
299          System.out.println("Could not find job " + jobid);
300        } else {
301          JobStatus jobStatus = job.getStatus();
302          if (jobStatus.getState() == JobStatus.State.FAILED) {
303            System.out.println("Could not mark the job " + jobid
304                + " as killed, as it has already failed.");
305            exitCode = -1;
306          } else if (jobStatus.getState() == JobStatus.State.KILLED) {
307            System.out
308                .println("The job " + jobid + " has already been killed.");
309            exitCode = -1;
310          } else if (jobStatus.getState() == JobStatus.State.SUCCEEDED) {
311            System.out.println("Could not kill the job " + jobid
312                + ", as it has already succeeded.");
313            exitCode = -1;
314          } else {
315            job.killJob();
316            System.out.println("Killed job " + jobid);
317            exitCode = 0;
318          }
319        }
320      } else if (setJobPriority) {
321        Job job = cluster.getJob(JobID.forName(jobid));
322        if (job == null) {
323          System.out.println("Could not find job " + jobid);
324        } else {
325          job.setPriority(jp);
326          System.out.println("Changed job priority.");
327          exitCode = 0;
328        } 
329      } else if (viewHistory) {
330        viewHistory(historyFile, viewAllHistory);
331        exitCode = 0;
332      } else if (listEvents) {
333        listEvents(cluster.getJob(JobID.forName(jobid)), fromEvent, nEvents);
334        exitCode = 0;
335      } else if (listJobs) {
336        listJobs(cluster);
337        exitCode = 0;
338      } else if (listAllJobs) {
339        listAllJobs(cluster);
340        exitCode = 0;
341      } else if (listActiveTrackers) {
342        listActiveTrackers(cluster);
343        exitCode = 0;
344      } else if (listBlacklistedTrackers) {
345        listBlacklistedTrackers(cluster);
346        exitCode = 0;
347      } else if (displayTasks) {
348        displayTasks(cluster.getJob(JobID.forName(jobid)), taskType, taskState);
349        exitCode = 0;
350      } else if(killTask) {
351        TaskAttemptID taskID = TaskAttemptID.forName(taskid);
352        Job job = cluster.getJob(taskID.getJobID());
353        if (job == null) {
354          System.out.println("Could not find job " + jobid);
355        } else if (job.killTask(taskID, false)) {
356          System.out.println("Killed task " + taskid);
357          exitCode = 0;
358        } else {
359          System.out.println("Could not kill task " + taskid);
360          exitCode = -1;
361        }
362      } else if(failTask) {
363        TaskAttemptID taskID = TaskAttemptID.forName(taskid);
364        Job job = cluster.getJob(taskID.getJobID());
365        if (job == null) {
366            System.out.println("Could not find job " + jobid);
367        } else if(job.killTask(taskID, true)) {
368          System.out.println("Killed task " + taskID + " by failing it");
369          exitCode = 0;
370        } else {
371          System.out.println("Could not fail task " + taskid);
372          exitCode = -1;
373        }
374      } else if (logs) {
375        try {
376        JobID jobID = JobID.forName(jobid);
377        TaskAttemptID taskAttemptID = TaskAttemptID.forName(taskid);
378        LogParams logParams = cluster.getLogParams(jobID, taskAttemptID);
379        LogCLIHelpers logDumper = new LogCLIHelpers();
380        logDumper.setConf(getConf());
381        exitCode = logDumper.dumpAContainersLogs(logParams.getApplicationId(),
382            logParams.getContainerId(), logParams.getNodeId(),
383            logParams.getOwner());
384        } catch (IOException e) {
385          if (e instanceof RemoteException) {
386            throw e;
387          } 
388          System.out.println(e.getMessage());
389        }
390      }
391    } catch (RemoteException re) {
392      IOException unwrappedException = re.unwrapRemoteException();
393      if (unwrappedException instanceof AccessControlException) {
394        System.out.println(unwrappedException.getMessage());
395      } else {
396        throw re;
397      }
398    } finally {
399      cluster.close();
400    }
401    return exitCode;
402  }
403
404  Cluster createCluster() throws IOException {
405    return new Cluster(getConf());
406  }
407
408  private String getJobPriorityNames() {
409    StringBuffer sb = new StringBuffer();
410    for (JobPriority p : JobPriority.values()) {
411      sb.append(p.name()).append(" ");
412    }
413    return sb.substring(0, sb.length()-1);
414  }
415
416  private String getTaskTypes() {
417    return StringUtils.join(taskTypes, " ");
418  }
419
420  /**
421   * Display usage of the command-line tool and terminate execution.
422   */
423  private void displayUsage(String cmd) {
424    String prefix = "Usage: CLI ";
425    String jobPriorityValues = getJobPriorityNames();
426    String taskStates = "running, completed";
427
428    if ("-submit".equals(cmd)) {
429      System.err.println(prefix + "[" + cmd + " <job-file>]");
430    } else if ("-status".equals(cmd) || "-kill".equals(cmd)) {
431      System.err.println(prefix + "[" + cmd + " <job-id>]");
432    } else if ("-counter".equals(cmd)) {
433      System.err.println(prefix + "[" + cmd + 
434        " <job-id> <group-name> <counter-name>]");
435    } else if ("-events".equals(cmd)) {
436      System.err.println(prefix + "[" + cmd + 
437        " <job-id> <from-event-#> <#-of-events>]. Event #s start from 1.");
438    } else if ("-history".equals(cmd)) {
439      System.err.println(prefix + "[" + cmd + " <jobHistoryFile>]");
440    } else if ("-list".equals(cmd)) {
441      System.err.println(prefix + "[" + cmd + " [all]]");
442    } else if ("-kill-task".equals(cmd) || "-fail-task".equals(cmd)) {
443      System.err.println(prefix + "[" + cmd + " <task-attempt-id>]");
444    } else if ("-set-priority".equals(cmd)) {
445      System.err.println(prefix + "[" + cmd + " <job-id> <priority>]. " +
446          "Valid values for priorities are: " 
447          + jobPriorityValues); 
448    } else if ("-list-active-trackers".equals(cmd)) {
449      System.err.println(prefix + "[" + cmd + "]");
450    } else if ("-list-blacklisted-trackers".equals(cmd)) {
451      System.err.println(prefix + "[" + cmd + "]");
452    } else if ("-list-attempt-ids".equals(cmd)) {
453      System.err.println(prefix + "[" + cmd + 
454          " <job-id> <task-type> <task-state>]. " +
455          "Valid values for <task-type> are " + getTaskTypes() + ". " +
456          "Valid values for <task-state> are " + taskStates);
457    } else if ("-logs".equals(cmd)) {
458      System.err.println(prefix + "[" + cmd +
459          " <job-id> <task-attempt-id>]. " +
460          " <task-attempt-id> is optional to get task attempt logs.");      
461    } else {
462      System.err.printf(prefix + "<command> <args>%n");
463      System.err.printf("\t[-submit <job-file>]%n");
464      System.err.printf("\t[-status <job-id>]%n");
465      System.err.printf("\t[-counter <job-id> <group-name> <counter-name>]%n");
466      System.err.printf("\t[-kill <job-id>]%n");
467      System.err.printf("\t[-set-priority <job-id> <priority>]. " +
468        "Valid values for priorities are: " + jobPriorityValues + "%n");
469      System.err.printf("\t[-events <job-id> <from-event-#> <#-of-events>]%n");
470      System.err.printf("\t[-history <jobHistoryFile>]%n");
471      System.err.printf("\t[-list [all]]%n");
472      System.err.printf("\t[-list-active-trackers]%n");
473      System.err.printf("\t[-list-blacklisted-trackers]%n");
474      System.err.println("\t[-list-attempt-ids <job-id> <task-type> " +
475        "<task-state>]. " +
476        "Valid values for <task-type> are " + getTaskTypes() + ". " +
477        "Valid values for <task-state> are " + taskStates);
478      System.err.printf("\t[-kill-task <task-attempt-id>]%n");
479      System.err.printf("\t[-fail-task <task-attempt-id>]%n");
480      System.err.printf("\t[-logs <job-id> <task-attempt-id>]%n%n");
481      ToolRunner.printGenericCommandUsage(System.out);
482    }
483  }
484    
485  private void viewHistory(String historyFile, boolean all) 
486    throws IOException {
487    HistoryViewer historyViewer = new HistoryViewer(historyFile,
488                                        getConf(), all);
489    historyViewer.print();
490  }
491
492  protected long getCounter(Counters counters, String counterGroupName,
493      String counterName) throws IOException {
494    return counters.findCounter(counterGroupName, counterName).getValue();
495  }
496  
497  /**
498   * List the events for the given job
499   * @param jobId the job id for the job's events to list
500   * @throws IOException
501   */
502  private void listEvents(Job job, int fromEventId, int numEvents)
503      throws IOException, InterruptedException {
504    TaskCompletionEvent[] events = job.
505      getTaskCompletionEvents(fromEventId, numEvents);
506    System.out.println("Task completion events for " + job.getJobID());
507    System.out.println("Number of events (from " + fromEventId + ") are: " 
508      + events.length);
509    for(TaskCompletionEvent event: events) {
510      System.out.println(event.getStatus() + " " + 
511        event.getTaskAttemptId() + " " + 
512        getTaskLogURL(event.getTaskAttemptId(), event.getTaskTrackerHttp()));
513    }
514  }
515
516  protected static String getTaskLogURL(TaskAttemptID taskId, String baseUrl) {
517    return (baseUrl + "/tasklog?plaintext=true&attemptid=" + taskId); 
518  }
519  
520
521  /**
522   * Dump a list of currently running jobs
523   * @throws IOException
524   */
525  private void listJobs(Cluster cluster) 
526      throws IOException, InterruptedException {
527    List<JobStatus> runningJobs = new ArrayList<JobStatus>();
528    for (JobStatus job : cluster.getAllJobStatuses()) {
529      if (!job.isJobComplete()) {
530        runningJobs.add(job);
531      }
532    }
533    displayJobList(runningJobs.toArray(new JobStatus[0]));
534  }
535    
536  /**
537   * Dump a list of all jobs submitted.
538   * @throws IOException
539   */
540  private void listAllJobs(Cluster cluster) 
541      throws IOException, InterruptedException {
542    displayJobList(cluster.getAllJobStatuses());
543  }
544  
545  /**
546   * Display the list of active trackers
547   */
548  private void listActiveTrackers(Cluster cluster) 
549      throws IOException, InterruptedException {
550    TaskTrackerInfo[] trackers = cluster.getActiveTaskTrackers();
551    for (TaskTrackerInfo tracker : trackers) {
552      System.out.println(tracker.getTaskTrackerName());
553    }
554  }
555
556  /**
557   * Display the list of blacklisted trackers
558   */
559  private void listBlacklistedTrackers(Cluster cluster) 
560      throws IOException, InterruptedException {
561    TaskTrackerInfo[] trackers = cluster.getBlackListedTaskTrackers();
562    if (trackers.length > 0) {
563      System.out.println("BlackListedNode \t Reason");
564    }
565    for (TaskTrackerInfo tracker : trackers) {
566      System.out.println(tracker.getTaskTrackerName() + "\t" + 
567        tracker.getReasonForBlacklist());
568    }
569  }
570
571  private void printTaskAttempts(TaskReport report) {
572    if (report.getCurrentStatus() == TIPStatus.COMPLETE) {
573      System.out.println(report.getSuccessfulTaskAttemptId());
574    } else if (report.getCurrentStatus() == TIPStatus.RUNNING) {
575      for (TaskAttemptID t : 
576        report.getRunningTaskAttemptIds()) {
577        System.out.println(t);
578      }
579    }
580  }
581
582  /**
583   * Display the information about a job's tasks, of a particular type and
584   * in a particular state
585   * 
586   * @param job the job
587   * @param type the type of the task (map/reduce/setup/cleanup)
588   * @param state the state of the task 
589   * (pending/running/completed/failed/killed)
590   */
591  protected void displayTasks(Job job, String type, String state) 
592  throws IOException, InterruptedException {
593    TaskReport[] reports = job.getTaskReports(TaskType.valueOf(
594        org.apache.hadoop.util.StringUtils.toUpperCase(type)));
595    for (TaskReport report : reports) {
596      TIPStatus status = report.getCurrentStatus();
597      if ((state.equalsIgnoreCase("pending") && status ==TIPStatus.PENDING) ||
598          (state.equalsIgnoreCase("running") && status ==TIPStatus.RUNNING) ||
599          (state.equalsIgnoreCase("completed") && status == TIPStatus.COMPLETE) ||
600          (state.equalsIgnoreCase("failed") && status == TIPStatus.FAILED) ||
601          (state.equalsIgnoreCase("killed") && status == TIPStatus.KILLED)) {
602        printTaskAttempts(report);
603      }
604    }
605  }
606
607  public void displayJobList(JobStatus[] jobs) 
608      throws IOException, InterruptedException {
609    displayJobList(jobs, new PrintWriter(new OutputStreamWriter(System.out,
610        Charsets.UTF_8)));
611  }
612
613  @Private
614  public static String headerPattern = "%23s\t%10s\t%14s\t%12s\t%12s\t%10s\t%15s\t%15s\t%8s\t%8s\t%10s\t%10s\n";
615  @Private
616  public static String dataPattern   = "%23s\t%10s\t%14d\t%12s\t%12s\t%10s\t%15s\t%15s\t%8s\t%8s\t%10s\t%10s\n";
617  private static String memPattern   = "%dM";
618  private static String UNAVAILABLE  = "N/A";
619
620  @Private
621  public void displayJobList(JobStatus[] jobs, PrintWriter writer) {
622    writer.println("Total jobs:" + jobs.length);
623    writer.printf(headerPattern, "JobId", "State", "StartTime", "UserName",
624      "Queue", "Priority", "UsedContainers",
625      "RsvdContainers", "UsedMem", "RsvdMem", "NeededMem", "AM info");
626    for (JobStatus job : jobs) {
627      int numUsedSlots = job.getNumUsedSlots();
628      int numReservedSlots = job.getNumReservedSlots();
629      int usedMem = job.getUsedMem();
630      int rsvdMem = job.getReservedMem();
631      int neededMem = job.getNeededMem();
632      writer.printf(dataPattern,
633          job.getJobID().toString(), job.getState(), job.getStartTime(),
634          job.getUsername(), job.getQueue(), 
635          job.getPriority().name(),
636          numUsedSlots < 0 ? UNAVAILABLE : numUsedSlots,
637          numReservedSlots < 0 ? UNAVAILABLE : numReservedSlots,
638          usedMem < 0 ? UNAVAILABLE : String.format(memPattern, usedMem),
639          rsvdMem < 0 ? UNAVAILABLE : String.format(memPattern, rsvdMem),
640          neededMem < 0 ? UNAVAILABLE : String.format(memPattern, neededMem),
641          job.getSchedulingInfo());
642    }
643    writer.flush();
644  }
645  
646  public static void main(String[] argv) throws Exception {
647    int res = ToolRunner.run(new CLI(), argv);
648    ExitUtil.terminate(res);
649  }
650}