• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2023 Huawei Device Co., Ltd.
3  * Licensed under the Apache License, Version 2.0 (the "License");
4  * you may not use this file except in compliance with the License.
5  * You may obtain a copy of the License at
6  *
7  *     http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14  */
15 
16 #include <csignal>
17 #include <dlfcn.h>
18 #include <file_ex.h>
19 #include <mutex>
20 #include <securec.h>
21 #include <set>
22 #include <string_view>
23 #include "common_utilities_hpp.h"
24 #include "frontend_api_handler.h"
25 #include "ui_action.h"
26 #include "ui_driver.h"
27 #include "ui_record.h"
28 #include "screen_copy.h"
29 #include "extension_c_api.h"
30 #include "extension_executor.h"
31 
32 namespace OHOS::uitest {
33     using namespace std;
34     static constexpr auto ERR_BAD_ARG = ErrCode::ERR_INVALID_INPUT;
35     static constexpr size_t LOG_BUF_SIZE = 512;
36     static constexpr LogType type = LogType::LOG_APP;
37     static string_view g_version = "";
38     static string g_lastErrorMessage = "";
39     static int32_t g_lastErrorCode = 0;
40     static mutex g_callThroughLock;
41     static mutex g_captureLock;
42     static mutex g_recordRunningLock;
43     static set<string> g_runningCaptures;
44 
45 #define EXTENSION_API_CHECK(cond, errorMessage, errorCode) \
46 do { \
47     if (!(cond)) { \
48         g_lastErrorMessage = (errorMessage); \
49         g_lastErrorCode = static_cast<int32_t>(errorCode); \
50         LOG_E("EXTENSION_API_CHECK failed: %{public}s", g_lastErrorMessage.c_str()); \
51         return RETCODE_FAIL; \
52     } \
53 } while (0)
54 
WriteToBuffer(ReceiveBuffer & buffer,string_view data)55     static RetCode WriteToBuffer(ReceiveBuffer &buffer, string_view data)
56     {
57         EXTENSION_API_CHECK(buffer.data != nullptr, "Illegal buffer pointer", ERR_BAD_ARG);
58         EXTENSION_API_CHECK(buffer.size != nullptr, "Illegal outLen pointer", ERR_BAD_ARG);
59         EXTENSION_API_CHECK(buffer.capacity > data.length(), "Char buffer capacity is not enough", ERR_BAD_ARG);
60         memcpy_s(buffer.data, buffer.capacity, data.data(), data.length());
61         *(buffer.size) = data.length();
62         buffer.data[*(buffer.size)] = 0;
63         return RETCODE_SUCCESS;
64     }
65 
GetUiTestVersion(ReceiveBuffer buffer)66     static RetCode GetUiTestVersion(ReceiveBuffer buffer)
67     {
68         return WriteToBuffer(buffer, g_version);
69     }
70 
PrintLog(int32_t level,Text label,Text format,va_list ap)71     static RetCode PrintLog(int32_t level, Text label, Text format, va_list ap)
72     {
73         EXTENSION_API_CHECK(level >= LogRank::DEBUG && level <= LogRank::ERROR, "Illegal log level", ERR_BAD_ARG);
74         EXTENSION_API_CHECK(label.data != nullptr && format.data != nullptr, "Illegal log tag/format", ERR_BAD_ARG);
75         char buf[LOG_BUF_SIZE];
76         EXTENSION_API_CHECK(vsprintf_s(buf, sizeof(buf), format.data, ap) >= 0, format.data, ERR_BAD_ARG);
77         if (level == LogRank::DEBUG) {
78             HILOG_DEBUG(type, "%{public}s", buf);
79         } else if (level == LogRank::INFO) {
80             HILOG_INFO(type, "%{public}s", buf);
81         } else if (level == LogRank::WARN) {
82             HILOG_WARN(type, "%{public}s", buf);
83         } else if (level == LogRank::ERROR) {
84             HILOG_ERROR(type, "%{public}s", buf);
85         }
86         return RETCODE_SUCCESS;
87     }
88 
GetAndClearLastError(int32_t * codeOut,ReceiveBuffer msgOut)89     static RetCode GetAndClearLastError(int32_t *codeOut, ReceiveBuffer msgOut)
90     {
91         if (codeOut == nullptr) {
92             LOG_E("Code receiver is nullptr, cannot write error");
93             return RETCODE_FAIL;
94         }
95         *codeOut = g_lastErrorCode;
96         auto ret = WriteToBuffer(msgOut, g_lastErrorMessage);
97         // clear error
98         g_lastErrorCode = ErrCode::NO_ERROR;
99         g_lastErrorMessage = "";
100         return ret;
101     }
102 
103 // input-errors of call-through api should also be passed through
104 #define CALL_THROUGH_CHECK(cond, message, code, asFatalError, fatalPtr) \
105 do { \
106     if (!(cond)) { \
107         LOG_E("Check condition (%{public}s) failed: %{public}s", #cond, string(message).c_str()); \
108         json errorJson; \
109         errorJson["code"] = (code); \
110         errorJson["message"] = (message); \
111         json replyJson; \
112         replyJson["exception"] = move(errorJson); \
113         WriteToBuffer(out, replyJson.dump()); \
114         if ((asFatalError) && (fatalPtr) != nullptr) { \
115             *(fatalPtr) = true; \
116         } \
117         return RETCODE_FAIL; \
118     } \
119 } while (0)
120 
CallThroughMessage(Text in,ReceiveBuffer out,bool * fatalError)121     static RetCode CallThroughMessage(Text in, ReceiveBuffer out, bool *fatalError)
122     {
123         auto ptr = fatalError;
124         CALL_THROUGH_CHECK(g_callThroughLock.try_lock(), "Disallow concurrent use", ERR_API_USAGE, false, ptr);
125         unique_lock<mutex> guard(g_callThroughLock, std::adopt_lock);
126         using namespace nlohmann;
127         using VT = nlohmann::detail::value_t;
128         static auto server = FrontendApiServer::Get();
129         CALL_THROUGH_CHECK(in.data != nullptr, "Null message", ERR_BAD_ARG, true, ptr);
130         CALL_THROUGH_CHECK(out.data != nullptr, "Null output buffer", ERR_BAD_ARG, true, ptr);
131         CALL_THROUGH_CHECK(out.size != nullptr, "Null output size pointer", ERR_BAD_ARG, true, ptr);
132         CALL_THROUGH_CHECK(fatalError != nullptr, "Null fatalError output pointer", ERR_BAD_ARG, true, ptr);
133         *fatalError = false;
134         auto message = json::parse(in.data, nullptr, false);
135         CALL_THROUGH_CHECK(!message.is_discarded(), "Illegal messsage, parse json failed", ERR_BAD_ARG, true, ptr);
136         auto hasProps = message.contains("api") && message.contains("this") && message.contains("args");
137         CALL_THROUGH_CHECK(hasProps, "Illegal messsage, api/this/args property missing", ERR_BAD_ARG, true, ptr);
138         auto api = message["api"];
139         auto caller = message["this"];
140         auto params = message["args"];
141         auto nullThis = caller.type() == VT::null;
142         CALL_THROUGH_CHECK(api.type() == VT::string, "Illegal api value type", ERR_BAD_ARG, true, ptr);
143         CALL_THROUGH_CHECK(caller.type() == VT::string || nullThis, "Illegal thisRef type", ERR_BAD_ARG, true, ptr);
144         CALL_THROUGH_CHECK(params.type() == VT::array, "Illegal api args type", ERR_BAD_ARG, true, ptr);
145         auto call = ApiCallInfo {
146             .apiId_ = api.get<string>(),
147             .callerObjRef_ = nullThis ? "" : caller.get<string>(),
148             .paramList_ = move(params)
149         };
150         auto reply = ApiReplyInfo();
151         server.Call(call, reply);
152         const auto errCode = reply.exception_.code_;
153         const auto isFatalErr = errCode == ErrCode::INTERNAL_ERROR || errCode == ErrCode::ERR_INTERNAL;
154         CALL_THROUGH_CHECK(errCode == ErrCode::NO_ERROR, reply.exception_.message_.c_str(), errCode, isFatalErr, ptr);
155         json result;
156         result["result"] = move(reply.resultValue_);
157         return WriteToBuffer(out, result.dump());
158     }
159 
SetCallbackMessageHandler(DataCallback handler)160     static RetCode SetCallbackMessageHandler(DataCallback handler)
161     {
162         EXTENSION_API_CHECK(handler != nullptr, "Null callback handler!", ERR_BAD_ARG);
163         FrontendApiServer::Get().SetCallbackHandler([handler](const ApiCallInfo& in, ApiReplyInfo& out) {
164             nlohmann::json msgJson;
165             msgJson["api"] = in.apiId_;
166             msgJson["this"] = in.callerObjRef_;
167             msgJson["args"] = in.paramList_;
168             auto msg = msgJson.dump();
169             handler(Text {msg.data(), msg.length()});
170         });
171         return RETCODE_SUCCESS;
172     }
173 
AtomicMouseAction(int32_t stage,int32_t px,int32_t py,int32_t btn)174     static RetCode AtomicMouseAction(int32_t stage, int32_t px, int32_t py, int32_t btn)
175     {
176         static auto driver = UiDriver();
177         EXTENSION_API_CHECK(stage >= ActionStage::DOWN && stage <= ActionStage::AXIS_STOP,
178                             "Illegal stage", ERR_BAD_ARG);
179         EXTENSION_API_CHECK(btn >= MouseButton::BUTTON_NONE && btn <= MouseButton::BUTTON_MIDDLE,
180                             "Illegal btn", ERR_BAD_ARG);
181         auto touch = GenericAtomicMouseAction(static_cast<ActionStage>(stage), Point(px, py),
182                                               static_cast<MouseButton>(btn));
183         auto err = ApiCallErr(NO_ERROR);
184         UiOpArgs uiOpArgs;
185         driver.PerformMouseAction(touch, uiOpArgs, err);
186         EXTENSION_API_CHECK(err.code_ == NO_ERROR, err.message_, err.code_);
187         return RETCODE_SUCCESS;
188     }
189 
AtomicTouch(int32_t stage,int32_t px,int32_t py)190     static RetCode AtomicTouch(int32_t stage, int32_t px, int32_t py)
191     {
192         static auto driver = UiDriver();
193         EXTENSION_API_CHECK(stage >= ActionStage::DOWN && stage <= ActionStage::UP, "Illegal stage", ERR_BAD_ARG);
194         auto touch = GenericAtomicAction(static_cast<ActionStage>(stage), Point(px, py));
195         auto err = ApiCallErr(NO_ERROR);
196         UiOpArgs uiOpArgs;
197         driver.PerformTouch(touch, uiOpArgs, err);
198         EXTENSION_API_CHECK(err.code_ == NO_ERROR, err.message_, err.code_);
199         return RETCODE_SUCCESS;
200     }
201 
StopCapture(Text name)202     static RetCode StopCapture(Text name)
203     {
204         EXTENSION_API_CHECK(name.data != nullptr, "Illegal name/callback", ERR_BAD_ARG);
205         unique_lock<mutex> guard(g_captureLock);
206         if (g_runningCaptures.find(name.data) == g_runningCaptures.end()) {
207             return RETCODE_SUCCESS;
208         }
209         if (strcmp(name.data, "copyScreen") == 0) {
210             StopScreenCopy();
211         } else if (strcmp(name.data, "recordUiAction") == 0) {
212             UiDriverRecordStop();
213             g_recordRunningLock.lock(); // this cause waiting for recordThread exit
214             g_recordRunningLock.unlock();
215         } else if (strcmp(name.data, "dumpLayout") != 0) {
216             EXTENSION_API_CHECK(false, string("Illegal capture type: ") + name.data, ERR_BAD_ARG);
217         }
218         g_runningCaptures.erase(name.data);
219         return RETCODE_SUCCESS;
220     }
221 
GetDumpInfo(nlohmann::json & options,nlohmann::json & tree,ApiCallErr & err)222     static RetCode GetDumpInfo(nlohmann::json &options, nlohmann::json &tree, ApiCallErr &err)
223     {
224         static auto driver = UiDriver();
225         DumpOption dumpOption;
226         if (options.type() == nlohmann::detail::value_t::object && options.contains("bundleName")) {
227             nlohmann::json val = options["bundleName"];
228             EXTENSION_API_CHECK(val.type() == detail::value_t::string, "Illegal bundleName value", ERR_BAD_ARG);
229             dumpOption.bundleName_ = val.get<string>();
230         }
231         if (options.type() == nlohmann::detail::value_t::object && options.contains("windowId")) {
232             nlohmann::json val = options["windowId"];
233             EXTENSION_API_CHECK(val.type() == detail::value_t::string, "Illegal windowId value", ERR_BAD_ARG);
234             auto windowId = val.get<string>();
235             EXTENSION_API_CHECK(atoi(windowId.c_str()) != 0, "Illegal windowId value", ERR_BAD_ARG);
236             dumpOption.windowId_ = windowId;
237         }
238         if (options.type() == nlohmann::detail::value_t::object && options.contains("mergeWindow")) {
239             nlohmann::json val = options["mergeWindow"];
240             EXTENSION_API_CHECK(val.type() == detail::value_t::string, "Illegal mergeWindow value", ERR_BAD_ARG);
241             auto mergeWindow = val.get<string>();
242             if (mergeWindow == "false") {
243                 dumpOption.notMergeWindow_ = true;
244             }
245         }
246         if (options.type() == nlohmann::detail::value_t::object && options.contains("displayId")) {
247             nlohmann::json val = options["displayId"];
248             EXTENSION_API_CHECK(val.type() == detail::value_t::number_integer, "Illegal displayId value", ERR_BAD_ARG);
249             dumpOption.displayId_ = val.get<int>();
250         }
251         driver.DumpUiHierarchy(tree, dumpOption, err);
252         return RETCODE_SUCCESS;
253     }
254 
StartCapture(Text name,DataCallback callback,Text optJson)255     static RetCode StartCapture(Text name, DataCallback callback, Text optJson)
256     {
257         static auto driver = UiDriver();
258         EXTENSION_API_CHECK(name.data != nullptr && callback != nullptr, "Illegal name/callback", ERR_BAD_ARG);
259         nlohmann::json options = nullptr;
260         if (optJson.data != nullptr) {
261             options = nlohmann::json::parse(optJson.data, nullptr, false);
262             EXTENSION_API_CHECK(!options.is_discarded(), "Illegal optJson format", ERR_BAD_ARG);
263         }
264         unique_lock<mutex> guard(g_captureLock);
265         const auto running = g_runningCaptures.find(name.data) != g_runningCaptures.end();
266         EXTENSION_API_CHECK(!running, string("Capture already running: ") + name.data, -1);
267         g_runningCaptures.insert(name.data);
268         guard.unlock();
269         if (strcmp(name.data, "dumpLayout") == 0) {
270             nlohmann::json tree;
271             ApiCallErr err(NO_ERROR);
272             EXTENSION_API_CHECK(GetDumpInfo(options, tree, err) == RETCODE_SUCCESS, "Illegal options", ERR_BAD_ARG);
273             g_runningCaptures.erase("dumpLayout"); // dumpLayout is sync&once
274             EXTENSION_API_CHECK(err.code_ == NO_ERROR, err.message_, err.code_);
275             auto layout = tree.dump(-1, ' ', false, nlohmann::detail::error_handler_t::replace);
276             callback(Text{layout.c_str(), layout.length()});
277         } else if (strcmp(name.data, "copyScreen") == 0) {
278             float scale = ReadArgFromJson<float>(options, "scale", 1.0f);
279             int32_t displayId = ReadArgFromJson<int32_t>(options, "displayId", UNASSIGNED);
280             StartScreenCopy(scale, displayId, [callback](uint8_t *data, size_t len) {
281                 callback(Text{reinterpret_cast<const char *>(data), len});
282                 free(data);
283             });
284         } else if (strcmp(name.data, "recordUiAction") == 0) {
285             UiDriverRecordStop();
286             g_recordRunningLock.lock(); // wait for running thread terminates
287             g_recordRunningLock.unlock();
288             auto recordThread = thread([callback]() {
289                 g_recordRunningLock.lock();
290                 UiDriverRecordStart([callback](nlohmann::json record) {
291                     auto data = record.dump(-1, ' ', false, nlohmann::detail::error_handler_t::replace);
292                     callback(Text{data.c_str(), data.length()});
293                     }, "");
294                 g_recordRunningLock.unlock();
295             });
296             recordThread.detach();
297         } else {
298             EXTENSION_API_CHECK(false, string("Illegal capture type: ") + name.data, ERR_BAD_ARG);
299         }
300         return RETCODE_SUCCESS;
301     }
302 
InitLowLevelFunctions(LowLevelFunctions * out)303     static RetCode InitLowLevelFunctions(LowLevelFunctions *out)
304     {
305         EXTENSION_API_CHECK(out != nullptr, "Null LowLevelFunctions recveive pointer", ERR_BAD_ARG);
306         out->callThroughMessage = CallThroughMessage;
307         out->setCallbackMessageHandler = SetCallbackMessageHandler;
308         out->atomicTouch = AtomicTouch;
309         out->startCapture = StartCapture;
310         out->stopCapture = StopCapture;
311         out->atomicMouseAction = AtomicMouseAction;
312         return RETCODE_SUCCESS;
313     }
314 
ExecuteExtension(string_view version,int32_t argc,char * argv[])315     bool ExecuteExtension(string_view version, int32_t argc, char *argv[])
316     {
317         int32_t used_argc = 0;
318         const char *name = "agent.so";
319         if (argc > 1 && string_view(argv[0]) == "--extension-name") {
320             name = argv[1];
321             used_argc = TWO; // argv0,1 is consumed, donot pass down
322         }
323         string extensionPath = string("/data/local/tmp/") + name;
324         g_version = version;
325         if (!OHOS::FileExists(extensionPath.data())) {
326             LOG_E("Client nativeCode not exist");
327             return false;
328         }
329         auto handle = dlopen(extensionPath.data(), RTLD_LAZY);
330         if (handle == nullptr) {
331             LOG_E("Dlopen %{public}s failed: %{public}s", extensionPath.data(), dlerror());
332             return false;
333         }
334         auto symInit = dlsym(handle, UITEST_EXTENSION_CALLBACK_ONINIT);
335         if (symInit == nullptr) {
336             LOG_E("Dlsym failed:" UITEST_EXTENSION_CALLBACK_ONINIT);
337             dlclose(handle);
338             return false;
339         }
340         auto symRun = dlsym(handle, UITEST_EXTENSION_CALLBACK_ONRUN);
341         if (symRun == nullptr) {
342             LOG_E("Dlsym failed: " UITEST_EXTENSION_CALLBACK_ONRUN);
343             dlclose(handle);
344             return false;
345         }
346         auto initFunction = reinterpret_cast<UiTestExtensionOnInitCallback>(symInit);
347         auto runFunction = reinterpret_cast<UiTestExtensionOnRunCallback>(symRun);
348         auto port = UiTestPort {
349             .getUiTestVersion = GetUiTestVersion,
350             .printLog = PrintLog,
351             .getAndClearLastError = GetAndClearLastError,
352             .initLowLevelFunctions = InitLowLevelFunctions,
353         };
354         if (initFunction(port, argc - used_argc, argv + used_argc) != RETCODE_SUCCESS) {
355             LOG_I("Initialize UiTest extension failed");
356             dlclose(handle);
357             return false;
358         }
359         auto ret = runFunction() == RETCODE_SUCCESS;
360         dlclose(handle);
361         return ret;
362     }
363 } // namespace OHOS::uitest
364