1 /*
2  * Copyright (C) 2020 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.tradefed.invoker.logger;
17 
18 import com.android.tradefed.invoker.ExecutionFiles;
19 import com.android.tradefed.log.LogUtil.CLog;
20 import com.android.tradefed.result.ActionInProgress;
21 import com.android.tradefed.result.FailureDescription;
22 import com.android.tradefed.result.error.ErrorIdentifier;
23 
24 import java.io.File;
25 import java.lang.StackWalker.Option;
26 import java.util.HashMap;
27 import java.util.Map;
28 import java.util.Optional;
29 import java.util.concurrent.ConcurrentHashMap;
30 
31 import javax.annotation.Nullable;
32 
33 /**
34  * A class that tracks and provides the current invocation information useful anywhere inside the
35  * invocation.
36  */
37 public class CurrentInvocation {
38 
39     /** Some special named key that we will always populate for the invocation. */
40     public enum InvocationInfo {
41         WORK_FOLDER("work_folder");
42 
43         private final String mKeyName;
44 
InvocationInfo(String key)45         private InvocationInfo(String key) {
46             mKeyName = key;
47         }
48 
49         @Override
toString()50         public String toString() {
51             return mKeyName;
52         }
53     }
54 
CurrentInvocation()55     private CurrentInvocation() {}
56 
57     /** Internal storage of the invocation values. */
58     private static class InternalInvocationTracking {
59         public Map<InvocationInfo, File> mInvocationInfoFiles = new HashMap<>();
60         public ExecutionFiles mExecutionFiles;
61         public ActionInProgress mActionInProgress = ActionInProgress.UNSET;
62     }
63 
64     /**
65      * Track info per ThreadGroup as a proxy to invocation since an invocation run within one
66      * threadgroup.
67      */
68     private static final Map<ThreadGroup, InternalInvocationTracking> mPerGroupInfo =
69             new ConcurrentHashMap<ThreadGroup, CurrentInvocation.InternalInvocationTracking>();
70 
71     private static final Map<ThreadGroup, Map<InvocationLocal<?>, Optional<?>>> mInvocationLocals =
72             new ConcurrentHashMap<>();
73 
74     /**
75      * Add one key-value to be tracked at the invocation level.
76      *
77      * @param key The key under which the invocation info will be tracked.
78      * @param value The value of the invocation metric.
79      */
addInvocationInfo(InvocationInfo key, File value)80     public static void addInvocationInfo(InvocationInfo key, File value) {
81         ThreadGroup group = Thread.currentThread().getThreadGroup();
82         synchronized (mPerGroupInfo) {
83             if (mPerGroupInfo.get(group) == null) {
84                 mPerGroupInfo.put(group, new InternalInvocationTracking());
85             }
86             mPerGroupInfo.get(group).mInvocationInfoFiles.put(key, value);
87         }
88     }
89 
90     /** Returns the Map of invocation metrics for the invocation in progress. */
getInfo(InvocationInfo key)91     public static File getInfo(InvocationInfo key) {
92         ThreadGroup group = Thread.currentThread().getThreadGroup();
93         synchronized (mPerGroupInfo) {
94             if (mPerGroupInfo.get(group) == null) {
95                 mPerGroupInfo.put(group, new InternalInvocationTracking());
96             }
97             return mPerGroupInfo.get(group).mInvocationInfoFiles.get(key);
98         }
99     }
100 
101     /** Clear the invocation info for an invocation. */
clearInvocationInfos()102     public static void clearInvocationInfos() {
103         ThreadGroup group = Thread.currentThread().getThreadGroup();
104         synchronized (mPerGroupInfo) {
105             mPerGroupInfo.remove(group);
106         }
107         mInvocationLocals.remove(group);
108     }
109 
110     /**
111      * One-time registration of the {@link ExecutionFiles}. This is done by the Test Harness.
112      *
113      * @param invocFiles The registered {@link ExecutionFiles}.
114      */
registerExecutionFiles(ExecutionFiles invocFiles)115     public static void registerExecutionFiles(ExecutionFiles invocFiles) {
116         ThreadGroup group = Thread.currentThread().getThreadGroup();
117         synchronized (mPerGroupInfo) {
118             if (mPerGroupInfo.get(group) == null) {
119                 mPerGroupInfo.put(group, new InternalInvocationTracking());
120             }
121             if (mPerGroupInfo.get(group).mExecutionFiles == null) {
122                 mPerGroupInfo.get(group).mExecutionFiles = invocFiles;
123             } else {
124                 CLog.w(
125                         "CurrentInvocation#registerExecutionFiles should only be called once "
126                                 + "per invocation.");
127             }
128         }
129     }
130 
131     /** Returns the {@link ExecutionFiles} for the invocation. */
getInvocationFiles()132     public static ExecutionFiles getInvocationFiles() {
133         ThreadGroup group = Thread.currentThread().getThreadGroup();
134         synchronized (mPerGroupInfo) {
135             if (mPerGroupInfo.get(group) == null) {
136                 mPerGroupInfo.put(group, new InternalInvocationTracking());
137             }
138             return mPerGroupInfo.get(group).mExecutionFiles;
139         }
140     }
141 
142     /** Sets the {@link ActionInProgress} for the invocation. */
setActionInProgress(ActionInProgress action)143     public static void setActionInProgress(ActionInProgress action) {
144         ThreadGroup group = Thread.currentThread().getThreadGroup();
145         synchronized (mPerGroupInfo) {
146             if (mPerGroupInfo.get(group) == null) {
147                 mPerGroupInfo.put(group, new InternalInvocationTracking());
148             }
149             mPerGroupInfo.get(group).mActionInProgress = action;
150         }
151     }
152 
153     /** Returns the current {@link ActionInProgress} for the invocation. Can be null. */
getActionInProgress()154     public static @Nullable ActionInProgress getActionInProgress() {
155         ThreadGroup group = Thread.currentThread().getThreadGroup();
156         synchronized (mPerGroupInfo) {
157             if (mPerGroupInfo.get(group) == null) {
158                 return null;
159             }
160             return mPerGroupInfo.get(group).mActionInProgress;
161         }
162     }
163 
164     /**
165      * Create a failure associated with the invocation action in progress. Convenience utility to
166      * avoid calling {@link FailureDescription#setActionInProgress(ActionInProgress)}.
167      */
createFailure( String errorMessage, ErrorIdentifier errorIdentifier)168     public static FailureDescription createFailure(
169             String errorMessage, ErrorIdentifier errorIdentifier) {
170         FailureDescription failure = FailureDescription.create(errorMessage);
171         ActionInProgress action = getActionInProgress();
172         if (action != null) {
173             failure.setActionInProgress(action);
174         }
175         if (errorIdentifier != null) {
176             failure.setErrorIdentifier(errorIdentifier);
177             failure.setFailureStatus(errorIdentifier.status());
178         }
179         // Automatically populate the origin
180         Class<?> clazz = StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE).getCallerClass();
181         failure.setOrigin(clazz.getCanonicalName());
182         return failure;
183     }
184 
getLocal(InvocationLocal<T> local)185     static <T> T getLocal(InvocationLocal<T> local) {
186         ThreadGroup group = Thread.currentThread().getThreadGroup();
187         Map<InvocationLocal<?>, Optional<?>> locals =
188                 mInvocationLocals.computeIfAbsent(group, unused -> new ConcurrentHashMap<>());
189 
190         // Note that ConcurrentHashMap guarantees that the function is atomic and called at-most
191         // once.
192         Optional<?> holder =
193                 locals.computeIfAbsent(local, unused -> Optional.ofNullable(local.initialValue()));
194 
195         @SuppressWarnings("unchecked")
196         T value = (T) holder.orElse(null);
197         return value;
198     }
199 }
200