1 /** 2 * Licensed to the Apache Software Foundation (ASF) under one 3 * or more contributor license agreements. See the NOTICE file 4 * distributed with this work for additional information 5 * regarding copyright ownership. The ASF licenses this file 6 * to you under the Apache License, Version 2.0 (the 7 * "License"); you may not use this file except in compliance 8 * with the License. You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 */ 18 19 package org.apache.hadoop.mapred; 20 21 import java.io.BufferedOutputStream; 22 import java.io.BufferedReader; 23 import java.io.DataOutputStream; 24 import java.io.File; 25 import java.io.FileInputStream; 26 import java.io.Flushable; 27 import java.io.IOException; 28 import java.io.InputStream; 29 import java.io.InputStreamReader; 30 import java.util.ArrayList; 31 import java.util.Enumeration; 32 import java.util.List; 33 import java.util.concurrent.Executors; 34 import java.util.concurrent.ScheduledExecutorService; 35 import java.util.concurrent.ThreadFactory; 36 import java.util.concurrent.TimeUnit; 37 38 import org.apache.commons.logging.Log; 39 import org.apache.commons.logging.LogFactory; 40 import org.apache.hadoop.classification.InterfaceAudience; 41 import org.apache.hadoop.conf.Configuration; 42 import org.apache.hadoop.fs.FileStatus; 43 import org.apache.hadoop.fs.FileSystem; 44 import org.apache.hadoop.fs.FileUtil; 45 import org.apache.hadoop.fs.LocalFileSystem; 46 import org.apache.hadoop.fs.Path; 47 import org.apache.hadoop.io.IOUtils; 48 import org.apache.hadoop.io.SecureIOUtils; 49 import org.apache.hadoop.mapreduce.JobID; 50 import org.apache.hadoop.mapreduce.util.ProcessTree; 51 import org.apache.hadoop.util.Shell; 52 import org.apache.hadoop.util.StringUtils; 53 import org.apache.hadoop.util.ShutdownHookManager; 54 import org.apache.hadoop.yarn.conf.YarnConfiguration; 55 import org.apache.log4j.Appender; 56 import org.apache.log4j.LogManager; 57 import org.apache.log4j.Logger; 58 59 import com.google.common.base.Charsets; 60 61 /** 62 * A simple logger to handle the task-specific user logs. 63 * This class uses the system property <code>hadoop.log.dir</code>. 64 * 65 */ 66 @InterfaceAudience.Private 67 public class TaskLog { 68 private static final Log LOG = 69 LogFactory.getLog(TaskLog.class); 70 71 static final String USERLOGS_DIR_NAME = "userlogs"; 72 73 private static final File LOG_DIR = 74 new File(getBaseLogDir(), USERLOGS_DIR_NAME).getAbsoluteFile(); 75 76 // localFS is set in (and used by) writeToIndexFile() 77 static LocalFileSystem localFS = null; 78 getMRv2LogDir()79 public static String getMRv2LogDir() { 80 return System.getProperty(YarnConfiguration.YARN_APP_CONTAINER_LOG_DIR); 81 } 82 getTaskLogFile(TaskAttemptID taskid, boolean isCleanup, LogName filter)83 public static File getTaskLogFile(TaskAttemptID taskid, boolean isCleanup, 84 LogName filter) { 85 if (getMRv2LogDir() != null) { 86 return new File(getMRv2LogDir(), filter.toString()); 87 } else { 88 return new File(getAttemptDir(taskid, isCleanup), filter.toString()); 89 } 90 } 91 getRealTaskLogFileLocation(TaskAttemptID taskid, boolean isCleanup, LogName filter)92 static File getRealTaskLogFileLocation(TaskAttemptID taskid, 93 boolean isCleanup, LogName filter) { 94 LogFileDetail l; 95 try { 96 l = getLogFileDetail(taskid, filter, isCleanup); 97 } catch (IOException ie) { 98 LOG.error("getTaskLogFileDetail threw an exception " + ie); 99 return null; 100 } 101 return new File(l.location, filter.toString()); 102 } 103 private static class LogFileDetail { 104 final static String LOCATION = "LOG_DIR:"; 105 String location; 106 long start; 107 long length; 108 } 109 getLogFileDetail(TaskAttemptID taskid, LogName filter, boolean isCleanup)110 private static LogFileDetail getLogFileDetail(TaskAttemptID taskid, 111 LogName filter, 112 boolean isCleanup) 113 throws IOException { 114 File indexFile = getIndexFile(taskid, isCleanup); 115 BufferedReader fis = new BufferedReader(new InputStreamReader( 116 SecureIOUtils.openForRead(indexFile, obtainLogDirOwner(taskid), null), 117 Charsets.UTF_8)); 118 //the format of the index file is 119 //LOG_DIR: <the dir where the task logs are really stored> 120 //stdout:<start-offset in the stdout file> <length> 121 //stderr:<start-offset in the stderr file> <length> 122 //syslog:<start-offset in the syslog file> <length> 123 LogFileDetail l = new LogFileDetail(); 124 String str = null; 125 try { 126 str = fis.readLine(); 127 if (str == null) { // the file doesn't have anything 128 throw new IOException("Index file for the log of " + taskid 129 + " doesn't exist."); 130 } 131 l.location = str.substring(str.indexOf(LogFileDetail.LOCATION) 132 + LogFileDetail.LOCATION.length()); 133 // special cases are the debugout and profile.out files. They are 134 // guaranteed 135 // to be associated with each task attempt since jvm reuse is disabled 136 // when profiling/debugging is enabled 137 if (filter.equals(LogName.DEBUGOUT) || filter.equals(LogName.PROFILE)) { 138 l.length = new File(l.location, filter.toString()).length(); 139 l.start = 0; 140 fis.close(); 141 return l; 142 } 143 str = fis.readLine(); 144 while (str != null) { 145 // look for the exact line containing the logname 146 if (str.contains(filter.toString())) { 147 str = str.substring(filter.toString().length() + 1); 148 String[] startAndLen = str.split(" "); 149 l.start = Long.parseLong(startAndLen[0]); 150 l.length = Long.parseLong(startAndLen[1]); 151 break; 152 } 153 str = fis.readLine(); 154 } 155 fis.close(); 156 fis = null; 157 } finally { 158 IOUtils.cleanup(LOG, fis); 159 } 160 return l; 161 } 162 getTmpIndexFile(TaskAttemptID taskid, boolean isCleanup)163 private static File getTmpIndexFile(TaskAttemptID taskid, boolean isCleanup) { 164 return new File(getAttemptDir(taskid, isCleanup), "log.tmp"); 165 } 166 getIndexFile(TaskAttemptID taskid, boolean isCleanup)167 static File getIndexFile(TaskAttemptID taskid, boolean isCleanup) { 168 return new File(getAttemptDir(taskid, isCleanup), "log.index"); 169 } 170 171 /** 172 * Obtain the owner of the log dir. This is 173 * determined by checking the job's log directory. 174 */ obtainLogDirOwner(TaskAttemptID taskid)175 static String obtainLogDirOwner(TaskAttemptID taskid) throws IOException { 176 Configuration conf = new Configuration(); 177 FileSystem raw = FileSystem.getLocal(conf).getRaw(); 178 Path jobLogDir = new Path(getJobDir(taskid.getJobID()).getAbsolutePath()); 179 FileStatus jobStat = raw.getFileStatus(jobLogDir); 180 return jobStat.getOwner(); 181 } 182 getBaseLogDir()183 static String getBaseLogDir() { 184 return System.getProperty("hadoop.log.dir"); 185 } 186 getAttemptDir(TaskAttemptID taskid, boolean isCleanup)187 static File getAttemptDir(TaskAttemptID taskid, boolean isCleanup) { 188 String cleanupSuffix = isCleanup ? ".cleanup" : ""; 189 return new File(getJobDir(taskid.getJobID()), taskid + cleanupSuffix); 190 } 191 private static long prevOutLength; 192 private static long prevErrLength; 193 private static long prevLogLength; 194 195 private static synchronized writeToIndexFile(String logLocation, boolean isCleanup)196 void writeToIndexFile(String logLocation, 197 boolean isCleanup) throws IOException { 198 // To ensure atomicity of updates to index file, write to temporary index 199 // file first and then rename. 200 File tmpIndexFile = getTmpIndexFile(currentTaskid, isCleanup); 201 202 BufferedOutputStream bos = null; 203 DataOutputStream dos = null; 204 try{ 205 bos = new BufferedOutputStream( 206 SecureIOUtils.createForWrite(tmpIndexFile, 0644)); 207 dos = new DataOutputStream(bos); 208 //the format of the index file is 209 //LOG_DIR: <the dir where the task logs are really stored> 210 //STDOUT: <start-offset in the stdout file> <length> 211 //STDERR: <start-offset in the stderr file> <length> 212 //SYSLOG: <start-offset in the syslog file> <length> 213 214 dos.writeBytes(LogFileDetail.LOCATION + logLocation + "\n" 215 + LogName.STDOUT.toString() + ":"); 216 dos.writeBytes(Long.toString(prevOutLength) + " "); 217 dos.writeBytes(Long.toString(new File(logLocation, LogName.STDOUT 218 .toString()).length() - prevOutLength) 219 + "\n" + LogName.STDERR + ":"); 220 dos.writeBytes(Long.toString(prevErrLength) + " "); 221 dos.writeBytes(Long.toString(new File(logLocation, LogName.STDERR 222 .toString()).length() - prevErrLength) 223 + "\n" + LogName.SYSLOG.toString() + ":"); 224 dos.writeBytes(Long.toString(prevLogLength) + " "); 225 dos.writeBytes(Long.toString(new File(logLocation, LogName.SYSLOG 226 .toString()).length() - prevLogLength) 227 + "\n"); 228 dos.close(); 229 dos = null; 230 bos.close(); 231 bos = null; 232 } finally { 233 IOUtils.cleanup(LOG, dos, bos); 234 } 235 236 File indexFile = getIndexFile(currentTaskid, isCleanup); 237 Path indexFilePath = new Path(indexFile.getAbsolutePath()); 238 Path tmpIndexFilePath = new Path(tmpIndexFile.getAbsolutePath()); 239 240 if (localFS == null) {// set localFS once 241 localFS = FileSystem.getLocal(new Configuration()); 242 } 243 localFS.rename (tmpIndexFilePath, indexFilePath); 244 } resetPrevLengths(String logLocation)245 private static void resetPrevLengths(String logLocation) { 246 prevOutLength = new File(logLocation, LogName.STDOUT.toString()).length(); 247 prevErrLength = new File(logLocation, LogName.STDERR.toString()).length(); 248 prevLogLength = new File(logLocation, LogName.SYSLOG.toString()).length(); 249 } 250 private volatile static TaskAttemptID currentTaskid = null; 251 252 @SuppressWarnings("unchecked") syncLogs(String logLocation, TaskAttemptID taskid, boolean isCleanup)253 public synchronized static void syncLogs(String logLocation, 254 TaskAttemptID taskid, 255 boolean isCleanup) 256 throws IOException { 257 System.out.flush(); 258 System.err.flush(); 259 Enumeration<Logger> allLoggers = LogManager.getCurrentLoggers(); 260 while (allLoggers.hasMoreElements()) { 261 Logger l = allLoggers.nextElement(); 262 Enumeration<Appender> allAppenders = l.getAllAppenders(); 263 while (allAppenders.hasMoreElements()) { 264 Appender a = allAppenders.nextElement(); 265 if (a instanceof TaskLogAppender) { 266 ((TaskLogAppender)a).flush(); 267 } 268 } 269 } 270 if (currentTaskid != taskid) { 271 currentTaskid = taskid; 272 resetPrevLengths(logLocation); 273 } 274 writeToIndexFile(logLocation, isCleanup); 275 } 276 syncLogsShutdown( ScheduledExecutorService scheduler)277 public static synchronized void syncLogsShutdown( 278 ScheduledExecutorService scheduler) 279 { 280 // flush standard streams 281 // 282 System.out.flush(); 283 System.err.flush(); 284 285 if (scheduler != null) { 286 scheduler.shutdownNow(); 287 } 288 289 // flush & close all appenders 290 LogManager.shutdown(); 291 } 292 293 @SuppressWarnings("unchecked") syncLogs()294 public static synchronized void syncLogs() { 295 // flush standard streams 296 // 297 System.out.flush(); 298 System.err.flush(); 299 300 // flush flushable appenders 301 // 302 final Logger rootLogger = Logger.getRootLogger(); 303 flushAppenders(rootLogger); 304 final Enumeration<Logger> allLoggers = rootLogger.getLoggerRepository(). 305 getCurrentLoggers(); 306 while (allLoggers.hasMoreElements()) { 307 final Logger l = allLoggers.nextElement(); 308 flushAppenders(l); 309 } 310 } 311 312 @SuppressWarnings("unchecked") flushAppenders(Logger l)313 private static void flushAppenders(Logger l) { 314 final Enumeration<Appender> allAppenders = l.getAllAppenders(); 315 while (allAppenders.hasMoreElements()) { 316 final Appender a = allAppenders.nextElement(); 317 if (a instanceof Flushable) { 318 try { 319 ((Flushable) a).flush(); 320 } catch (IOException ioe) { 321 System.err.println(a + ": Failed to flush!" 322 + StringUtils.stringifyException(ioe)); 323 } 324 } 325 } 326 } 327 createLogSyncer()328 public static ScheduledExecutorService createLogSyncer() { 329 final ScheduledExecutorService scheduler = 330 Executors.newSingleThreadScheduledExecutor( 331 new ThreadFactory() { 332 @Override 333 public Thread newThread(Runnable r) { 334 final Thread t = Executors.defaultThreadFactory().newThread(r); 335 t.setDaemon(true); 336 t.setName("Thread for syncLogs"); 337 return t; 338 } 339 }); 340 ShutdownHookManager.get().addShutdownHook(new Runnable() { 341 @Override 342 public void run() { 343 TaskLog.syncLogsShutdown(scheduler); 344 } 345 }, 50); 346 scheduler.scheduleWithFixedDelay( 347 new Runnable() { 348 @Override 349 public void run() { 350 TaskLog.syncLogs(); 351 } 352 }, 0L, 5L, TimeUnit.SECONDS); 353 return scheduler; 354 } 355 356 /** 357 * The filter for userlogs. 358 */ 359 @InterfaceAudience.Private 360 public static enum LogName { 361 /** Log on the stdout of the task. */ 362 STDOUT ("stdout"), 363 364 /** Log on the stderr of the task. */ 365 STDERR ("stderr"), 366 367 /** Log on the map-reduce system logs of the task. */ 368 SYSLOG ("syslog"), 369 370 /** The java profiler information. */ 371 PROFILE ("profile.out"), 372 373 /** Log the debug script's stdout */ 374 DEBUGOUT ("debugout"); 375 376 private String prefix; 377 LogName(String prefix)378 private LogName(String prefix) { 379 this.prefix = prefix; 380 } 381 382 @Override toString()383 public String toString() { 384 return prefix; 385 } 386 } 387 388 public static class Reader extends InputStream { 389 private long bytesRemaining; 390 private FileInputStream file; 391 392 /** 393 * Read a log file from start to end positions. The offsets may be negative, 394 * in which case they are relative to the end of the file. For example, 395 * Reader(taskid, kind, 0, -1) is the entire file and 396 * Reader(taskid, kind, -4197, -1) is the last 4196 bytes. 397 * @param taskid the id of the task to read the log file for 398 * @param kind the kind of log to read 399 * @param start the offset to read from (negative is relative to tail) 400 * @param end the offset to read upto (negative is relative to tail) 401 * @param isCleanup whether the attempt is cleanup attempt or not 402 * @throws IOException 403 */ Reader(TaskAttemptID taskid, LogName kind, long start, long end, boolean isCleanup)404 public Reader(TaskAttemptID taskid, LogName kind, 405 long start, long end, boolean isCleanup) throws IOException { 406 // find the right log file 407 LogFileDetail fileDetail = getLogFileDetail(taskid, kind, isCleanup); 408 // calculate the start and stop 409 long size = fileDetail.length; 410 if (start < 0) { 411 start += size + 1; 412 } 413 if (end < 0) { 414 end += size + 1; 415 } 416 start = Math.max(0, Math.min(start, size)); 417 end = Math.max(0, Math.min(end, size)); 418 start += fileDetail.start; 419 end += fileDetail.start; 420 bytesRemaining = end - start; 421 String owner = obtainLogDirOwner(taskid); 422 file = SecureIOUtils.openForRead(new File(fileDetail.location, kind.toString()), 423 owner, null); 424 // skip upto start 425 long pos = 0; 426 while (pos < start) { 427 long result = file.skip(start - pos); 428 if (result < 0) { 429 bytesRemaining = 0; 430 break; 431 } 432 pos += result; 433 } 434 } 435 436 @Override read()437 public int read() throws IOException { 438 int result = -1; 439 if (bytesRemaining > 0) { 440 bytesRemaining -= 1; 441 result = file.read(); 442 } 443 return result; 444 } 445 446 @Override read(byte[] buffer, int offset, int length)447 public int read(byte[] buffer, int offset, int length) throws IOException { 448 length = (int) Math.min(length, bytesRemaining); 449 int bytes = file.read(buffer, offset, length); 450 if (bytes > 0) { 451 bytesRemaining -= bytes; 452 } 453 return bytes; 454 } 455 456 @Override available()457 public int available() throws IOException { 458 return (int) Math.min(bytesRemaining, file.available()); 459 } 460 461 @Override close()462 public void close() throws IOException { 463 file.close(); 464 } 465 } 466 467 private static final String bashCommand = "bash"; 468 private static final String tailCommand = "tail"; 469 470 /** 471 * Get the desired maximum length of task's logs. 472 * @param conf the job to look in 473 * @return the number of bytes to cap the log files at 474 */ getTaskLogLength(JobConf conf)475 public static long getTaskLogLength(JobConf conf) { 476 return getTaskLogLimitBytes(conf); 477 } 478 getTaskLogLimitBytes(Configuration conf)479 public static long getTaskLogLimitBytes(Configuration conf) { 480 return conf.getLong(JobContext.TASK_USERLOG_LIMIT, 0) * 1024; 481 } 482 483 484 /** 485 * Wrap a command in a shell to capture stdout and stderr to files. 486 * Setup commands such as setting memory limit can be passed which 487 * will be executed before exec. 488 * If the tailLength is 0, the entire output will be saved. 489 * @param setup The setup commands for the execed process. 490 * @param cmd The command and the arguments that should be run 491 * @param stdoutFilename The filename that stdout should be saved to 492 * @param stderrFilename The filename that stderr should be saved to 493 * @param tailLength The length of the tail to be saved. 494 * @param useSetsid Should setsid be used in the command or not. 495 * @return the modified command that should be run 496 */ captureOutAndError(List<String> setup, List<String> cmd, File stdoutFilename, File stderrFilename, long tailLength, boolean useSetsid )497 public static List<String> captureOutAndError(List<String> setup, 498 List<String> cmd, 499 File stdoutFilename, 500 File stderrFilename, 501 long tailLength, 502 boolean useSetsid 503 ) throws IOException { 504 List<String> result = new ArrayList<String>(3); 505 result.add(bashCommand); 506 result.add("-c"); 507 String mergedCmd = buildCommandLine(setup, cmd, stdoutFilename, 508 stderrFilename, tailLength, 509 useSetsid); 510 result.add(mergedCmd); 511 return result; 512 } 513 514 /** 515 * Construct the command line for running the task JVM 516 * @param setup The setup commands for the execed process. 517 * @param cmd The command and the arguments that should be run 518 * @param stdoutFilename The filename that stdout should be saved to 519 * @param stderrFilename The filename that stderr should be saved to 520 * @param tailLength The length of the tail to be saved. 521 * @return the command line as a String 522 * @throws IOException 523 */ buildCommandLine(List<String> setup, List<String> cmd, File stdoutFilename, File stderrFilename, long tailLength, boolean useSetsid)524 static String buildCommandLine(List<String> setup, List<String> cmd, 525 File stdoutFilename, 526 File stderrFilename, 527 long tailLength, 528 boolean useSetsid) 529 throws IOException { 530 531 String stdout = FileUtil.makeShellPath(stdoutFilename); 532 String stderr = FileUtil.makeShellPath(stderrFilename); 533 StringBuffer mergedCmd = new StringBuffer(); 534 535 // Export the pid of taskJvm to env variable JVM_PID. 536 // Currently pid is not used on Windows 537 if (!Shell.WINDOWS) { 538 mergedCmd.append(" export JVM_PID=`echo $$` ; "); 539 } 540 541 if (setup != null && setup.size() > 0) { 542 mergedCmd.append(addCommand(setup, false)); 543 mergedCmd.append(";"); 544 } 545 if (tailLength > 0) { 546 mergedCmd.append("("); 547 } else if(ProcessTree.isSetsidAvailable && useSetsid && 548 !Shell.WINDOWS) { 549 mergedCmd.append("exec setsid "); 550 } else { 551 mergedCmd.append("exec "); 552 } 553 mergedCmd.append(addCommand(cmd, true)); 554 mergedCmd.append(" < /dev/null "); 555 if (tailLength > 0) { 556 mergedCmd.append(" | "); 557 mergedCmd.append(tailCommand); 558 mergedCmd.append(" -c "); 559 mergedCmd.append(tailLength); 560 mergedCmd.append(" >> "); 561 mergedCmd.append(stdout); 562 mergedCmd.append(" ; exit $PIPESTATUS ) 2>&1 | "); 563 mergedCmd.append(tailCommand); 564 mergedCmd.append(" -c "); 565 mergedCmd.append(tailLength); 566 mergedCmd.append(" >> "); 567 mergedCmd.append(stderr); 568 mergedCmd.append(" ; exit $PIPESTATUS"); 569 } else { 570 mergedCmd.append(" 1>> "); 571 mergedCmd.append(stdout); 572 mergedCmd.append(" 2>> "); 573 mergedCmd.append(stderr); 574 } 575 return mergedCmd.toString(); 576 } 577 578 /** 579 * Construct the command line for running the debug script 580 * @param cmd The command and the arguments that should be run 581 * @param stdoutFilename The filename that stdout should be saved to 582 * @param stderrFilename The filename that stderr should be saved to 583 * @param tailLength The length of the tail to be saved. 584 * @return the command line as a String 585 * @throws IOException 586 */ buildDebugScriptCommandLine(List<String> cmd, String debugout)587 static String buildDebugScriptCommandLine(List<String> cmd, String debugout) 588 throws IOException { 589 StringBuilder mergedCmd = new StringBuilder(); 590 mergedCmd.append("exec "); 591 boolean isExecutable = true; 592 for(String s: cmd) { 593 if (isExecutable) { 594 // the executable name needs to be expressed as a shell path for the 595 // shell to find it. 596 mergedCmd.append(FileUtil.makeShellPath(new File(s))); 597 isExecutable = false; 598 } else { 599 mergedCmd.append(s); 600 } 601 mergedCmd.append(" "); 602 } 603 mergedCmd.append(" < /dev/null "); 604 mergedCmd.append(" >"); 605 mergedCmd.append(debugout); 606 mergedCmd.append(" 2>&1 "); 607 return mergedCmd.toString(); 608 } 609 /** 610 * Add quotes to each of the command strings and 611 * return as a single string 612 * @param cmd The command to be quoted 613 * @param isExecutable makes shell path if the first 614 * argument is executable 615 * @return returns The quoted string. 616 * @throws IOException 617 */ addCommand(List<String> cmd, boolean isExecutable)618 public static String addCommand(List<String> cmd, boolean isExecutable) 619 throws IOException { 620 StringBuffer command = new StringBuffer(); 621 for(String s: cmd) { 622 command.append('\''); 623 if (isExecutable) { 624 // the executable name needs to be expressed as a shell path for the 625 // shell to find it. 626 command.append(FileUtil.makeShellPath(new File(s))); 627 isExecutable = false; 628 } else { 629 command.append(s); 630 } 631 command.append('\''); 632 command.append(" "); 633 } 634 return command.toString(); 635 } 636 637 638 /** 639 * Method to return the location of user log directory. 640 * 641 * @return base log directory 642 */ getUserLogDir()643 static File getUserLogDir() { 644 if (!LOG_DIR.exists()) { 645 boolean b = LOG_DIR.mkdirs(); 646 if (!b) { 647 LOG.debug("mkdirs failed. Ignoring."); 648 } 649 } 650 return LOG_DIR; 651 } 652 653 /** 654 * Get the user log directory for the job jobid. 655 * 656 * @param jobid 657 * @return user log directory for the job 658 */ getJobDir(JobID jobid)659 public static File getJobDir(JobID jobid) { 660 return new File(getUserLogDir(), jobid.toString()); 661 } 662 663 } // TaskLog 664