1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16
17package com.android.internal.os;
18
19import android.net.LocalSocket;
20import android.net.LocalSocketAddress;
21import android.os.SystemClock;
22import android.text.TextUtils;
23import android.util.Slog;
24
25import com.android.internal.util.Preconditions;
26
27import libcore.io.IoUtils;
28import libcore.io.Streams;
29
30import java.io.IOException;
31import java.io.InputStream;
32import java.io.OutputStream;
33import java.util.Arrays;
34
35/**
36 * Represents a connection to {@code installd}. Allows multiple connect and
37 * disconnect cycles.
38 *
39 * @hide for internal use only
40 */
41public class InstallerConnection {
42    private static final String TAG = "InstallerConnection";
43    private static final boolean LOCAL_DEBUG = false;
44
45    private InputStream mIn;
46    private OutputStream mOut;
47    private LocalSocket mSocket;
48
49    private volatile Object mWarnIfHeld;
50
51    private final byte buf[] = new byte[1024];
52
53    public InstallerConnection() {
54    }
55
56    /**
57     * Yell loudly if someone tries making future calls while holding a lock on
58     * the given object.
59     */
60    public void setWarnIfHeld(Object warnIfHeld) {
61        Preconditions.checkState(mWarnIfHeld == null);
62        mWarnIfHeld = Preconditions.checkNotNull(warnIfHeld);
63    }
64
65    public synchronized String transact(String cmd) {
66        if (mWarnIfHeld != null && Thread.holdsLock(mWarnIfHeld)) {
67            Slog.wtf(TAG, "Calling thread " + Thread.currentThread().getName() + " is holding 0x"
68                    + Integer.toHexString(System.identityHashCode(mWarnIfHeld)), new Throwable());
69        }
70
71        if (!connect()) {
72            Slog.e(TAG, "connection failed");
73            return "-1";
74        }
75
76        if (!writeCommand(cmd)) {
77            /*
78             * If installd died and restarted in the background (unlikely but
79             * possible) we'll fail on the next write (this one). Try to
80             * reconnect and write the command one more time before giving up.
81             */
82            Slog.e(TAG, "write command failed? reconnect!");
83            if (!connect() || !writeCommand(cmd)) {
84                return "-1";
85            }
86        }
87        if (LOCAL_DEBUG) {
88            Slog.i(TAG, "send: '" + cmd + "'");
89        }
90
91        final int replyLength = readReply();
92        if (replyLength > 0) {
93            String s = new String(buf, 0, replyLength);
94            if (LOCAL_DEBUG) {
95                Slog.i(TAG, "recv: '" + s + "'");
96            }
97            return s;
98        } else {
99            if (LOCAL_DEBUG) {
100                Slog.i(TAG, "fail");
101            }
102            return "-1";
103        }
104    }
105
106    public String[] execute(String cmd, Object... args) throws InstallerException {
107        final StringBuilder builder = new StringBuilder(cmd);
108        for (Object arg : args) {
109            String escaped;
110            if (arg == null) {
111                escaped = "";
112            } else {
113                escaped = String.valueOf(arg);
114            }
115            if (escaped.indexOf('\0') != -1 || escaped.indexOf(' ') != -1 || "!".equals(escaped)) {
116                throw new InstallerException(
117                        "Invalid argument while executing " + cmd + " " + Arrays.toString(args));
118            }
119            if (TextUtils.isEmpty(escaped)) {
120                escaped = "!";
121            }
122            builder.append(' ').append(escaped);
123        }
124        final String[] resRaw = transact(builder.toString()).split(" ");
125        int res = -1;
126        try {
127            res = Integer.parseInt(resRaw[0]);
128        } catch (ArrayIndexOutOfBoundsException | NumberFormatException ignored) {
129        }
130        if (res != 0) {
131            throw new InstallerException(
132                    "Failed to execute " + cmd + " " + Arrays.toString(args) + ": " + res);
133        }
134        return resRaw;
135    }
136
137    public void dexopt(String apkPath, int uid, String instructionSet, int dexoptNeeded,
138            int dexFlags, String compilerFilter, String volumeUuid, String sharedLibraries)
139            throws InstallerException {
140        dexopt(apkPath, uid, "*", instructionSet, dexoptNeeded, null /*outputPath*/, dexFlags,
141                compilerFilter, volumeUuid, sharedLibraries);
142    }
143
144    public void dexopt(String apkPath, int uid, String pkgName, String instructionSet,
145            int dexoptNeeded, String outputPath, int dexFlags, String compilerFilter,
146            String volumeUuid, String sharedLibraries) throws InstallerException {
147        execute("dexopt",
148                apkPath,
149                uid,
150                pkgName,
151                instructionSet,
152                dexoptNeeded,
153                outputPath,
154                dexFlags,
155                compilerFilter,
156                volumeUuid,
157                sharedLibraries);
158    }
159
160    private boolean safeParseBooleanResult(String[] res) throws InstallerException {
161        if ((res == null) || (res.length != 2)) {
162            throw new InstallerException("Invalid size result: " + Arrays.toString(res));
163        }
164
165        // Just as a sanity check. Anything != "true" will be interpreted as false by parseBoolean.
166        if (!res[1].equals("true") && !res[1].equals("false")) {
167            throw new InstallerException("Invalid boolean result: " + Arrays.toString(res));
168        }
169
170        return Boolean.parseBoolean(res[1]);
171    }
172
173    public boolean mergeProfiles(int uid, String pkgName) throws InstallerException {
174        final String[] res = execute("merge_profiles", uid, pkgName);
175
176        return safeParseBooleanResult(res);
177    }
178
179    public boolean dumpProfiles(String gid, String packageName, String codePaths)
180            throws InstallerException {
181        final String[] res = execute("dump_profiles", gid, packageName, codePaths);
182
183        return safeParseBooleanResult(res);
184    }
185
186    private boolean connect() {
187        if (mSocket != null) {
188            return true;
189        }
190        Slog.i(TAG, "connecting...");
191        try {
192            mSocket = new LocalSocket();
193
194            LocalSocketAddress address = new LocalSocketAddress("installd",
195                    LocalSocketAddress.Namespace.RESERVED);
196
197            mSocket.connect(address);
198
199            mIn = mSocket.getInputStream();
200            mOut = mSocket.getOutputStream();
201        } catch (IOException ex) {
202            disconnect();
203            return false;
204        }
205        return true;
206    }
207
208    public void disconnect() {
209        Slog.i(TAG, "disconnecting...");
210        IoUtils.closeQuietly(mSocket);
211        IoUtils.closeQuietly(mIn);
212        IoUtils.closeQuietly(mOut);
213
214        mSocket = null;
215        mIn = null;
216        mOut = null;
217    }
218
219
220    private boolean readFully(byte[] buffer, int len) {
221        try {
222            Streams.readFully(mIn, buffer, 0, len);
223        } catch (IOException ioe) {
224            Slog.e(TAG, "read exception");
225            disconnect();
226            return false;
227        }
228
229        if (LOCAL_DEBUG) {
230            Slog.i(TAG, "read " + len + " bytes");
231        }
232
233        return true;
234    }
235
236    private int readReply() {
237        if (!readFully(buf, 2)) {
238            return -1;
239        }
240
241        final int len = (((int) buf[0]) & 0xff) | ((((int) buf[1]) & 0xff) << 8);
242        if ((len < 1) || (len > buf.length)) {
243            Slog.e(TAG, "invalid reply length (" + len + ")");
244            disconnect();
245            return -1;
246        }
247
248        if (!readFully(buf, len)) {
249            return -1;
250        }
251
252        return len;
253    }
254
255    private boolean writeCommand(String cmdString) {
256        final byte[] cmd = cmdString.getBytes();
257        final int len = cmd.length;
258        if ((len < 1) || (len > buf.length)) {
259            return false;
260        }
261
262        buf[0] = (byte) (len & 0xff);
263        buf[1] = (byte) ((len >> 8) & 0xff);
264        try {
265            mOut.write(buf, 0, 2);
266            mOut.write(cmd, 0, len);
267        } catch (IOException ex) {
268            Slog.e(TAG, "write error");
269            disconnect();
270            return false;
271        }
272        return true;
273    }
274
275    public void waitForConnection() {
276        for (;;) {
277            try {
278                execute("ping");
279                return;
280            } catch (InstallerException ignored) {
281            }
282            Slog.w(TAG, "installd not ready");
283            SystemClock.sleep(1000);
284        }
285    }
286
287    public static class InstallerException extends Exception {
288        public InstallerException(String detailMessage) {
289            super(detailMessage);
290        }
291    }
292}
293