1 /*
2  * Copyright (C) 2023 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 
17 package com.android.tests.odsign;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 
21 import com.android.tradefed.invoker.TestInformation;
22 
23 import org.w3c.dom.Document;
24 import org.w3c.dom.Element;
25 import org.w3c.dom.Node;
26 import org.w3c.dom.NodeList;
27 
28 import java.io.File;
29 import java.util.HashMap;
30 import java.util.HashSet;
31 import java.util.Map;
32 import java.util.Set;
33 import java.util.UUID;
34 import javax.xml.parsers.DocumentBuilder;
35 import javax.xml.parsers.DocumentBuilderFactory;
36 import javax.xml.transform.Transformer;
37 import javax.xml.transform.TransformerFactory;
38 import javax.xml.transform.dom.DOMSource;
39 import javax.xml.transform.stream.StreamResult;
40 
41 /** A helper class that can mutate the device state and restore it afterwards. */
42 public class DeviceState {
43     private static final String TEST_JAR_RESOURCE_NAME = "/art-gtest-jars-Main.jar";
44     private static final String PHENOTYPE_FLAG_NAMESPACE = "runtime_native_boot";
45     private static final String ART_APEX_DALVIK_CACHE_BACKUP_DIRNAME =
46             OdsignTestUtils.ART_APEX_DALVIK_CACHE_DIRNAME + ".bak";
47 
48     private final TestInformation mTestInfo;
49     private final OdsignTestUtils mTestUtils;
50 
51     private Set<String> mTempFiles = new HashSet<>();
52     private Set<String> mMountPoints = new HashSet<>();
53     private Map<String, String> mMutatedProperties = new HashMap<>();
54     private Map<String, String> mMutatedPhenotypeFlags = new HashMap<>();
55     private Map<String, String> mDeletedFiles = new HashMap<>();
56     private boolean mHasArtifactsBackup = false;
57 
DeviceState(TestInformation testInfo)58     public DeviceState(TestInformation testInfo) throws Exception {
59         mTestInfo = testInfo;
60         mTestUtils = new OdsignTestUtils(testInfo);
61     }
62 
63     /** Restores the device state. */
restore()64     public void restore() throws Exception {
65         for (String mountPoint : mMountPoints) {
66             mTestInfo.getDevice().executeShellV2Command(String.format("umount '%s'", mountPoint));
67         }
68 
69         for (String tempFile : mTempFiles) {
70             mTestInfo.getDevice().deleteFile(tempFile);
71         }
72 
73         for (var entry : mMutatedProperties.entrySet()) {
74             mTestInfo.getDevice().setProperty(
75                     entry.getKey(), entry.getValue() != null ? entry.getValue() : "");
76         }
77 
78         for (var entry : mMutatedPhenotypeFlags.entrySet()) {
79             if (entry.getValue() != null) {
80                 mTestInfo.getDevice().executeShellV2Command(
81                         String.format("device_config put '%s' '%s' '%s'", PHENOTYPE_FLAG_NAMESPACE,
82                                 entry.getKey(), entry.getValue()));
83             } else {
84                 mTestInfo.getDevice().executeShellV2Command(
85                         String.format("device_config delete '%s' '%s'", PHENOTYPE_FLAG_NAMESPACE,
86                                 entry.getKey()));
87             }
88         }
89 
90         for (var entry : mDeletedFiles.entrySet()) {
91             mTestInfo.getDevice().executeShellV2Command(
92                     String.format("cp '%s' '%s'", entry.getValue(), entry.getKey()));
93             mTestInfo.getDevice().executeShellV2Command(String.format("rm '%s'", entry.getValue()));
94             mTestInfo.getDevice().executeShellV2Command(
95                     String.format("restorecon '%s'", entry.getKey()));
96         }
97 
98         if (mHasArtifactsBackup) {
99             mTestInfo.getDevice().executeShellV2Command(
100                     String.format("rm -rf '%s'", OdsignTestUtils.ART_APEX_DALVIK_CACHE_DIRNAME));
101             mTestInfo.getDevice().executeShellV2Command(
102                     String.format("mv '%s' '%s'", ART_APEX_DALVIK_CACHE_BACKUP_DIRNAME,
103                             OdsignTestUtils.ART_APEX_DALVIK_CACHE_DIRNAME));
104         }
105     }
106 
107     /** Simulates that the ART APEX has been upgraded. */
simulateArtApexUpgrade()108     public void simulateArtApexUpgrade() throws Exception {
109         updateApexInfo("com.android.art", false /* isFactory */);
110     }
111 
112     /**
113      * Simulates that the new ART APEX has been uninstalled (i.e., the ART module goes back to the
114      * factory version).
115      */
simulateArtApexUninstall()116     public void simulateArtApexUninstall() throws Exception {
117         updateApexInfo("com.android.art", true /* isFactory */);
118     }
119 
120     /**
121      * Simulates that an APEX has been upgraded. We could install a real APEX, but that would
122      * introduce an extra dependency to this test, which we want to avoid.
123      */
simulateApexUpgrade()124     public void simulateApexUpgrade() throws Exception {
125         updateApexInfo("com.android.wifi", false /* isFactory */);
126     }
127 
128     /**
129      * Simulates that the new APEX has been uninstalled (i.e., the module goes back to the factory
130      * version).
131      */
simulateApexUninstall()132     public void simulateApexUninstall() throws Exception {
133         updateApexInfo("com.android.wifi", true /* isFactory */);
134     }
135 
updateApexInfo(String moduleName, boolean isFactory)136     private void updateApexInfo(String moduleName, boolean isFactory) throws Exception {
137         try (var xmlMutator = new XmlMutator(OdsignTestUtils.APEX_INFO_FILE)) {
138             NodeList list = xmlMutator.getDocument().getElementsByTagName("apex-info");
139             for (int i = 0; i < list.getLength(); i++) {
140                 Element node = (Element) list.item(i);
141                 if (node.getAttribute("moduleName").equals(moduleName)
142                         && node.getAttribute("isActive").equals("true")) {
143                     node.setAttribute("isFactory", String.valueOf(isFactory));
144                     node.setAttribute(
145                             "lastUpdateMillis", String.valueOf(System.currentTimeMillis()));
146                 }
147             }
148         }
149     }
150 
151     /** Simulates that there is an OTA that updates a boot classpath jar. */
simulateBootClasspathOta()152     public void simulateBootClasspathOta() throws Exception {
153         File localFile = mTestUtils.copyResourceToFile(TEST_JAR_RESOURCE_NAME);
154         pushAndBindMount(localFile, "/system/framework/framework-graphics.jar");
155     }
156 
157     /** Simulates that there is an OTA that updates a system server jar. */
simulateSystemServerOta()158     public void simulateSystemServerOta() throws Exception {
159         File localFile = mTestUtils.copyResourceToFile(TEST_JAR_RESOURCE_NAME);
160         pushAndBindMount(localFile, "/system/framework/services.jar");
161     }
162 
163     /** Simulates that a system server jar is bad. */
simulateBadSystemServerJar()164     public void simulateBadSystemServerJar() throws Exception {
165         File tempFile = File.createTempFile("empty", ".jar");
166         tempFile.deleteOnExit();
167         pushAndBindMount(tempFile, "/system/framework/services.jar");
168     }
169 
makeDex2oatFail()170     public void makeDex2oatFail() throws Exception {
171         setProperty("dalvik.vm.boot-dex2oat-threads", "-1");
172     }
173 
174     /** Sets a system property. */
setProperty(String key, String value)175     public void setProperty(String key, String value) throws Exception {
176         if (!mMutatedProperties.containsKey(key)) {
177             // Backup the original value.
178             mMutatedProperties.put(key, mTestInfo.getDevice().getProperty(key));
179         }
180 
181         mTestInfo.getDevice().setProperty(key, value);
182     }
183 
184     /** Sets a phenotype flag. */
setPhenotypeFlag(String key, String value)185     public void setPhenotypeFlag(String key, String value) throws Exception {
186         if (!mMutatedPhenotypeFlags.containsKey(key)) {
187             String output = mTestUtils.assertCommandSucceeds(
188                     String.format("device_config get '%s' '%s'", PHENOTYPE_FLAG_NAMESPACE, key));
189             mMutatedPhenotypeFlags.put(key, output.equals("null") ? null : output);
190         }
191 
192         if (value != null) {
193             mTestUtils.assertCommandSucceeds(String.format(
194                     "device_config put '%s' '%s' '%s'", PHENOTYPE_FLAG_NAMESPACE, key, value));
195         } else {
196             mTestUtils.assertCommandSucceeds(
197                     String.format("device_config delete '%s' '%s'", PHENOTYPE_FLAG_NAMESPACE, key));
198         }
199     }
200 
backupAndDeleteFile(String remotePath)201     public void backupAndDeleteFile(String remotePath) throws Exception {
202         String tempFile = "/data/local/tmp/odsign_e2e_tests_" + UUID.randomUUID() + ".tmp";
203         // Backup the file before deleting it.
204         mTestUtils.assertCommandSucceeds(String.format("cp '%s' '%s'", remotePath, tempFile));
205         mTestUtils.assertCommandSucceeds(String.format("rm '%s'", remotePath));
206         mDeletedFiles.put(remotePath, tempFile);
207     }
208 
backupArtifacts()209     public void backupArtifacts() throws Exception {
210         mTestInfo.getDevice().executeShellV2Command(
211                 String.format("rm -rf '%s'", ART_APEX_DALVIK_CACHE_BACKUP_DIRNAME));
212         mTestUtils.assertCommandSucceeds(
213                 String.format("cp -r '%s' '%s'", OdsignTestUtils.ART_APEX_DALVIK_CACHE_DIRNAME,
214                         ART_APEX_DALVIK_CACHE_BACKUP_DIRNAME));
215         mHasArtifactsBackup = true;
216     }
217 
218     /**
219      * Pushes the file to a temporary location and bind-mount it at the given path. This is useful
220      * when the path is readonly.
221      */
pushAndBindMount(File localFile, String remotePath)222     private void pushAndBindMount(File localFile, String remotePath) throws Exception {
223         String tempFile = "/data/local/tmp/odsign_e2e_tests_" + UUID.randomUUID() + ".tmp";
224         assertThat(mTestInfo.getDevice().pushFile(localFile, tempFile)).isTrue();
225         mTempFiles.add(tempFile);
226 
227         // If the path has already been bind-mounted by this method before, unmount it first.
228         if (mMountPoints.contains(remotePath)) {
229             mTestUtils.assertCommandSucceeds(String.format("umount '%s'", remotePath));
230             mMountPoints.remove(remotePath);
231         }
232 
233         mTestUtils.assertCommandSucceeds(
234                 String.format("mount --bind '%s' '%s'", tempFile, remotePath));
235         mMountPoints.add(remotePath);
236         mTestUtils.assertCommandSucceeds(String.format("restorecon '%s'", remotePath));
237     }
238 
239     /** A helper class for mutating an XML file. */
240     private class XmlMutator implements AutoCloseable {
241         private final Document mDocument;
242         private final String mRemoteXmlFile;
243         private final File mLocalFile;
244 
XmlMutator(String remoteXmlFile)245         public XmlMutator(String remoteXmlFile) throws Exception {
246             // Load the XML file.
247             mRemoteXmlFile = remoteXmlFile;
248             mLocalFile = mTestInfo.getDevice().pullFile(remoteXmlFile);
249             assertThat(mLocalFile).isNotNull();
250             DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
251             mDocument = builder.parse(mLocalFile);
252         }
253 
254         @Override
close()255         public void close() throws Exception {
256             // Save the XML file.
257             Transformer transformer = TransformerFactory.newInstance().newTransformer();
258             transformer.transform(new DOMSource(mDocument), new StreamResult(mLocalFile));
259             pushAndBindMount(mLocalFile, mRemoteXmlFile);
260         }
261 
262         /** Returns a mutable XML document. */
getDocument()263         public Document getDocument() {
264             return mDocument;
265         }
266     }
267 }
268