1 /* 2 * Copyright (C) 2017 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.compatibility.common.tradefed.targetprep; 17 18 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper; 19 import com.android.compatibility.common.tradefed.util.DynamicConfigFileReader; 20 import com.android.compatibility.common.util.BusinessLogic; 21 import com.android.compatibility.common.util.BusinessLogicFactory; 22 import com.android.compatibility.common.util.FeatureUtil; 23 import com.android.compatibility.common.util.PropertyUtil; 24 import com.android.tradefed.build.IBuildInfo; 25 import com.android.tradefed.config.GlobalConfiguration; 26 import com.android.tradefed.config.Option; 27 import com.android.tradefed.config.OptionClass; 28 import com.android.tradefed.device.DeviceNotAvailableException; 29 import com.android.tradefed.device.ITestDevice; 30 import com.android.tradefed.device.NativeDevice; 31 import com.android.tradefed.device.contentprovider.ContentProviderHandler; 32 import com.android.tradefed.invoker.IInvocationContext; 33 import com.android.tradefed.invoker.TestInformation; 34 import com.android.tradefed.log.LogUtil.CLog; 35 import com.android.tradefed.result.error.DeviceErrorIdentifier; 36 import com.android.tradefed.result.error.InfraErrorIdentifier; 37 import com.android.tradefed.targetprep.BaseTargetPreparer; 38 import com.android.tradefed.targetprep.BuildError; 39 import com.android.tradefed.targetprep.TargetSetupError; 40 import com.android.tradefed.testtype.IAbi; 41 import com.android.tradefed.testtype.IAbiReceiver; 42 import com.android.tradefed.testtype.IInvocationContextReceiver; 43 import com.android.tradefed.testtype.suite.TestSuiteInfo; 44 import com.android.tradefed.util.FileUtil; 45 import com.android.tradefed.util.MultiMap; 46 import com.android.tradefed.util.RunUtil; 47 import com.android.tradefed.util.StreamUtil; 48 import com.android.tradefed.util.net.HttpHelper; 49 import com.android.tradefed.util.net.IHttpHelper; 50 51 import com.google.api.client.auth.oauth2.Credential; 52 import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; 53 import com.google.common.annotations.VisibleForTesting; 54 import com.google.common.base.Strings; 55 56 import org.json.JSONException; 57 import org.json.JSONObject; 58 import org.xmlpull.v1.XmlPullParserException; 59 60 import java.io.DataOutputStream; 61 import java.io.File; 62 import java.io.FileInputStream; 63 import java.io.FileNotFoundException; 64 import java.io.IOException; 65 import java.net.HttpURLConnection; 66 import java.net.URL; 67 import java.util.ArrayList; 68 import java.util.Collections; 69 import java.util.Date; 70 import java.util.List; 71 import java.util.Map; 72 import java.util.regex.Pattern; 73 import java.util.regex.Matcher; 74 import java.util.Set; 75 76 /** 77 * Pushes business Logic to the host and the test device, for use by test cases in the test suite. 78 */ 79 @OptionClass(alias = "business-logic-preparer") 80 public class BusinessLogicPreparer extends BaseTargetPreparer 81 implements IAbiReceiver, IInvocationContextReceiver { 82 83 /* Placeholder in the service URL for the suite to be configured */ 84 private static final String SUITE_PLACEHOLDER = "{suite-name}"; 85 86 /* String for the key to get file from GlobalConfiguration */ 87 private static final String GLOBAL_APE_API_KEY = "ape-api-key"; 88 89 /* String for creating files to store the business logic configuration on the host */ 90 private static final String FILE_LOCATION = "business-logic"; 91 /* String for creating cached business logic configuration files */ 92 private static final String BL_CACHE_FILE = "business-logic-cache"; 93 /* Number of days for which cached business logic is valid */ 94 private static final int BL_CACHE_DAYS = 5; 95 /* BL_CACHE_DAYS converted to millis */ 96 private static final long BL_CACHE_MILLIS = BL_CACHE_DAYS * 1000 * 60 * 60 * 24L; 97 /* Extension of business logic files */ 98 private static final String FILE_EXT = ".bl"; 99 /* Default amount of time to attempt connection to the business logic service, in seconds */ 100 private static final int DEFAULT_CONNECTION_TIME = 60; 101 /* Time to wait between connection attempts to the business logic service, in millis */ 102 private static final long SLEEP_BETWEEN_CONNECTIONS_MS = 5000; // 5 seconds 103 /* Dynamic config constants */ 104 private static final String DYNAMIC_CONFIG_FEATURES_KEY = "business_logic_device_features"; 105 private static final String DYNAMIC_CONFIG_PROPERTIES_KEY = "business_logic_device_properties"; 106 private static final String DYNAMIC_CONFIG_PACKAGES_KEY = "business_logic_device_packages"; 107 private static final String DYNAMIC_CONFIG_EXTENDED_DEVICE_INFO_KEY = 108 "business_logic_extended_device_info"; 109 110 @Option(name = "business-logic-url", description = "The URL to use when accessing the " + 111 "business logic service, parameters not included", mandatory = true) 112 private String mUrl; 113 114 @Option(name = "business-logic-api-key", description = "The API key to use when accessing " + 115 "the business logic service.", mandatory = true) 116 private String mApiKey; 117 118 @Option(name = "business-logic-api-scope", description = "The URI of api scope to use when " + 119 "retrieving business logic rules.") 120 /* URI of api scope to use when retrieving business logic rules */ 121 private String mApiScope; 122 123 @Option(name = "cache-business-logic", description = "Whether to keep and use cached " + 124 "business logic files.") 125 private boolean mCache = false; 126 127 @Option(name = "clean-cache-business-logic", description = "Like option " + 128 "'cache-business-logic', but forces a refresh of the cached business logic file") 129 private boolean mCleanCache = false; 130 131 @Option(name = "ignore-business-logic-failure", description = "Whether to proceed with the " + 132 "suite invocation if retrieval of business logic fails.") 133 private boolean mIgnoreFailure = false; 134 135 @Option(name = "business-logic-connection-time", description = "Amount of time to attempt " + 136 "connection to the business logic service, in seconds.") 137 private int mMaxConnectionTime = DEFAULT_CONNECTION_TIME; 138 139 @Option(name = "config-filename", description = "The module name for module-level " + 140 "configurations, or the suite name for suite-level configurations. Will lookup " + 141 "suite name if not provided.") 142 private String mModuleName = null; 143 144 @Option(name = "version", description = "The module configuration version to retrieve.") 145 private String mModuleVersion = null; 146 147 @Option( 148 name = "suite-version-extraction-regex", 149 description = 150 "A regex string with a named capture group \"version\". Used to compare" 151 + " versions on the BL server. To exclude a platform version name" 152 + " prefix for example, use \".+?_sts(?<version>.+)\"" 153 + "('12.1_sts-r1' -> '-r1'). Note that <version> can be represented" 154 + " in xml with <version>.") 155 private String mSuiteVersionExtractionRegex = "(?<version>.+)"; 156 157 private String mDeviceFilePushed; 158 private String mHostFilePushed; 159 private IAbi mAbi = null; 160 private IInvocationContext mModuleContext = null; 161 162 /** {@inheritDoc} */ 163 @Override setAbi(IAbi abi)164 public void setAbi(IAbi abi) { 165 mAbi = abi; 166 } 167 168 /** {@inheritDoc} */ 169 @Override getAbi()170 public IAbi getAbi() { 171 return mAbi; 172 } 173 174 175 /** {@inheritDoc} */ 176 @Override setInvocationContext(IInvocationContext invocationContext)177 public void setInvocationContext(IInvocationContext invocationContext) { 178 mModuleContext = invocationContext; 179 } 180 181 /** {@inheritDoc} */ 182 @Override setUp(TestInformation testInfo)183 public void setUp(TestInformation testInfo) 184 throws TargetSetupError, BuildError, DeviceNotAvailableException { 185 IBuildInfo buildInfo = testInfo.getBuildInfo(); 186 ITestDevice device = testInfo.getDevice(); 187 CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(buildInfo); 188 if (buildHelper.hasBusinessLogicHostFile()) { 189 CLog.i("Business logic file already collected, skipping BusinessLogicPreparer."); 190 return; 191 } 192 // Ensure mModuleName is set. 193 if (mModuleName == null) { 194 mModuleName = ""; 195 CLog.w("Option config-filename isn't set. Using empty string instead."); 196 } 197 if (mModuleVersion == null) { 198 CLog.w("Option version isn't set. Using 'null' instead."); 199 mModuleVersion = "null"; 200 } 201 String requestParams = buildRequestParams(device, buildInfo); 202 String baseUrl = mUrl.replace(SUITE_PLACEHOLDER, getSuiteNames().get(0)); 203 String businessLogicString = null; 204 // use cached business logic string if options are set accordingly and cache is valid, 205 // otherwise proceed with remote download. 206 if (!shouldReadCache() 207 || (businessLogicString = readFromCache(baseUrl, requestParams)) == null) { 208 CLog.i("Attempting to connect to business logic service..."); 209 } 210 long start = System.currentTimeMillis(); 211 Exception connectIssue = null; 212 while (businessLogicString == null 213 && System.currentTimeMillis() < (start + (mMaxConnectionTime * 1000))) { 214 try { 215 businessLogicString = doPost(baseUrl, requestParams); 216 } catch (IOException e) { 217 // ignore, re-attempt connection with remaining time 218 CLog.d("BusinessLogic connection failure message: %s\nRetrying...", e.getMessage()); 219 connectIssue = e; 220 RunUtil.getDefault().sleep(SLEEP_BETWEEN_CONNECTIONS_MS); 221 } 222 } 223 if (businessLogicString == null) { 224 if (mIgnoreFailure) { 225 CLog.e("Failed to connect to business logic service.\nProceeding with test " 226 + "invocation, tests depending on the remote configuration will fail.\n"); 227 return; 228 } else { 229 String baseMessage = 230 String.format( 231 "Cannot connect to business logic service for config %s. If this" 232 + " problem persists, re-invoking with option" 233 + " '--ignore-business-logic-failure' will cause tests to" 234 + " execute anyways (though tests depending on the remote" 235 + " configuration will fail).", 236 mModuleName); 237 if (connectIssue != null) { 238 baseMessage = String.format("%s.\n%s", connectIssue.getMessage(), baseMessage); 239 } 240 throw new TargetSetupError( 241 baseMessage, 242 device.getDeviceDescriptor(), 243 InfraErrorIdentifier.ANDROID_PARTNER_SERVER_ERROR); 244 } 245 } 246 247 if (shouldWriteCache()) { 248 writeToCache(businessLogicString, baseUrl, requestParams, mCleanCache); 249 } 250 // Push business logic string to host file 251 try { 252 File hostFile = FileUtil.createTempFile(FILE_LOCATION, FILE_EXT); 253 FileUtil.writeToFile(businessLogicString, hostFile); 254 mHostFilePushed = hostFile.getAbsolutePath(); 255 // Ensure bitness is set. 256 String bitness = (mAbi != null) ? mAbi.getBitness() : ""; 257 buildHelper.setBusinessLogicHostFile(hostFile, bitness + mModuleName); 258 } catch (IOException e) { 259 throw new TargetSetupError( 260 String.format( 261 "Retrieved business logic for config %s could not be written to host", 262 mModuleName), 263 device.getDeviceDescriptor(), 264 InfraErrorIdentifier.FAIL_TO_CREATE_FILE); 265 } 266 // Push business logic string to device file 267 removeDeviceFile(device); // remove any existing business logic file from device 268 if (device.pushString(businessLogicString, BusinessLogic.DEVICE_FILE)) { 269 mDeviceFilePushed = BusinessLogic.DEVICE_FILE; 270 } else { 271 throw new TargetSetupError( 272 String.format( 273 "Retrieved business logic for config %s could not be written to device" 274 + " %s", 275 mModuleName, device.getSerialNumber()), 276 device.getDeviceDescriptor(), 277 DeviceErrorIdentifier.FAIL_PUSH_FILE); 278 } 279 checkAndInstallContentProvider(device); 280 } 281 282 /** Helper to populate the business logic service request with info about the device. */ 283 @VisibleForTesting buildRequestParams(ITestDevice device, IBuildInfo buildInfo)284 String buildRequestParams(ITestDevice device, IBuildInfo buildInfo) 285 throws DeviceNotAvailableException, TargetSetupError { 286 MultiMap<String, String> paramMap = new MultiMap<>(); 287 String suiteVersion = getSuiteVersionExtracted(buildInfo); 288 if (suiteVersion == null) { 289 suiteVersion = "null"; 290 } 291 paramMap.put("suite_version", suiteVersion); 292 paramMap.put("module_version", mModuleVersion); 293 paramMap.put("oem", String.valueOf(PropertyUtil.getManufacturer(device))); 294 for (String feature : getBusinessLogicFeatures(device, buildInfo)) { 295 paramMap.put("features", feature); 296 } 297 for (String property : getBusinessLogicProperties(device, buildInfo)) { 298 paramMap.put("properties", property); 299 } 300 for (String pkg : getBusinessLogicPackages(device, buildInfo)) { 301 paramMap.put("packages", pkg); 302 } 303 for (String deviceInfo : getExtendedDeviceInfo(buildInfo)) { 304 paramMap.put("device_info", deviceInfo); 305 } 306 IHttpHelper helper = new HttpHelper(); 307 String paramString = helper.buildParameters(paramMap); 308 CLog.d("Built param string: \"%s\"", paramString); 309 return paramString; 310 } 311 312 /** 313 * Extract the version string we should use to compare versions on the BL server. Control what's 314 * extracted with the suite-version-extraction-regex option. This defaults to no changes to the 315 * original build. Suites that prepend the platform version name may use this to remove it. 316 */ 317 @VisibleForTesting getSuiteVersionExtracted(IBuildInfo buildInfo)318 String getSuiteVersionExtracted(IBuildInfo buildInfo) throws TargetSetupError { 319 CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(buildInfo); 320 String suiteVersion = buildHelper.getSuiteVersion(); 321 if (suiteVersion == null) { 322 return null; 323 } 324 Matcher m = Pattern.compile(mSuiteVersionExtractionRegex).matcher(suiteVersion); 325 if (m.matches()) { 326 try { 327 String extracted = m.group("version"); 328 CLog.d("original version: %s, extracted version: %s", suiteVersion, extracted); 329 return extracted; 330 } catch (IllegalStateException | IllegalArgumentException e) { 331 throw new TargetSetupError( 332 String.format( 333 "Could not match the extraction regex (%s) against the suite" 334 + " version (%s)", 335 mSuiteVersionExtractionRegex, suiteVersion), 336 e, 337 InfraErrorIdentifier.OPTION_CONFIGURATION_ERROR); 338 } 339 } 340 throw new TargetSetupError( 341 String.format( 342 "Could not match the extraction regex (%s) against the suite version (%s)", 343 mSuiteVersionExtractionRegex, suiteVersion), 344 InfraErrorIdentifier.OPTION_CONFIGURATION_ERROR); 345 } 346 347 /** 348 * Return list of test-suite-tag from configuration if it's not empty, 349 * otherwise, return the name from test-suite-info.properties. 350 */ 351 @VisibleForTesting getSuiteNames()352 List<String> getSuiteNames() { 353 if (mModuleContext != null) { 354 List<String> testSuiteTags = mModuleContext.getConfigurationDescriptor(). 355 getSuiteTags(); 356 if (!testSuiteTags.isEmpty()) { 357 CLog.i("Adding %s from test suite tags to get value from dynamic config", 358 testSuiteTags); 359 return testSuiteTags; 360 } 361 } 362 String suiteName = TestSuiteInfo.getInstance().getName().toLowerCase(); 363 CLog.i("Using %s from TestSuiteInfo to get value from dynamic config", 364 suiteName); 365 return Collections.singletonList(suiteName); 366 } 367 368 /** 369 * Check and install tradefed content provider. 370 * 371 * <p>Do nothing if the content provider is already installed, otherwise install it and 372 * initialize a {@code ContentProviderHandler} for the current user. 373 * 374 * <p>BusinessLogicTestCase relies on the tradefed content provider to read the BL config file 375 * successfully from the device side. 376 */ 377 @VisibleForTesting checkAndInstallContentProvider(ITestDevice device)378 static void checkAndInstallContentProvider(ITestDevice device) 379 throws DeviceNotAvailableException { 380 if (!device.isPackageInstalled(ContentProviderHandler.PACKAGE_NAME)) { 381 if (device instanceof NativeDevice) { 382 var unused = ((NativeDevice) device).getContentProvider(device.getCurrentUser()); 383 } 384 } 385 } 386 387 /* Get device properties list, with element format "<property_name>:<property_value>" */ getBusinessLogicProperties(ITestDevice device, IBuildInfo buildInfo)388 private List<String> getBusinessLogicProperties(ITestDevice device, IBuildInfo buildInfo) 389 throws DeviceNotAvailableException { 390 List<String> properties = new ArrayList<>(); 391 Map<String, String> clientIds = PropertyUtil.getClientIds(device); 392 for (Map.Entry<String, String> id : clientIds.entrySet()) { 393 // add client IDs to the list of properties 394 properties.add(String.format("%s:%s", id.getKey(), id.getValue())); 395 } 396 397 try { 398 List<String> propertyNames = DynamicConfigFileReader.getValuesFromConfig(buildInfo, 399 getSuiteNames(), DYNAMIC_CONFIG_PROPERTIES_KEY); 400 for (String name : propertyNames) { 401 // Use String.valueOf in case property is undefined for the device ("null") 402 String value = String.valueOf(device.getProperty(name)); 403 properties.add(String.format("%s:%s", name, value)); 404 } 405 } catch (XmlPullParserException | IOException e) { 406 CLog.e("Failed to pull business logic properties from dynamic config"); 407 } 408 return properties; 409 } 410 411 /* Get device features list */ getBusinessLogicFeatures(ITestDevice device, IBuildInfo buildInfo)412 private List<String> getBusinessLogicFeatures(ITestDevice device, IBuildInfo buildInfo) 413 throws DeviceNotAvailableException { 414 try { 415 List<String> dynamicConfigFeatures = DynamicConfigFileReader.getValuesFromConfig( 416 buildInfo, getSuiteNames(), DYNAMIC_CONFIG_FEATURES_KEY); 417 Set<String> deviceFeatures = FeatureUtil.getAllFeatures(device); 418 dynamicConfigFeatures.retainAll(deviceFeatures); 419 return dynamicConfigFeatures; 420 } catch (XmlPullParserException | IOException e) { 421 CLog.e("Failed to pull business logic features from dynamic config"); 422 return new ArrayList<>(); 423 } 424 } 425 426 /* Get device packages list */ getBusinessLogicPackages(ITestDevice device, IBuildInfo buildInfo)427 private List<String> getBusinessLogicPackages(ITestDevice device, IBuildInfo buildInfo) 428 throws DeviceNotAvailableException { 429 try { 430 List<String> dynamicConfigPackages = DynamicConfigFileReader.getValuesFromConfig( 431 buildInfo, getSuiteNames(), DYNAMIC_CONFIG_PACKAGES_KEY); 432 Set<String> devicePackages = device.getInstalledPackageNames(); 433 dynamicConfigPackages.retainAll(devicePackages); 434 return dynamicConfigPackages; 435 } catch (XmlPullParserException | IOException e) { 436 CLog.e("Failed to pull business logic packages from dynamic config"); 437 return new ArrayList<>(); 438 } 439 } 440 441 /* Get extended device info*/ getExtendedDeviceInfo(IBuildInfo buildInfo)442 private List<String> getExtendedDeviceInfo(IBuildInfo buildInfo) { 443 List<String> extendedDeviceInfo = new ArrayList<>(); 444 File deviceInfoPath = buildInfo.getFile(DeviceInfoCollector.DEVICE_INFO_DIR); 445 if (deviceInfoPath == null || !deviceInfoPath.exists()) { 446 CLog.w("Device Info directory was not created (Make sure you are not running plan " + 447 "\"*ts-dev\" or including option -d/--skip-device-info)"); 448 return extendedDeviceInfo; 449 } 450 List<String> requiredDeviceInfo = null; 451 try { 452 requiredDeviceInfo = DynamicConfigFileReader.getValuesFromConfig( 453 buildInfo, getSuiteNames(), DYNAMIC_CONFIG_EXTENDED_DEVICE_INFO_KEY); 454 } catch (XmlPullParserException | IOException e) { 455 CLog.e("Failed to pull business logic Extended DeviceInfo from dynamic config. " 456 + "Error: %s", e); 457 return extendedDeviceInfo; 458 } 459 File ediFile = null; 460 String[] fileAndKey = null; 461 try{ 462 for (String ediEntry: requiredDeviceInfo) { 463 fileAndKey = ediEntry.split(":"); 464 if (fileAndKey.length <= 1) { 465 CLog.e("Dynamic config Extended DeviceInfo key has problem."); 466 return new ArrayList<>(); 467 } 468 ediFile = FileUtil 469 .findFile(deviceInfoPath, fileAndKey[0] + ".deviceinfo.json"); 470 if (ediFile == null) { 471 CLog.e( 472 "Could not find Extended DeviceInfo JSON file: %s.", 473 deviceInfoPath + fileAndKey[0] + ".deviceinfo.json"); 474 return new ArrayList<>(); 475 } 476 String jsonString = FileUtil.readStringFromFile(ediFile); 477 JSONObject jsonObj = new JSONObject(jsonString); 478 String value = jsonObj.getString(fileAndKey[1]); 479 extendedDeviceInfo 480 .add(String.format("%s:%s:%s", fileAndKey[0], fileAndKey[1], value)); 481 } 482 }catch(JSONException | IOException | RuntimeException e){ 483 CLog.e( 484 "Failed to read or parse Extended DeviceInfo JSON file: %s. Error: %s", 485 deviceInfoPath + fileAndKey[0] + ".deviceinfo.json", e); 486 return new ArrayList<>(); 487 } 488 return extendedDeviceInfo; 489 } 490 shouldReadCache()491 private boolean shouldReadCache() { 492 return mCache && !mCleanCache; 493 } 494 shouldWriteCache()495 private boolean shouldWriteCache() { 496 return mCache || mCleanCache; 497 } 498 499 /** 500 * Read the string from the business logic cache, handling the following cases with a null 501 * return value: 502 * - The cached file does not exist 503 * - The cached file cannot be read 504 * - The cached file is timestamped more than BL_CACHE_DAYS prior to now 505 * In the last two cases, the file is deleted so an up-to-date configuration may be cached anew 506 */ readFromCache(String baseUrl, String params)507 private static synchronized String readFromCache(String baseUrl, String params) { 508 // baseUrl + params hashCode makes file unique, in case host runs invocations for different 509 // device builds and/or test suites using business logic 510 File cachedFile = getCachedFile(baseUrl, params); 511 if (!cachedFile.exists()) { 512 CLog.i("No cached business logic found"); 513 return null; 514 } 515 try { 516 BusinessLogic cachedLogic = BusinessLogicFactory.createFromFile(cachedFile); 517 Date cachedDate = cachedLogic.getTimestamp(); 518 if (System.currentTimeMillis() - cachedDate.getTime() < BL_CACHE_MILLIS) { 519 CLog.i("Using cached business logic from: %s", cachedDate.toString()); 520 return FileUtil.readStringFromFile(cachedFile); 521 } else { 522 CLog.i("Cached business logic out-of-date, deleting cached file"); 523 FileUtil.deleteFile(cachedFile); 524 } 525 } catch (IOException e) { 526 CLog.w("Failed to read cached business logic, deleting cached file"); 527 FileUtil.deleteFile(cachedFile); 528 } 529 return null; 530 } 531 532 /** 533 * Write a string retrieved from the business logic service to the cache file, only if the 534 * file does not already exist. Synchronize this method to prevent concurrent writes in the 535 * sharding case. 536 * @param blString the string to cache 537 * @param baseUrl the base business logic request url containing suite info 538 * @param params the string of params for the business logic request containing device info 539 */ writeToCache(String blString, String baseUrl, String params, boolean overwrite)540 private static synchronized void writeToCache(String blString, String baseUrl, String params, 541 boolean overwrite) { 542 // baseUrl + params hashCode makes file unique, in case host runs invocations for different 543 // device builds and/or test suites using business logic 544 File cachedFile = getCachedFile(baseUrl, params); 545 if (!cachedFile.exists() || overwrite) { 546 // don't overwrite existing file, whether from previous shard or previous invocation 547 try { 548 FileUtil.writeToFile(blString, cachedFile); 549 } catch (IOException e) { 550 throw new RuntimeException("Failed to write business logic to cache file", e); 551 } 552 } 553 } 554 555 /** 556 * Get the cached business logic file given the base url and params used to retrieve this logic. 557 */ getCachedFile(String baseUrl, String params)558 private static File getCachedFile(String baseUrl, String params) { 559 int hashCode = (baseUrl + params).hashCode(); 560 return new File(System.getProperty("java.io.tmpdir"), BL_CACHE_FILE + hashCode); 561 } 562 doPost(String baseUrl, String params)563 private String doPost(String baseUrl, String params) throws IOException { 564 String accessToken = getToken(); 565 if (Strings.isNullOrEmpty(accessToken)) { 566 // Set API key on base URL 567 baseUrl += String.format("?key=%s", mApiKey); 568 } 569 URL url = new URL(baseUrl); 570 HttpURLConnection conn = (HttpURLConnection) url.openConnection(); 571 conn.setRequestMethod("POST"); 572 conn.setRequestProperty("User-Agent", "BusinessLogicClient"); 573 if (!Strings.isNullOrEmpty(accessToken)) { 574 // Set authorization access token in POST header 575 conn.setRequestProperty("Authorization", String.format("Bearer %s", accessToken)); 576 } 577 // Send params in POST request body 578 conn.setDoOutput(true); 579 try (DataOutputStream wr = new DataOutputStream(conn.getOutputStream())) { 580 wr.writeBytes(params); 581 } 582 int responseCode = conn.getResponseCode(); 583 CLog.d("Business Logic Service Response Code : %s", responseCode); 584 return StreamUtil.getStringFromStream(conn.getInputStream()); 585 } 586 587 /** {@inheritDoc} */ 588 @Override tearDown(TestInformation testInfo, Throwable e)589 public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException { 590 // Clean up existing host and device files unconditionally 591 if (mHostFilePushed != null) { 592 FileUtil.deleteFile(new File(mHostFilePushed)); 593 } 594 if (mDeviceFilePushed != null && !(e instanceof DeviceNotAvailableException)) { 595 removeDeviceFile(testInfo.getDevice()); 596 } 597 } 598 599 /** Remove business logic file from the device */ removeDeviceFile(ITestDevice device)600 private static void removeDeviceFile(ITestDevice device) throws DeviceNotAvailableException { 601 device.deleteFile(BusinessLogic.DEVICE_FILE); 602 } 603 604 /** 605 * Returns an OAuth2 token string obtained using a service account json key file. 606 * 607 * Uses the service account key file location stored in environment variable 'APE_API_KEY' 608 * to request an OAuth2 token. If APE_API_KEY wasn't set, try to get if file is dynamically 609 * downloaded from GlobalConfiguration. 610 */ getToken()611 private String getToken() { 612 String keyFilePath = System.getenv("APE_API_KEY"); 613 if (Strings.isNullOrEmpty(keyFilePath)) { 614 File globalKeyFile = GlobalConfiguration.getInstance().getHostOptions(). 615 getServiceAccountJsonKeyFiles().get(GLOBAL_APE_API_KEY); 616 if (globalKeyFile == null || !globalKeyFile.exists()) { 617 CLog.d("Unable to fetch the service key because neither environment variable " + 618 "APE_API_KEY is set nor the key file is dynamically downloaded."); 619 return null; 620 } 621 keyFilePath = globalKeyFile.getAbsolutePath(); 622 } 623 if (Strings.isNullOrEmpty(mApiScope)) { 624 CLog.d("API scope not set, use flag --business-logic-api-scope."); 625 return null; 626 } 627 try { 628 Credential credential = GoogleCredential.fromStream(new FileInputStream(keyFilePath)) 629 .createScoped(Collections.singleton(mApiScope)); 630 credential.refreshToken(); 631 return credential.getAccessToken(); 632 } catch (FileNotFoundException e) { 633 CLog.e(String.format("Service key file %s doesn't exist.", keyFilePath)); 634 } catch (IOException e) { 635 CLog.e(String.format("Can't read the service key file, %s", keyFilePath)); 636 } 637 return null; 638 } 639 } 640