Home | History | Annotate | Download | only in base
      1 // Copyright 2013 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 package org.chromium.base;
      6 
      7 import android.text.TextUtils;
      8 import android.util.Log;
      9 
     10 import org.chromium.base.annotations.MainDex;
     11 
     12 import java.io.File;
     13 import java.io.FileInputStream;
     14 import java.io.FileNotFoundException;
     15 import java.io.IOException;
     16 import java.io.InputStreamReader;
     17 import java.io.Reader;
     18 import java.util.ArrayList;
     19 import java.util.Arrays;
     20 import java.util.HashMap;
     21 import java.util.List;
     22 import java.util.concurrent.atomic.AtomicReference;
     23 
     24 /**
     25  * Java mirror of base/command_line.h.
     26  * Android applications don't have command line arguments. Instead, they're "simulated" by reading a
     27  * file at a specific location early during startup. Applications each define their own files, e.g.,
     28  * ContentShellApplication.COMMAND_LINE_FILE.
     29 **/
     30 @MainDex
     31 public abstract class CommandLine {
     32     /**
     33      * Allows classes who cache command line flags to be notified when those arguments are updated
     34      * at runtime. This happens in tests.
     35      */
     36     public interface ResetListener {
     37         /** Called when the command line arguments are reset. */
     38         void onCommandLineReset();
     39     }
     40 
     41     // Public abstract interface, implemented in derived classes.
     42     // All these methods reflect their native-side counterparts.
     43     /**
     44      *  Returns true if this command line contains the given switch.
     45      *  (Switch names ARE case-sensitive).
     46      */
     47     @VisibleForTesting
     48     public abstract boolean hasSwitch(String switchString);
     49 
     50     /**
     51      * Return the value associated with the given switch, or null.
     52      * @param switchString The switch key to lookup. It should NOT start with '--' !
     53      * @return switch value, or null if the switch is not set or set to empty.
     54      */
     55     public abstract String getSwitchValue(String switchString);
     56 
     57     /**
     58      * Return the value associated with the given switch, or {@code defaultValue} if the switch
     59      * was not specified.
     60      * @param switchString The switch key to lookup. It should NOT start with '--' !
     61      * @param defaultValue The default value to return if the switch isn't set.
     62      * @return Switch value, or {@code defaultValue} if the switch is not set or set to empty.
     63      */
     64     public String getSwitchValue(String switchString, String defaultValue) {
     65         String value = getSwitchValue(switchString);
     66         return TextUtils.isEmpty(value) ? defaultValue : value;
     67     }
     68 
     69     /**
     70      * Append a switch to the command line.  There is no guarantee
     71      * this action happens before the switch is needed.
     72      * @param switchString the switch to add.  It should NOT start with '--' !
     73      */
     74     @VisibleForTesting
     75     public abstract void appendSwitch(String switchString);
     76 
     77     /**
     78      * Append a switch and value to the command line.  There is no
     79      * guarantee this action happens before the switch is needed.
     80      * @param switchString the switch to add.  It should NOT start with '--' !
     81      * @param value the value for this switch.
     82      * For example, --foo=bar becomes 'foo', 'bar'.
     83      */
     84     public abstract void appendSwitchWithValue(String switchString, String value);
     85 
     86     /**
     87      * Append switch/value items in "command line" format (excluding argv[0] program name).
     88      * E.g. { '--gofast', '--username=fred' }
     89      * @param array an array of switch or switch/value items in command line format.
     90      *   Unlike the other append routines, these switches SHOULD start with '--' .
     91      *   Unlike init(), this does not include the program name in array[0].
     92      */
     93     public abstract void appendSwitchesAndArguments(String[] array);
     94 
     95     /**
     96      * Determine if the command line is bound to the native (JNI) implementation.
     97      * @return true if the underlying implementation is delegating to the native command line.
     98      */
     99     public boolean isNativeImplementation() {
    100         return false;
    101     }
    102 
    103     private static final List<ResetListener> sResetListeners = new ArrayList<>();
    104     private static final AtomicReference<CommandLine> sCommandLine =
    105             new AtomicReference<CommandLine>();
    106 
    107     /**
    108      * @returns true if the command line has already been initialized.
    109      */
    110     public static boolean isInitialized() {
    111         return sCommandLine.get() != null;
    112     }
    113 
    114     // Equivalent to CommandLine::ForCurrentProcess in C++.
    115     @VisibleForTesting
    116     public static CommandLine getInstance() {
    117         CommandLine commandLine = sCommandLine.get();
    118         assert commandLine != null;
    119         return commandLine;
    120     }
    121 
    122     /**
    123      * Initialize the singleton instance, must be called exactly once (either directly or
    124      * via one of the convenience wrappers below) before using the static singleton instance.
    125      * @param args command line flags in 'argv' format: args[0] is the program name.
    126      */
    127     public static void init(String[] args) {
    128         setInstance(new JavaCommandLine(args));
    129     }
    130 
    131     /**
    132      * Initialize the command line from the command-line file.
    133      *
    134      * @param file The fully qualified command line file.
    135      */
    136     public static void initFromFile(String file) {
    137         // Arbitrary clamp of 16k on the amount of file we read in.
    138         char[] buffer = readUtf8FileFullyCrashIfTooBig(file, 16 * 1024);
    139         init(buffer == null ? null : tokenizeQuotedAruments(buffer));
    140     }
    141 
    142     /**
    143      * Resets both the java proxy and the native command lines. This allows the entire
    144      * command line initialization to be re-run including the call to onJniLoaded.
    145      */
    146     @VisibleForTesting
    147     public static void reset() {
    148         setInstance(null);
    149         ThreadUtils.postOnUiThread(new Runnable() {
    150             @Override
    151             public void run() {
    152                 for (ResetListener listener : sResetListeners) listener.onCommandLineReset();
    153             }
    154         });
    155     }
    156 
    157     public static void addResetListener(ResetListener listener) {
    158         sResetListeners.add(listener);
    159     }
    160 
    161     public static void removeResetListener(ResetListener listener) {
    162         sResetListeners.remove(listener);
    163     }
    164 
    165     /**
    166      * Public for testing (TODO: why are the tests in a different package?)
    167      * Parse command line flags from a flat buffer, supporting double-quote enclosed strings
    168      * containing whitespace. argv elements are derived by splitting the buffer on whitepace;
    169      * double quote characters may enclose tokens containing whitespace; a double-quote literal
    170      * may be escaped with back-slash. (Otherwise backslash is taken as a literal).
    171      * @param buffer A command line in command line file format as described above.
    172      * @return the tokenized arguments, suitable for passing to init().
    173      */
    174     public static String[] tokenizeQuotedAruments(char[] buffer) {
    175         ArrayList<String> args = new ArrayList<String>();
    176         StringBuilder arg = null;
    177         final char noQuote = '\0';
    178         final char singleQuote = '\'';
    179         final char doubleQuote = '"';
    180         char currentQuote = noQuote;
    181         for (char c : buffer) {
    182             // Detect start or end of quote block.
    183             if ((currentQuote == noQuote && (c == singleQuote || c == doubleQuote))
    184                     || c == currentQuote) {
    185                 if (arg != null && arg.length() > 0 && arg.charAt(arg.length() - 1) == '\\') {
    186                     // Last char was a backslash; pop it, and treat c as a literal.
    187                     arg.setCharAt(arg.length() - 1, c);
    188                 } else {
    189                     currentQuote = currentQuote == noQuote ? c : noQuote;
    190                 }
    191             } else if (currentQuote == noQuote && Character.isWhitespace(c)) {
    192                 if (arg != null) {
    193                     args.add(arg.toString());
    194                     arg = null;
    195                 }
    196             } else {
    197                 if (arg == null) arg = new StringBuilder();
    198                 arg.append(c);
    199             }
    200         }
    201         if (arg != null) {
    202             if (currentQuote != noQuote) {
    203                 Log.w(TAG, "Unterminated quoted string: " + arg);
    204             }
    205             args.add(arg.toString());
    206         }
    207         return args.toArray(new String[args.size()]);
    208     }
    209 
    210     private static final String TAG = "CommandLine";
    211     private static final String SWITCH_PREFIX = "--";
    212     private static final String SWITCH_TERMINATOR = SWITCH_PREFIX;
    213     private static final String SWITCH_VALUE_SEPARATOR = "=";
    214 
    215     public static void enableNativeProxy() {
    216         // Make a best-effort to ensure we make a clean (atomic) switch over from the old to
    217         // the new command line implementation. If another thread is modifying the command line
    218         // when this happens, all bets are off. (As per the native CommandLine).
    219         sCommandLine.set(new NativeCommandLine());
    220     }
    221 
    222     public static String[] getJavaSwitchesOrNull() {
    223         CommandLine commandLine = sCommandLine.get();
    224         if (commandLine != null) {
    225             assert !commandLine.isNativeImplementation();
    226             return ((JavaCommandLine) commandLine).getCommandLineArguments();
    227         }
    228         return null;
    229     }
    230 
    231     private static void setInstance(CommandLine commandLine) {
    232         CommandLine oldCommandLine = sCommandLine.getAndSet(commandLine);
    233         if (oldCommandLine != null && oldCommandLine.isNativeImplementation()) {
    234             nativeReset();
    235         }
    236     }
    237 
    238     /**
    239      * @param fileName the file to read in.
    240      * @param sizeLimit cap on the file size.
    241      * @return Array of chars read from the file, or null if the file cannot be read.
    242      * @throws RuntimeException if the file size exceeds |sizeLimit|.
    243      */
    244     private static char[] readUtf8FileFullyCrashIfTooBig(String fileName, int sizeLimit) {
    245         Reader reader = null;
    246         File f = new File(fileName);
    247         long fileLength = f.length();
    248 
    249         if (fileLength == 0) {
    250             return null;
    251         }
    252 
    253         if (fileLength > sizeLimit) {
    254             throw new RuntimeException(
    255                     "File " + fileName + " length " + fileLength + " exceeds limit " + sizeLimit);
    256         }
    257 
    258         try {
    259             char[] buffer = new char[(int) fileLength];
    260             reader = new InputStreamReader(new FileInputStream(f), "UTF-8");
    261             int charsRead = reader.read(buffer);
    262             // Debug check that we've exhausted the input stream (will fail e.g. if the
    263             // file grew after we inspected its length).
    264             assert !reader.ready();
    265             return charsRead < buffer.length ? Arrays.copyOfRange(buffer, 0, charsRead) : buffer;
    266         } catch (FileNotFoundException e) {
    267             return null;
    268         } catch (IOException e) {
    269             return null;
    270         } finally {
    271             try {
    272                 if (reader != null) reader.close();
    273             } catch (IOException e) {
    274                 Log.e(TAG, "Unable to close file reader.", e);
    275             }
    276         }
    277     }
    278 
    279     private CommandLine() {}
    280 
    281     private static class JavaCommandLine extends CommandLine {
    282         private HashMap<String, String> mSwitches = new HashMap<String, String>();
    283         private ArrayList<String> mArgs = new ArrayList<String>();
    284 
    285         // The arguments begin at index 1, since index 0 contains the executable name.
    286         private int mArgsBegin = 1;
    287 
    288         JavaCommandLine(String[] args) {
    289             if (args == null || args.length == 0 || args[0] == null) {
    290                 mArgs.add("");
    291             } else {
    292                 mArgs.add(args[0]);
    293                 appendSwitchesInternal(args, 1);
    294             }
    295             // Invariant: we always have the argv[0] program name element.
    296             assert mArgs.size() > 0;
    297         }
    298 
    299         /**
    300          * Returns the switches and arguments passed into the program, with switches and their
    301          * values coming before all of the arguments.
    302          */
    303         private String[] getCommandLineArguments() {
    304             return mArgs.toArray(new String[mArgs.size()]);
    305         }
    306 
    307         @Override
    308         public boolean hasSwitch(String switchString) {
    309             return mSwitches.containsKey(switchString);
    310         }
    311 
    312         @Override
    313         public String getSwitchValue(String switchString) {
    314             // This is slightly round about, but needed for consistency with the NativeCommandLine
    315             // version which does not distinguish empty values from key not present.
    316             String value = mSwitches.get(switchString);
    317             return value == null || value.isEmpty() ? null : value;
    318         }
    319 
    320         @Override
    321         public void appendSwitch(String switchString) {
    322             appendSwitchWithValue(switchString, null);
    323         }
    324 
    325         /**
    326          * Appends a switch to the current list.
    327          * @param switchString the switch to add.  It should NOT start with '--' !
    328          * @param value the value for this switch.
    329          */
    330         @Override
    331         public void appendSwitchWithValue(String switchString, String value) {
    332             mSwitches.put(switchString, value == null ? "" : value);
    333 
    334             // Append the switch and update the switches/arguments divider mArgsBegin.
    335             String combinedSwitchString = SWITCH_PREFIX + switchString;
    336             if (value != null && !value.isEmpty()) {
    337                 combinedSwitchString += SWITCH_VALUE_SEPARATOR + value;
    338             }
    339 
    340             mArgs.add(mArgsBegin++, combinedSwitchString);
    341         }
    342 
    343         @Override
    344         public void appendSwitchesAndArguments(String[] array) {
    345             appendSwitchesInternal(array, 0);
    346         }
    347 
    348         // Add the specified arguments, but skipping the first |skipCount| elements.
    349         private void appendSwitchesInternal(String[] array, int skipCount) {
    350             boolean parseSwitches = true;
    351             for (String arg : array) {
    352                 if (skipCount > 0) {
    353                     --skipCount;
    354                     continue;
    355                 }
    356 
    357                 if (arg.equals(SWITCH_TERMINATOR)) {
    358                     parseSwitches = false;
    359                 }
    360 
    361                 if (parseSwitches && arg.startsWith(SWITCH_PREFIX)) {
    362                     String[] parts = arg.split(SWITCH_VALUE_SEPARATOR, 2);
    363                     String value = parts.length > 1 ? parts[1] : null;
    364                     appendSwitchWithValue(parts[0].substring(SWITCH_PREFIX.length()), value);
    365                 } else {
    366                     mArgs.add(arg);
    367                 }
    368             }
    369         }
    370     }
    371 
    372     private static class NativeCommandLine extends CommandLine {
    373         @Override
    374         public boolean hasSwitch(String switchString) {
    375             return nativeHasSwitch(switchString);
    376         }
    377 
    378         @Override
    379         public String getSwitchValue(String switchString) {
    380             return nativeGetSwitchValue(switchString);
    381         }
    382 
    383         @Override
    384         public void appendSwitch(String switchString) {
    385             nativeAppendSwitch(switchString);
    386         }
    387 
    388         @Override
    389         public void appendSwitchWithValue(String switchString, String value) {
    390             nativeAppendSwitchWithValue(switchString, value);
    391         }
    392 
    393         @Override
    394         public void appendSwitchesAndArguments(String[] array) {
    395             nativeAppendSwitchesAndArguments(array);
    396         }
    397 
    398         @Override
    399         public boolean isNativeImplementation() {
    400             return true;
    401         }
    402     }
    403 
    404     private static native void nativeReset();
    405     private static native boolean nativeHasSwitch(String switchString);
    406     private static native String nativeGetSwitchValue(String switchString);
    407     private static native void nativeAppendSwitch(String switchString);
    408     private static native void nativeAppendSwitchWithValue(String switchString, String value);
    409     private static native void nativeAppendSwitchesAndArguments(String[] array);
    410 }
    411