1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one or more
3  * contributor license agreements.  See the NOTICE file distributed with
4  * this work for additional information regarding copyright ownership.
5  * The ASF licenses this file to You under the Apache License, Version 2.0
6  * (the "License"); you may not use this file except in compliance with
7  * the License.  You may obtain a copy of the License at
8  *
9  *    http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 package org.apache.spark.launcher;
19 
20 import java.io.File;
21 import java.util.ArrayList;
22 import java.util.List;
23 import java.util.Map;
24 
25 /**
26  * Helper methods for command builders.
27  */
28 class CommandBuilderUtils {
29 
30   static final String DEFAULT_MEM = "1g";
31   static final String DEFAULT_PROPERTIES_FILE = "spark-defaults.conf";
32   static final String ENV_SPARK_HOME = "SPARK_HOME";
33 
34   /** The set of known JVM vendors. */
35   enum JavaVendor {
36     Oracle, IBM, OpenJDK, Unknown
37   }
38 
39   /** Returns whether the given string is null or empty. */
isEmpty(String s)40   static boolean isEmpty(String s) {
41     return s == null || s.isEmpty();
42   }
43 
44   /** Joins a list of strings using the given separator. */
join(String sep, String... elements)45   static String join(String sep, String... elements) {
46     StringBuilder sb = new StringBuilder();
47     for (String e : elements) {
48       if (e != null) {
49         if (sb.length() > 0) {
50           sb.append(sep);
51         }
52         sb.append(e);
53       }
54     }
55     return sb.toString();
56   }
57 
58   /** Joins a list of strings using the given separator. */
join(String sep, Iterable<String> elements)59   static String join(String sep, Iterable<String> elements) {
60     StringBuilder sb = new StringBuilder();
61     for (String e : elements) {
62       if (e != null) {
63         if (sb.length() > 0) {
64           sb.append(sep);
65         }
66         sb.append(e);
67       }
68     }
69     return sb.toString();
70   }
71 
72   /**
73    * Returns the first non-empty value mapped to the given key in the given maps, or null otherwise.
74    */
firstNonEmptyValue(String key, Map<?, ?>... maps)75   static String firstNonEmptyValue(String key, Map<?, ?>... maps) {
76     for (Map<?, ?> map : maps) {
77       String value = (String) map.get(key);
78       if (!isEmpty(value)) {
79         return value;
80       }
81     }
82     return null;
83   }
84 
85   /** Returns the first non-empty, non-null string in the given list, or null otherwise. */
firstNonEmpty(String... candidates)86   static String firstNonEmpty(String... candidates) {
87     for (String s : candidates) {
88       if (!isEmpty(s)) {
89         return s;
90       }
91     }
92     return null;
93   }
94 
95   /** Returns the name of the env variable that holds the native library path. */
getLibPathEnvName()96   static String getLibPathEnvName() {
97     if (isWindows()) {
98       return "PATH";
99     }
100 
101     String os = System.getProperty("os.name");
102     if (os.startsWith("Mac OS X")) {
103       return "DYLD_LIBRARY_PATH";
104     } else {
105       return "LD_LIBRARY_PATH";
106     }
107   }
108 
109   /** Returns whether the OS is Windows. */
isWindows()110   static boolean isWindows() {
111     String os = System.getProperty("os.name");
112     return os.startsWith("Windows");
113   }
114 
115   /** Returns an enum value indicating whose JVM is being used. */
getJavaVendor()116   static JavaVendor getJavaVendor() {
117     String vendorString = System.getProperty("java.vendor");
118     if (vendorString.contains("Oracle")) {
119       return JavaVendor.Oracle;
120     }
121     if (vendorString.contains("IBM")) {
122       return JavaVendor.IBM;
123     }
124     if (vendorString.contains("OpenJDK")) {
125       return JavaVendor.OpenJDK;
126     }
127     return JavaVendor.Unknown;
128   }
129 
130   /**
131    * Updates the user environment, appending the given pathList to the existing value of the given
132    * environment variable (or setting it if it hasn't yet been set).
133    */
mergeEnvPathList(Map<String, String> userEnv, String envKey, String pathList)134   static void mergeEnvPathList(Map<String, String> userEnv, String envKey, String pathList) {
135     if (!isEmpty(pathList)) {
136       String current = firstNonEmpty(userEnv.get(envKey), System.getenv(envKey));
137       userEnv.put(envKey, join(File.pathSeparator, current, pathList));
138     }
139   }
140 
141   /**
142    * Parse a string as if it were a list of arguments, following bash semantics.
143    * For example:
144    *
145    * Input: "\"ab cd\" efgh 'i \" j'"
146    * Output: [ "ab cd", "efgh", "i \" j" ]
147    */
parseOptionString(String s)148   static List<String> parseOptionString(String s) {
149     List<String> opts = new ArrayList<>();
150     StringBuilder opt = new StringBuilder();
151     boolean inOpt = false;
152     boolean inSingleQuote = false;
153     boolean inDoubleQuote = false;
154     boolean escapeNext = false;
155 
156     // This is needed to detect when a quoted empty string is used as an argument ("" or '').
157     boolean hasData = false;
158 
159     for (int i = 0; i < s.length(); i++) {
160       int c = s.codePointAt(i);
161       if (escapeNext) {
162         opt.appendCodePoint(c);
163         escapeNext = false;
164       } else if (inOpt) {
165         switch (c) {
166         case '\\':
167           if (inSingleQuote) {
168             opt.appendCodePoint(c);
169           } else {
170             escapeNext = true;
171           }
172           break;
173         case '\'':
174           if (inDoubleQuote) {
175             opt.appendCodePoint(c);
176           } else {
177             inSingleQuote = !inSingleQuote;
178           }
179           break;
180         case '"':
181           if (inSingleQuote) {
182             opt.appendCodePoint(c);
183           } else {
184             inDoubleQuote = !inDoubleQuote;
185           }
186           break;
187         default:
188           if (!Character.isWhitespace(c) || inSingleQuote || inDoubleQuote) {
189             opt.appendCodePoint(c);
190           } else {
191             opts.add(opt.toString());
192             opt.setLength(0);
193             inOpt = false;
194             hasData = false;
195           }
196         }
197       } else {
198         switch (c) {
199         case '\'':
200           inSingleQuote = true;
201           inOpt = true;
202           hasData = true;
203           break;
204         case '"':
205           inDoubleQuote = true;
206           inOpt = true;
207           hasData = true;
208           break;
209         case '\\':
210           escapeNext = true;
211           inOpt = true;
212           hasData = true;
213           break;
214         default:
215           if (!Character.isWhitespace(c)) {
216             inOpt = true;
217             hasData = true;
218             opt.appendCodePoint(c);
219           }
220         }
221       }
222     }
223 
224     checkArgument(!inSingleQuote && !inDoubleQuote && !escapeNext, "Invalid option string: %s", s);
225     if (hasData) {
226       opts.add(opt.toString());
227     }
228     return opts;
229   }
230 
231   /** Throws IllegalArgumentException if the given object is null. */
checkNotNull(Object o, String arg)232   static void checkNotNull(Object o, String arg) {
233     if (o == null) {
234       throw new IllegalArgumentException(String.format("'%s' must not be null.", arg));
235     }
236   }
237 
238   /** Throws IllegalArgumentException with the given message if the check is false. */
checkArgument(boolean check, String msg, Object... args)239   static void checkArgument(boolean check, String msg, Object... args) {
240     if (!check) {
241       throw new IllegalArgumentException(String.format(msg, args));
242     }
243   }
244 
245   /** Throws IllegalStateException with the given message if the check is false. */
checkState(boolean check, String msg, Object... args)246   static void checkState(boolean check, String msg, Object... args) {
247     if (!check) {
248       throw new IllegalStateException(String.format(msg, args));
249     }
250   }
251 
252   /**
253    * Quote a command argument for a command to be run by a Windows batch script, if the argument
254    * needs quoting. Arguments only seem to need quotes in batch scripts if they have certain
255    * special characters, some of which need extra (and different) escaping.
256    *
257    *  For example:
258    *    original single argument: ab="cde fgh"
259    *    quoted: "ab^=""cde fgh"""
260    */
quoteForBatchScript(String arg)261   static String quoteForBatchScript(String arg) {
262 
263     boolean needsQuotes = false;
264     for (int i = 0; i < arg.length(); i++) {
265       int c = arg.codePointAt(i);
266       if (Character.isWhitespace(c) || c == '"' || c == '=' || c == ',' || c == ';') {
267         needsQuotes = true;
268         break;
269       }
270     }
271     if (!needsQuotes) {
272       return arg;
273     }
274     StringBuilder quoted = new StringBuilder();
275     quoted.append("\"");
276     for (int i = 0; i < arg.length(); i++) {
277       int cp = arg.codePointAt(i);
278       switch (cp) {
279       case '"':
280         quoted.append('"');
281         break;
282 
283       default:
284         break;
285       }
286       quoted.appendCodePoint(cp);
287     }
288     if (arg.codePointAt(arg.length() - 1) == '\\') {
289       quoted.append("\\");
290     }
291     quoted.append("\"");
292     return quoted.toString();
293   }
294 
295   /**
296    * Quotes a string so that it can be used in a command string.
297    * Basically, just add simple escapes. E.g.:
298    *    original single argument : ab "cd" ef
299    *    after: "ab \"cd\" ef"
300    *
301    * This can be parsed back into a single argument by python's "shlex.split()" function.
302    */
quoteForCommandString(String s)303   static String quoteForCommandString(String s) {
304     StringBuilder quoted = new StringBuilder().append('"');
305     for (int i = 0; i < s.length(); i++) {
306       int cp = s.codePointAt(i);
307       if (cp == '"' || cp == '\\') {
308         quoted.appendCodePoint('\\');
309       }
310       quoted.appendCodePoint(cp);
311     }
312     return quoted.append('"').toString();
313   }
314 
315   /**
316    * Adds the default perm gen size option for Spark if the VM requires it and the user hasn't
317    * set it.
318    */
addPermGenSizeOpt(List<String> cmd)319   static void addPermGenSizeOpt(List<String> cmd) {
320     // Don't set MaxPermSize for IBM Java, or Oracle Java 8 and later.
321     if (getJavaVendor() == JavaVendor.IBM) {
322       return;
323     }
324     if (javaMajorVersion(System.getProperty("java.version")) > 7) {
325       return;
326     }
327     for (String arg : cmd) {
328       if (arg.contains("-XX:MaxPermSize=")) {
329         return;
330       }
331     }
332 
333     cmd.add("-XX:MaxPermSize=256m");
334   }
335 
336   /**
337    * Get the major version of the java version string supplied. This method
338    * accepts any JEP-223-compliant strings (9-ea, 9+100), as well as legacy
339    * version strings such as 1.7.0_79
340    */
javaMajorVersion(String javaVersion)341   static int javaMajorVersion(String javaVersion) {
342     String[] version = javaVersion.split("[+.\\-]+");
343     int major = Integer.parseInt(version[0]);
344     // if major > 1, we're using the JEP-223 version string, e.g., 9-ea, 9+120
345     // otherwise the second number is the major version
346     if (major > 1) {
347       return major;
348     } else {
349       return Integer.parseInt(version[1]);
350     }
351   }
352 
353   /**
354    * Find the location of the Spark jars dir, depending on whether we're looking at a build
355    * or a distribution directory.
356    */
findJarsDir(String sparkHome, String scalaVersion, boolean failIfNotFound)357   static String findJarsDir(String sparkHome, String scalaVersion, boolean failIfNotFound) {
358     // TODO: change to the correct directory once the assembly build is changed.
359     File libdir;
360     if (new File(sparkHome, "jars").isDirectory()) {
361       libdir = new File(sparkHome, "jars");
362       checkState(!failIfNotFound || libdir.isDirectory(),
363         "Library directory '%s' does not exist.",
364         libdir.getAbsolutePath());
365     } else {
366       libdir = new File(sparkHome, String.format("assembly/target/scala-%s/jars", scalaVersion));
367       if (!libdir.isDirectory()) {
368         checkState(!failIfNotFound,
369           "Library directory '%s' does not exist; make sure Spark is built.",
370           libdir.getAbsolutePath());
371         libdir = null;
372       }
373     }
374     return libdir != null ? libdir.getAbsolutePath() : null;
375   }
376 
377 }
378