blob: 0ca1be1571eb78453fa9e845cc6190e9c427fb0e [file] [log] [blame]
Di Qian38c02a72019-11-18 19:14:07 -08001/*
2 * Copyright (C) 2019 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 */
16package com.android.tradefed.cluster;
17
18import com.google.common.annotations.VisibleForTesting;
19
20import com.android.tradefed.command.remote.DeviceDescriptor;
21import com.android.tradefed.config.Option;
22import com.android.tradefed.config.OptionClass;
23import com.android.tradefed.device.DeviceAllocationState;
24import com.android.tradefed.device.IDeviceMonitor;
25import com.android.tradefed.log.LogUtil.CLog;
26import com.android.tradefed.util.CommandResult;
27import com.android.tradefed.util.CommandStatus;
28import com.android.tradefed.util.IRunUtil;
29import com.android.tradefed.util.QuotationAwareTokenizer;
30import com.android.tradefed.util.RunUtil;
31
32import java.util.HashMap;
33import java.util.List;
34import java.util.Map;
35
36/**
37 * An {@link IDeviceMonitor} implementation that reports results to the Tradefed Cluster service.
38 */
39@OptionClass(alias = "cluster-device-monitor")
40public class ClusterDeviceMonitor implements IDeviceMonitor {
41
42 @Option(
43 name = "host-info-cmd",
44 description =
45 "A label and command to run periodically, to "
46 + "collect and send host info to the backend. May be repeated. Commands containing "
47 + "spaces should be double-quoted.")
48 private Map<String, String> mHostInfoCmds = new HashMap<String, String>();
49
50 private Map<String, String[]> mTokenizedHostInfoCmds = null;
51
52 @Option(
53 name = "host-info-cmd-timeout",
54 description =
55 "How long to wait for each "
56 + "host-info-cmd to complete, in millis. If the command times out, a (null) value "
57 + "will be passed to the backend for that particular command.")
58 private long mHostInfoCmdTimeout = 5 * 1000;
59
60 /** Worker thread to dispatch a cluster host event that includes a snapshot of the devices */
61 class EventDispatcher extends Thread {
62
63 private boolean mIsCanceled = false;
64 private IClusterEventUploader<ClusterHostEvent> mEventUploader = null;
65 private IClusterOptions mClusterOptions = null;
66
67 public EventDispatcher() {
68 super("ClusterDeviceMonitor.EventDispatcher");
69 this.setDaemon(true);
70 }
71
72 @Override
73 public void run() {
74 try {
75 while (!mIsCanceled) {
76 dispatch();
77 getRunUtil()
78 .sleep(
79 ClusterHostUtil.getClusterOptions()
80 .getDeviceMonitorSnapshotInterval());
81 }
82 } catch (Exception e) {
83 CLog.e(e);
84 }
85 }
86
87 IClusterEventUploader<ClusterHostEvent> getEventUploader() {
88 if (mEventUploader == null) {
89 mEventUploader = ClusterHostUtil.getClusterClient().getHostEventUploader();
90 }
91 return mEventUploader;
92 }
93
94 IClusterOptions getClusterOptions() {
95 if (mClusterOptions == null) {
96 mClusterOptions = ClusterHostUtil.getClusterOptions();
97 }
98 return mClusterOptions;
99 }
100
101 void dispatch() {
102 CLog.d("Start device snapshot.");
103 final IClusterEventUploader<ClusterHostEvent> eventUploader = getEventUploader();
104 final List<DeviceDescriptor> devices = listDevices();
105 final ClusterHostEvent.Builder builder =
106 new ClusterHostEvent.Builder()
107 .setHostEventType(ClusterHostEvent.HostEventType.DeviceSnapshot)
108 .setHostName(ClusterHostUtil.getHostName())
109 .setTfVersion(ClusterHostUtil.getTfVersion())
110 .setData(getAdditionalHostInfo())
111 .setClusterId(getClusterOptions().getClusterId())
Xing Dai81f6f9f2019-12-03 14:01:32 -0800112 .setNextClusterIds(getClusterOptions().getNextClusterIds())
113 .setLabName(getClusterOptions().getLabName());
Di Qian38c02a72019-11-18 19:14:07 -0800114 for (DeviceDescriptor device : devices) {
115 if (device.isTemporary()) {
116 // Do not report temporary devices, they will go away when the invocation
117 // finish.
118 continue;
119 }
120 final ClusterDeviceInfo.Builder deviceBuilder = new ClusterDeviceInfo.Builder();
121 String runTargetFormat = getClusterOptions().getRunTargetFormat();
122 deviceBuilder.setDeviceDescriptor(device);
123 deviceBuilder.setRunTarget(
124 ClusterHostUtil.getRunTarget(
125 device, runTargetFormat, getClusterOptions().getDeviceTag()));
126
127 builder.addDeviceInfo(deviceBuilder.build());
128 }
129 // We want to force an upload.
130 CLog.d("Dispatched devicesnapshot.");
131 eventUploader.postEvent(builder.build());
132 eventUploader.flush();
133 }
134
135 void cancel() {
136 mIsCanceled = true;
137 }
138
139 boolean isCanceled() {
140 return mIsCanceled;
141 }
142 }
143
144 private EventDispatcher mDispatcher;
145 private DeviceLister mDeviceLister;
146
147 /** {@inheritDoc} */
148 @Override
149 public void run() {
150 if (ClusterHostUtil.getClusterOptions().isDeviceMonitorDisabled()) {
151 return;
152 }
153 mDispatcher = getEventDispatcher();
154 mDispatcher.start();
155 }
156
157 /** {@inheritDoc} */
158 @Override
159 public void stop() {
160 if (mDispatcher != null && mDispatcher.isAlive()) {
161 mDispatcher.cancel();
162 }
163 }
164
165 /** {@inheritDoc} */
166 @Override
167 public void setDeviceLister(DeviceLister lister) {
168 if (lister == null) {
169 throw new NullPointerException();
170 }
171 mDeviceLister = lister;
172 }
173
174 /** {@inheritDoc} */
175 @Override
176 public void notifyDeviceStateChange(
177 String serial, DeviceAllocationState oldState, DeviceAllocationState newState) {
178 // Nothing happens. We only take snapshots. Maybe we can add state change in the future.
179 }
180
181 @VisibleForTesting
182 EventDispatcher getEventDispatcher() {
183 if (mDispatcher == null) {
184 mDispatcher = new EventDispatcher();
185 }
186 return mDispatcher;
187 }
188
189 @VisibleForTesting
190 List<DeviceDescriptor> listDevices() {
191 return mDeviceLister.listDevices();
192 }
193
194 @VisibleForTesting
195 IRunUtil getRunUtil() {
196 return RunUtil.getDefault();
197 }
198
199 /** A helper method to tokenize the host info commands. */
200 void tokenizeCommands() {
201 if (mTokenizedHostInfoCmds != null && !mTokenizedHostInfoCmds.isEmpty()) {
202 // Commands already tokenized and cached. No need to tokenize again.
203 return;
204 }
205
206 mTokenizedHostInfoCmds = new HashMap<String, String[]>(mHostInfoCmds.size());
207 // Tokenize the commands and cache the result
208 for (Map.Entry<String, String> entry : mHostInfoCmds.entrySet()) {
209 final String key = entry.getKey();
210 final String cmd = entry.getValue();
211
212 CLog.d("Tokenized key %s command: %s", key, cmd);
213 mTokenizedHostInfoCmds.put(key, QuotationAwareTokenizer.tokenizeLine(cmd));
214 }
215 }
216
217 /** Queries additional host info from host-info-cmd options in TF configs. */
218 Map<String, String> getAdditionalHostInfo() {
219 final Map<String, String> info = new HashMap<>();
220 this.tokenizeCommands();
221
222 for (Map.Entry<String, String[]> entry : mTokenizedHostInfoCmds.entrySet()) {
223 final String key = entry.getKey();
224 final String[] cmd = entry.getValue();
225 final String cmdString = mHostInfoCmds.get(key);
226
227 final CommandResult result = getRunUtil().runTimedCmdSilently(mHostInfoCmdTimeout, cmd);
228
229 CLog.d("Command %s result: %s", cmdString, result.getStatus().toString());
230
231 if (result.getStatus() == CommandStatus.SUCCESS) {
232 info.put(key, result.getStdout());
233 } else {
234 info.put(key, result.getStderr());
235 }
236 }
237
238 return info;
239 }
240}