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 package com.android.hoststubgen.hosthelper; 17 18 import java.io.PrintStream; 19 import java.lang.StackWalker.Option; 20 import java.lang.reflect.Method; 21 import java.lang.reflect.Modifier; 22 import java.util.HashMap; 23 24 import javax.annotation.concurrent.GuardedBy; 25 26 /** 27 * Utilities used in the host side test environment. 28 */ 29 public class HostTestUtils { HostTestUtils()30 private HostTestUtils() { 31 } 32 33 /** 34 * Same as ASM's Type.getInternalName(). Copied here, to avoid having a reference to ASM 35 * in this JAR. 36 */ getInternalName(final Class<?> clazz)37 public static String getInternalName(final Class<?> clazz) { 38 return clazz.getName().replace('.', '/'); 39 } 40 41 public static final String CLASS_INTERNAL_NAME = getInternalName(HostTestUtils.class); 42 43 /** If true, we won't print method call log. */ 44 private static final boolean SKIP_METHOD_LOG = "1".equals(System.getenv( 45 "HOSTTEST_SKIP_METHOD_LOG")); 46 47 /** If true, we won't print class load log. */ 48 private static final boolean SKIP_CLASS_LOG = "1".equals(System.getenv( 49 "HOSTTEST_SKIP_CLASS_LOG")); 50 51 /** If true, we won't perform non-stub method direct call check. */ 52 private static final boolean SKIP_NON_STUB_METHOD_CHECK = "1".equals(System.getenv( 53 "HOSTTEST_SKIP_NON_STUB_METHOD_CHECK")); 54 55 56 /** 57 * Method call log will be printed to it. 58 */ 59 public static PrintStream logPrintStream = System.out; 60 61 /** 62 * Called from methods with FilterPolicy.Throw. 63 */ onThrowMethodCalled()64 public static void onThrowMethodCalled() { 65 // TODO: Maybe add call tracking? 66 throw new RuntimeException( 67 "This method is not yet supported under the Ravenwood deviceless testing " 68 + "environment; consider requesting support from the API owner or " 69 + "consider using Mockito; more details at go/ravenwood-docs"); 70 } 71 72 /** 73 * Trampoline method for method-call-hook. 74 */ callMethodCallHook( Class<?> methodClass, String methodName, String methodDescriptor, String callbackMethod )75 public static void callMethodCallHook( 76 Class<?> methodClass, 77 String methodName, 78 String methodDescriptor, 79 String callbackMethod 80 ) { 81 callStaticMethodByName(callbackMethod, "method call hook", methodClass, 82 methodName, methodDescriptor); 83 } 84 85 /** 86 * I can be used as 87 * {@code --default-method-call-hook 88 * com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall}. 89 * 90 * It logs every single methods called. 91 */ logMethodCall( Class<?> methodClass, String methodName, String methodDescriptor )92 public static void logMethodCall( 93 Class<?> methodClass, 94 String methodName, 95 String methodDescriptor 96 ) { 97 if (SKIP_METHOD_LOG) { 98 return; 99 } 100 logPrintStream.println("# method called: " + methodClass.getCanonicalName() + "." 101 + methodName + methodDescriptor); 102 } 103 104 private static final StackWalker sStackWalker = 105 StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE); 106 107 /** 108 * Return a {@link StackWalker} that supports {@link StackWalker#getCallerClass()}. 109 */ getStackWalker()110 public static StackWalker getStackWalker() { 111 return sStackWalker; 112 } 113 114 /** 115 * Cache used by {@link #isClassAllowedToCallNonStubMethods}. 116 */ 117 @GuardedBy("sAllowedClasses") 118 private static final HashMap<Class, Boolean> sAllowedClasses = new HashMap(); 119 120 /** 121 * Return true if a given class is allowed to access non-stub methods -- that is, if the class 122 * is in the hoststubgen generated JARs. (not in the test jar.) 123 */ isClassAllowedToCallNonStubMethods(Class<?> clazz)124 private static boolean isClassAllowedToCallNonStubMethods(Class<?> clazz) { 125 synchronized (sAllowedClasses) { 126 var cached = sAllowedClasses.get(clazz); 127 if (cached != null) { 128 return cached; 129 } 130 } 131 // All processed classes have this annotation. 132 var allowed = clazz.getAnnotation(HostStubGenKeptInImpl.class) != null; 133 134 // Java classes should be able to access any methods. (via callbacks, etc.) 135 if (!allowed) { 136 if (clazz.getPackageName().startsWith("java.") 137 || clazz.getPackageName().startsWith("javax.")) { 138 allowed = true; 139 } 140 } 141 synchronized (sAllowedClasses) { 142 sAllowedClasses.put(clazz, allowed); 143 } 144 return allowed; 145 } 146 147 /** 148 * Called when non-stub methods are called. We do a host-unsupported method direct call check 149 * in here. 150 */ onNonStubMethodCalled( String methodClass, String methodName, String methodDescriptor, Class<?> callerClass)151 public static void onNonStubMethodCalled( 152 String methodClass, 153 String methodName, 154 String methodDescriptor, 155 Class<?> callerClass) { 156 if (SKIP_NON_STUB_METHOD_CHECK) { 157 return; 158 } 159 if (isClassAllowedToCallNonStubMethods(callerClass)) { 160 return; // Generated class is allowed to call framework class. 161 } 162 logPrintStream.println("! " + methodClass + "." + methodName + methodDescriptor 163 + " called by " + callerClass.getCanonicalName()); 164 } 165 166 /** 167 * Called when any top level class (not nested classes) in the impl jar is loaded. 168 * 169 * When HostStubGen inject a class-load hook, it's always a call to this method, with the 170 * actual method name as the second argument. 171 * 172 * This method discovers the hook method with reflections and call it. 173 * 174 * TODO: Add a unit test. 175 */ onClassLoaded(Class<?> loadedClass, String callbackMethod)176 public static void onClassLoaded(Class<?> loadedClass, String callbackMethod) { 177 logPrintStream.println("! Class loaded: " + loadedClass.getCanonicalName() 178 + " calling hook " + callbackMethod); 179 180 callStaticMethodByName(callbackMethod, "class load hook", loadedClass); 181 } 182 callStaticMethodByName(String classAndMethodName, String description, Object... args)183 private static void callStaticMethodByName(String classAndMethodName, 184 String description, Object... args) { 185 // Forward the call to callbackMethod. 186 final int lastPeriod = classAndMethodName.lastIndexOf("."); 187 188 if ((lastPeriod) < 0 || (lastPeriod == classAndMethodName.length() - 1)) { 189 throw new HostTestException(String.format( 190 "Unable to find %s: malformed method name \"%s\"", 191 description, 192 classAndMethodName)); 193 } 194 195 final String className = classAndMethodName.substring(0, lastPeriod); 196 final String methodName = classAndMethodName.substring(lastPeriod + 1); 197 198 Class<?> clazz = null; 199 try { 200 clazz = Class.forName(className); 201 } catch (Exception e) { 202 throw new HostTestException(String.format( 203 "Unable to find %s: Class %s not found", 204 description, 205 className), e); 206 } 207 if (!Modifier.isPublic(clazz.getModifiers())) { 208 throw new HostTestException(String.format( 209 "Unable to find %s: Class %s must be public", 210 description, 211 className)); 212 } 213 214 Class<?>[] argTypes = new Class[args.length]; 215 for (int i = 0; i < args.length; i++) { 216 argTypes[i] = args[i].getClass(); 217 } 218 219 Method method = null; 220 try { 221 method = clazz.getMethod(methodName, argTypes); 222 } catch (Exception e) { 223 throw new HostTestException(String.format( 224 "Unable to find %s: class %s doesn't have method %s" 225 + " (method must take exactly one parameter of type Class," 226 + " and public static)", 227 description, className, methodName), e); 228 } 229 if (!(Modifier.isPublic(method.getModifiers()) 230 && Modifier.isStatic(method.getModifiers()))) { 231 throw new HostTestException(String.format( 232 "Unable to find %s: Method %s in class %s must be public static", 233 description, methodName, className)); 234 } 235 try { 236 method.invoke(null, args); 237 } catch (Exception e) { 238 throw new HostTestException(String.format( 239 "Unable to invoke %s %s.%s", 240 description, className, methodName), e); 241 } 242 } 243 244 /** 245 * I can be used as 246 * {@code --default-class-load-hook 247 * com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded}. 248 * 249 * It logs every loaded class. 250 */ logClassLoaded(Class<?> clazz)251 public static void logClassLoaded(Class<?> clazz) { 252 if (SKIP_CLASS_LOG) { 253 return; 254 } 255 logPrintStream.println("# class loaded: " + clazz.getCanonicalName()); 256 } 257 } 258