lin
2025-08-14 dae8bad597b6607a449b32bf76c523423f7720ed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
 
package vogar;
 
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import vogar.tasks.Task;
 
/**
 * A target runtime environment such as a remote device or the local host
 */
public abstract class Target {
 
    /**
     * Return the process prefix for this target.
     * <p>
     * A process prefix is a list of tokens added in front of a script (VM command) to be run on
     * this target.
     *
     * @see AdbTarget#targetProcessPrefix()
     * @see AdbChrootTarget#targetProcessPrefix()
     * @see SshTarget#targetProcessPrefix()
     *
     * @see Target#targetProcessWrapper()
     * @see ScriptBuilder#commandLine()
     */
 
    protected abstract ImmutableList<String> targetProcessPrefix();
 
    /**
     * Return the process wrapper for this target if there is one, or {@code null} otherwise.
     * <p>
     * A process wrapper is a command that is used to execute a script (VM command) to be run on
     * this target. The script, preceded by the process prefix, is surrounded with double quotes
     * and passed as an argument to the process wrapper:
     * <ul>
     *   <li>{@code <process-wrapper> "<process-prefix> <script>"}
     * </ul>
     * A {@code null} process wrapper means that the script will be executed as-is (but still
     * preceded by the process prefix):
     * <ul>
     *   <li>{@code <process-prefix> <script>}
     * </ul>
     *
     * @see AdbChrootTarget#targetProcessWrapper()
     *
     * @see Target#targetProcessPrefix()
     * @see ScriptBuilder#commandLine()
     */
    protected String targetProcessWrapper() {
        // By default, targets don't have a process wrapper.
        return null;
    };
 
    public abstract String getDeviceUserName();
 
    public abstract List<File> ls(File directory) throws FileNotFoundException;
    public abstract void await(File nonEmptyDirectory);
    public abstract void rm(File file);
    public abstract void mkdirs(File file);
    public abstract void forwardTcp(int port);
    public abstract void push(File local, File remote);
    public abstract void pull(File remote, File local);
 
    public final Task pushTask(final File local, final File remote) {
        return new Task("push " + remote) {
            @Override protected Result execute() throws Exception {
                push(local, remote);
                return Result.SUCCESS;
            }
        };
    }
 
    public final Task rmTask(final File remote) {
        return new Task("rm " + remote) {
            @Override protected Result execute() throws Exception {
                rm(remote);
                return Result.SUCCESS;
            }
        };
    }
 
    /**
     * Create a {@link ScriptBuilder} appropriate for this target.
     */
    public ScriptBuilder newScriptBuilder() {
        return new ScriptBuilder(targetProcessPrefix(), targetProcessWrapper());
    }
 
    /**
     * Responsible for constructing a one line script appropriate for a specific target.
     */
    public static class ScriptBuilder {
 
        private static final Joiner SCRIPT_JOINER = Joiner.on(" ");
 
        /**
         * Escape any special shell characters so that the target shell treats them as literal
         * characters.
         *
         * <p>e.g. an escaped space will not be treated as a token separator, an escaped dollar will
         * not cause shell substitution.
         */
        @VisibleForTesting
        static String escape(String token) {
            int length = token.length();
            StringBuilder builder = new StringBuilder(length);
            for (int i = 0; i < length; ++i) {
                char c = token.charAt(i);
                if (Character.isWhitespace(c)
                        || c == '\'' || c == '\"'
                        || c == '|' || c == '&'
                        || c == '<' || c == '>'
                        || c == '$' || c == '!'
                        || c == '(' || c == ')') {
                    builder.append('\\');
                }
 
                builder.append(c);
            }
 
            return builder.toString();
        }
 
        /**
         * The prefix to insert before the script to produce a command line that will execute the
         * script within a shell.
         */
        private final ImmutableList<String> commandLinePrefix;
 
        /**
         * An optional wrapper of the command line. This can be used e.g. to wrap 'COMMAND' into
         * 'sh -c "COMMAND"' before execution.
         */
        private final String commandLineWrapper;
 
        /**
         * The list of tokens making up the script, they were escaped where necessary before they
         * were added to the list.
         */
        private final List<String> escapedTokens;
 
        private ScriptBuilder(ImmutableList<String> commandLinePrefix, String commandLineWrapper) {
            this.commandLinePrefix = commandLinePrefix;
            this.commandLineWrapper = commandLineWrapper;
            escapedTokens = new ArrayList<>();
        }
 
        /**
         * Set the working directory in the target shell before running the command.
         */
        public ScriptBuilder workingDirectory(File workingDirectory) {
            escapedTokens.add("cd");
            escapedTokens.add(escape(workingDirectory.getPath()));
            escapedTokens.add("&&");
            return this;
        }
 
        /**
         * Set inline environment variables on the target shell that will affect the command.
         */
        public void env(Map<String, String> env) {
            for (Map.Entry<String, String> entry : env.entrySet()) {
                String name = entry.getKey();
                String value = entry.getValue();
                escapedTokens.add(name + "=" + escape(value));
            }
        }
 
        /**
         * Add tokens to the script.
         *
         * <p>Each token is escaped before adding it to the list. This method is suitable for adding
         * the command line name and arguments.
         */
        public ScriptBuilder tokens(List<String> tokens) {
            for (String token : tokens) {
                escapedTokens.add(escape(token));
            }
            return this;
        }
 
        /**
         * Add tokens to the script.
         *
         * <p>Each token is escaped before adding it to the list. This method is suitable for adding
         * the command line name and arguments.
         */
        public ScriptBuilder tokens(String... tokens) {
            for (String token : tokens) {
                escapedTokens.add(escape(token));
            }
            return this;
        }
 
        /**
         * Construct a command line to execute the script in the target shell.
         */
        public List<String> commandLine() {
            // Group the tokens into a single String argument. This is necessary for running in
            // a local shell where the first argument after the -c option is the script and the
            // remainder are treated as arguments to the script. This has no effect on either
            // adb or ssh shells as they both concatenate all their arguments into one single
            // string before parsing.
            String grouped = SCRIPT_JOINER.join(escapedTokens);
            // Honor a wrapper if there is one.
            if (commandLineWrapper != null) {
                grouped = commandLineWrapper + " \"" + grouped + "\"";
            }
            return new ImmutableList.Builder<String>()
                    .addAll(commandLinePrefix)
                    .add(grouped)
                    .build();
        }
    }
}