47
48 import static jdk.nashorn.internal.runtime.CommandExecutor.RedirectType.*;
49 import static jdk.nashorn.internal.runtime.ECMAErrors.rangeError;
50
51 /**
52 * The CommandExecutor class provides support for Nashorn's $EXEC
53 * builtin function. CommandExecutor provides support for command parsing,
54 * I/O redirection, piping, completion timeouts, # comments, and simple
55 * environment variable management (cd, setenv, and unsetenv).
56 */
57 class CommandExecutor {
58 // Size of byte buffers used for piping.
59 private static final int BUFFER_SIZE = 1024;
60
61 // Test to see if running on Windows.
62 private static final boolean IS_WINDOWS =
63 AccessController.doPrivileged((PrivilegedAction<Boolean>)() -> {
64 return System.getProperty("os.name").contains("Windows");
65 });
66
67 // User's home directory
68 private static final String HOME_DIRECTORY =
69 AccessController.doPrivileged((PrivilegedAction<String>)() -> {
70 return System.getProperty("user.home");
71 });
72
73 // Various types of standard redirects.
74 enum RedirectType {
75 NO_REDIRECT,
76 REDIRECT_INPUT,
77 REDIRECT_OUTPUT,
78 REDIRECT_OUTPUT_APPEND,
79 REDIRECT_ERROR,
80 REDIRECT_ERROR_APPEND,
81 REDIRECT_OUTPUT_ERROR_APPEND,
82 REDIRECT_ERROR_TO_OUTPUT
83 };
84
85 // Prefix strings to standard redirects.
86 private static final String[] redirectPrefixes = new String[] {
371 * stripQuotes - strip quotes from token if present. Quoted tokens kept
372 * quotes to prevent search for redirects.
373 * @param token token to strip
374 * @return stripped token
375 */
376 private static String stripQuotes(String token) {
377 if ((token.startsWith("\"") && token.endsWith("\"")) ||
378 token.startsWith("\'") && token.endsWith("\'")) {
379 token = token.substring(1, token.length() - 1);
380 }
381 return token;
382 }
383
384 /**
385 * resolvePath - resolves a path against a current working directory.
386 * @param cwd current working directory
387 * @param fileName name of file or directory
388 * @return resolved Path to file
389 */
390 private static Path resolvePath(final String cwd, final String fileName) {
391 return Paths.get(cwd).resolve(fileName).normalize();
392 }
393
394 /**
395 * builtIn - checks to see if the command is a builtin and performs
396 * appropriate action.
397 * @param cmd current command
398 * @param cwd current working directory
399 * @return true if was a builtin command
400 */
401 private boolean builtIn(final List<String> cmd, final String cwd) {
402 switch (cmd.get(0)) {
403 // Set current working directory.
404 case "cd":
405 // If zero args then use home dirrectory as cwd else use first arg.
406 final String newCWD = cmd.size() < 2 ? HOME_DIRECTORY : cmd.get(1);
407 // Normalize the cwd
408 final Path cwdPath = resolvePath(cwd, newCWD);
409
410 // Check if is a directory.
411 final File file = cwdPath.toFile();
412 if (!file.exists()) {
413 reportError("file.not.exist", file.toString());
414 return true;
415 } else if (!file.isDirectory()) {
416 reportError("not.directory", file.toString());
417 return true;
418 }
419
420 // Set PWD environment variable to be picked up as cwd.
421 environment.put("PWD", cwdPath.toString());
422 return true;
423
424 // Set an environment variable.
425 case "setenv":
426 if (3 <= cmd.size()) {
427 final String key = cmd.get(1);
428 final String value = cmd.get(2);
429 environment.put(key, value);
430 }
431
432 return true;
433
434 // Unset an environment variable.
435 case "unsetenv":
436 if (2 <= cmd.size()) {
437 final String key = cmd.get(1);
438 environment.remove(key);
439 }
440
441 return true;
442 }
443
444 return false;
445 }
446
447 /**
448 * preprocessCommand - scan the command for redirects
449 * @param tokens command tokens
450 * @param cwd current working directory
451 * @param redirectInfo redirection information
452 * @return tokens remaining for actual command
453 */
454 private List<String> preprocessCommand(final List<String> tokens,
455 final String cwd, final RedirectInfo redirectInfo) {
456 // Tokens remaining for actual command.
457 final List<String> command = new ArrayList<>();
458
459 // iterate through all tokens.
460 final Iterator<String> iterator = tokens.iterator();
461 while (iterator.hasNext()) {
462 String token = iterator.next();
463
464 // Check if is a redirect.
465 if (redirectInfo.check(token, iterator, cwd)) {
466 // Don't add to the command.
467 continue;
468 }
469
470 // Strip quotes and add to command.
471 command.add(stripQuotes(token));
472 }
473
474 return command;
475 }
476
477 /**
478 * createProcessBuilder - create a ProcessBuilder for the command.
479 * @param command command tokens
480 * @param cwd current working directory
481 * @param redirectInfo redirect information
482 */
483 private void createProcessBuilder(final List<String> command,
484 final String cwd, final RedirectInfo redirectInfo) {
485 // Create new ProcessBuilder.
486 final ProcessBuilder pb = new ProcessBuilder(command);
487 // Set current working directory.
488 pb.directory(new File(cwd));
489
490 // Map environment variables.
491 final Map<String, String> processEnvironment = pb.environment();
492 processEnvironment.clear();
493 processEnvironment.putAll(environment);
494
495 // Apply redirects.
496 redirectInfo.apply(pb);
497 // Add to current list of commands.
498 processBuilders.add(pb);
499 }
500
501 /**
502 * command - process the command
503 * @param tokens tokens of the command
504 * @param isPiped true if the output of this command should be piped to the next
505 */
506 private void command(final List<String> tokens, boolean isPiped) {
507 // Test to see if we should echo the command to output.
508 if (envVarBooleanValue("JJS_ECHO")) {
509 System.out.println(String.join(" ", tokens));
510 }
511
512 // Get the current working directory.
513 final String cwd = envVarValue("PWD", HOME_DIRECTORY);
514 // Preprocess the command for redirects.
515 final RedirectInfo redirectInfo = new RedirectInfo();
516 final List<String> command = preprocessCommand(tokens, cwd, redirectInfo);
517
518 // Skip if empty or a built in.
519 if (command.isEmpty() || builtIn(command, cwd)) {
520 return;
521 }
522
523 // Create ProcessBuilder with cwd and redirects set.
524 createProcessBuilder(command, cwd, redirectInfo);
525
526 // If piped the wait for the next command.
527 if (isPiped) {
528 return;
529 }
530
531 // Fetch first and last ProcessBuilder.
532 final ProcessBuilder firstProcessBuilder = processBuilders.get(0);
533 final ProcessBuilder lastProcessBuilder = processBuilders.get(processBuilders.size() - 1);
534
535 // Determine which streams have not be redirected from pipes.
536 boolean inputIsPipe = firstProcessBuilder.redirectInput() == Redirect.PIPE;
537 boolean outputIsPipe = lastProcessBuilder.redirectOutput() == Redirect.PIPE;
538 boolean errorIsPipe = lastProcessBuilder.redirectError() == Redirect.PIPE;
539 boolean inheritIO = envVarBooleanValue("JJS_INHERIT_IO");
540
541 // If not redirected and inputStream is current processes' input.
542 if (inputIsPipe && (inheritIO || inputStream == System.in)) {
543 // Inherit current processes' input.
544 firstProcessBuilder.redirectInput(Redirect.INHERIT);
545 inputIsPipe = false;
546 }
748 // Partial token pending.
749 command.add(sb.append(token).toString());
750 sb.setLength(0);
751 }
752 }
753 } catch (final IOException ex) {
754 // Do nothing.
755 }
756
757 // Partial token pending.
758 if (sb.length() != 0) {
759 command.add(sb.toString());
760 }
761
762 // Process last command.
763 command(command, false);
764 }
765
766 /**
767 * process - process a command array of strings
768 * @param script command script to be processed
769 */
770 void process(final List<String> tokens) {
771 // Prepare to accumulate command tokens.
772 final List<String> command = new ArrayList<>();
773
774 // Iterate through tokens.
775 final Iterator<String> iterator = tokens.iterator();
776 while (iterator.hasNext() && exitCode == EXIT_SUCCESS) {
777 // Next word token.
778 String token = iterator.next();
779
780 if (token == null) {
781 continue;
782 }
783
784 switch (token) {
785 case "|":
786 // Process as a piped command.
787 command(command, true);
788 // Start with a new set of tokens.
|
47
48 import static jdk.nashorn.internal.runtime.CommandExecutor.RedirectType.*;
49 import static jdk.nashorn.internal.runtime.ECMAErrors.rangeError;
50
51 /**
52 * The CommandExecutor class provides support for Nashorn's $EXEC
53 * builtin function. CommandExecutor provides support for command parsing,
54 * I/O redirection, piping, completion timeouts, # comments, and simple
55 * environment variable management (cd, setenv, and unsetenv).
56 */
57 class CommandExecutor {
58 // Size of byte buffers used for piping.
59 private static final int BUFFER_SIZE = 1024;
60
61 // Test to see if running on Windows.
62 private static final boolean IS_WINDOWS =
63 AccessController.doPrivileged((PrivilegedAction<Boolean>)() -> {
64 return System.getProperty("os.name").contains("Windows");
65 });
66
67 // Cygwin drive alias prefix.
68 private static final String CYGDRIVE = "/cygdrive/";
69
70 // User's home directory
71 private static final String HOME_DIRECTORY =
72 AccessController.doPrivileged((PrivilegedAction<String>)() -> {
73 return System.getProperty("user.home");
74 });
75
76 // Various types of standard redirects.
77 enum RedirectType {
78 NO_REDIRECT,
79 REDIRECT_INPUT,
80 REDIRECT_OUTPUT,
81 REDIRECT_OUTPUT_APPEND,
82 REDIRECT_ERROR,
83 REDIRECT_ERROR_APPEND,
84 REDIRECT_OUTPUT_ERROR_APPEND,
85 REDIRECT_ERROR_TO_OUTPUT
86 };
87
88 // Prefix strings to standard redirects.
89 private static final String[] redirectPrefixes = new String[] {
374 * stripQuotes - strip quotes from token if present. Quoted tokens kept
375 * quotes to prevent search for redirects.
376 * @param token token to strip
377 * @return stripped token
378 */
379 private static String stripQuotes(String token) {
380 if ((token.startsWith("\"") && token.endsWith("\"")) ||
381 token.startsWith("\'") && token.endsWith("\'")) {
382 token = token.substring(1, token.length() - 1);
383 }
384 return token;
385 }
386
387 /**
388 * resolvePath - resolves a path against a current working directory.
389 * @param cwd current working directory
390 * @param fileName name of file or directory
391 * @return resolved Path to file
392 */
393 private static Path resolvePath(final String cwd, final String fileName) {
394 return Paths.get(sanitizePath(cwd)).resolve(fileName).normalize();
395 }
396
397 /**
398 * builtIn - checks to see if the command is a builtin and performs
399 * appropriate action.
400 * @param cmd current command
401 * @param cwd current working directory
402 * @return true if was a builtin command
403 */
404 private boolean builtIn(final List<String> cmd, final String cwd) {
405 switch (cmd.get(0)) {
406 // Set current working directory.
407 case "cd":
408 final boolean cygpath = IS_WINDOWS && cwd.startsWith(CYGDRIVE);
409 // If zero args then use home directory as cwd else use first arg.
410 final String newCWD = cmd.size() < 2 ? HOME_DIRECTORY : cmd.get(1);
411 // Normalize the cwd
412 final Path cwdPath = resolvePath(cwd, newCWD);
413
414 // Check if is a directory.
415 final File file = cwdPath.toFile();
416 if (!file.exists()) {
417 reportError("file.not.exist", file.toString());
418 return true;
419 } else if (!file.isDirectory()) {
420 reportError("not.directory", file.toString());
421 return true;
422 }
423
424 // Set PWD environment variable to be picked up as cwd.
425 // Make sure Cygwin paths look like Unix paths.
426 String scwd = cwdPath.toString();
427 if (cygpath && scwd.length() >= 2 &&
428 Character.isLetter(scwd.charAt(0)) && scwd.charAt(1) == ':') {
429 scwd = CYGDRIVE + Character.toLowerCase(scwd.charAt(0)) + "/" + scwd.substring(2);
430 }
431 environment.put("PWD", scwd);
432 return true;
433
434 // Set an environment variable.
435 case "setenv":
436 if (3 <= cmd.size()) {
437 final String key = cmd.get(1);
438 final String value = cmd.get(2);
439 environment.put(key, value);
440 }
441
442 return true;
443
444 // Unset an environment variable.
445 case "unsetenv":
446 if (2 <= cmd.size()) {
447 final String key = cmd.get(1);
448 environment.remove(key);
449 }
450
451 return true;
452 }
453
454 return false;
455 }
456
457 /**
458 * preprocessCommand - scan the command for redirects, and sanitize the
459 * executable path
460 * @param tokens command tokens
461 * @param cwd current working directory
462 * @param redirectInfo redirection information
463 * @return tokens remaining for actual command
464 */
465 private List<String> preprocessCommand(final List<String> tokens,
466 final String cwd, final RedirectInfo redirectInfo) {
467 // Tokens remaining for actual command.
468 final List<String> command = new ArrayList<>();
469
470 // iterate through all tokens.
471 final Iterator<String> iterator = tokens.iterator();
472 while (iterator.hasNext()) {
473 String token = iterator.next();
474
475 // Check if is a redirect.
476 if (redirectInfo.check(token, iterator, cwd)) {
477 // Don't add to the command.
478 continue;
479 }
480
481 // Strip quotes and add to command.
482 command.add(stripQuotes(token));
483 }
484
485 if (command.size() > 0) {
486 command.set(0, sanitizePath(command.get(0)));
487 }
488
489 return command;
490 }
491
492 /**
493 * Sanitize a path in case the underlying platform is Cygwin. In that case,
494 * convert from the {@code /cygdrive/x} drive specification to the usual
495 * Windows {@code X:} format.
496 *
497 * @param d a String representing a path
498 * @return a String representing the same path in a form that can be
499 * processed by the underlying platform
500 */
501 private static String sanitizePath(final String d) {
502 if (!IS_WINDOWS || (IS_WINDOWS && !d.startsWith(CYGDRIVE))) {
503 return d;
504 }
505 final String pd = d.substring(CYGDRIVE.length());
506 if (pd.length() >= 2 && pd.charAt(1) == '/') {
507 // drive letter plus / -> convert /cygdrive/x/... to X:/...
508 return pd.charAt(0) + ":" + pd.substring(1);
509 } else if (pd.length() == 1) {
510 // just drive letter -> convert /cygdrive/x to X:
511 return pd.charAt(0) + ":";
512 }
513 // remaining case: /cygdrive/ -> can't convert
514 return d;
515 }
516
517 /**
518 * createProcessBuilder - create a ProcessBuilder for the command.
519 * @param command command tokens
520 * @param cwd current working directory
521 * @param redirectInfo redirect information
522 */
523 private void createProcessBuilder(final List<String> command,
524 final String cwd, final RedirectInfo redirectInfo) {
525 // Create new ProcessBuilder.
526 final ProcessBuilder pb = new ProcessBuilder(command);
527 // Set current working directory.
528 pb.directory(new File(sanitizePath(cwd)));
529
530 // Map environment variables.
531 final Map<String, String> processEnvironment = pb.environment();
532 processEnvironment.clear();
533 processEnvironment.putAll(environment);
534
535 // Apply redirects.
536 redirectInfo.apply(pb);
537 // Add to current list of commands.
538 processBuilders.add(pb);
539 }
540
541 /**
542 * command - process the command
543 * @param tokens tokens of the command
544 * @param isPiped true if the output of this command should be piped to the next
545 */
546 private void command(final List<String> tokens, boolean isPiped) {
547 // Test to see if we should echo the command to output.
548 if (envVarBooleanValue("JJS_ECHO")) {
549 System.out.println(String.join(" ", tokens));
550 }
551
552 // Get the current working directory.
553 final String cwd = envVarValue("PWD", HOME_DIRECTORY);
554 // Preprocess the command for redirects.
555 final RedirectInfo redirectInfo = new RedirectInfo();
556 final List<String> command = preprocessCommand(tokens, cwd, redirectInfo);
557
558 // Skip if empty or a built in.
559 if (command.isEmpty() || builtIn(command, cwd)) {
560 return;
561 }
562
563 // Create ProcessBuilder with cwd and redirects set.
564 createProcessBuilder(command, cwd, redirectInfo);
565
566 // If piped, wait for the next command.
567 if (isPiped) {
568 return;
569 }
570
571 // Fetch first and last ProcessBuilder.
572 final ProcessBuilder firstProcessBuilder = processBuilders.get(0);
573 final ProcessBuilder lastProcessBuilder = processBuilders.get(processBuilders.size() - 1);
574
575 // Determine which streams have not be redirected from pipes.
576 boolean inputIsPipe = firstProcessBuilder.redirectInput() == Redirect.PIPE;
577 boolean outputIsPipe = lastProcessBuilder.redirectOutput() == Redirect.PIPE;
578 boolean errorIsPipe = lastProcessBuilder.redirectError() == Redirect.PIPE;
579 boolean inheritIO = envVarBooleanValue("JJS_INHERIT_IO");
580
581 // If not redirected and inputStream is current processes' input.
582 if (inputIsPipe && (inheritIO || inputStream == System.in)) {
583 // Inherit current processes' input.
584 firstProcessBuilder.redirectInput(Redirect.INHERIT);
585 inputIsPipe = false;
586 }
788 // Partial token pending.
789 command.add(sb.append(token).toString());
790 sb.setLength(0);
791 }
792 }
793 } catch (final IOException ex) {
794 // Do nothing.
795 }
796
797 // Partial token pending.
798 if (sb.length() != 0) {
799 command.add(sb.toString());
800 }
801
802 // Process last command.
803 command(command, false);
804 }
805
806 /**
807 * process - process a command array of strings
808 * @param tokens command script to be processed
809 */
810 void process(final List<String> tokens) {
811 // Prepare to accumulate command tokens.
812 final List<String> command = new ArrayList<>();
813
814 // Iterate through tokens.
815 final Iterator<String> iterator = tokens.iterator();
816 while (iterator.hasNext() && exitCode == EXIT_SUCCESS) {
817 // Next word token.
818 String token = iterator.next();
819
820 if (token == null) {
821 continue;
822 }
823
824 switch (token) {
825 case "|":
826 // Process as a piped command.
827 command(command, true);
828 // Start with a new set of tokens.
|