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