• 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 <hilog/log.h>
20 #include <mutex>
21 #include <securec.h>
22 #include <set>
23 #include <string_view>
24 #include "common_utilities_hpp.h"
25 #include "frontend_api_handler.h"
26 #include "ui_action.h"
27 #include "ui_driver.h"
28 #include "ui_record.h"
29 #include "screen_copy.h"
30 #include "extension_executor.h"
31 
32 namespace OHOS::uitest {
33     using namespace std;
34     // uitest_extension port definitions =================================>>>>>>>>
35     extern "C" {
36         typedef int32_t RetCode;
37 #define RETCODE_SUCCESS 0
38 #define RETCODE_FAIL (-1)
39 
40         struct Text {
41             const char *data;
42             size_t size;
43         };
44 
45         struct ReceiveBuffer {
46             uint8_t *data;
47             size_t capacity;
48             size_t *size;
49         };
50 
51         // enum LogLevel : int32_t { DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3 };
52 
53         typedef void (*DataCallback)(Text bytes);
54 
55         struct LowLevelFunctions {
56             RetCode (*callThroughMessage)(Text in, ReceiveBuffer out, bool *fatalError);
57             RetCode (*setCallbackMessageHandler)(DataCallback handler);
58             RetCode (*atomicTouch)(int32_t stage, int32_t px, int32_t py);
59             RetCode (*startCapture)(Text name, DataCallback callback, Text optJson);
60             RetCode (*stopCapture)(Text name);
61         };
62 
63         struct UiTestPort {
64             RetCode (*getUiTestVersion)(ReceiveBuffer out);
65             RetCode (*printLog)(int32_t level, Text tag, Text format, va_list ap);
66             RetCode (*getAndClearLastError)(int32_t *codeOut, ReceiveBuffer msgOut);
67             RetCode (*initLowLevelFunctions)(LowLevelFunctions *out);
68         };
69 
70         // hook function names of UiTestExtension library
71 #define UITEST_EXTENSION_CALLBACK_ONINIT "UiTestExtension_OnInit"
72 #define UITEST_EXTENSION_CALLBACK_ONRUN "UiTestExtension_OnRun"
73         // proto of UiTestExtensionOnInitCallback: void (UiTestPort port, size_t argc, const char **argv)
74         typedef RetCode (*UiTestExtensionOnInitCallback)(UiTestPort, size_t, const char **);
75         // proto of UiTestExtensionOnRun
76         typedef RetCode (*UiTestExtensionOnRunCallback)();
77     };
78     // uitest_extension port definitions =================================<<<<<<<<
79 
80     static constexpr auto ERR_BAD_ARG = ErrCode::ERR_INVALID_INPUT;
81     static constexpr size_t LOG_BUF_SIZE = 512;
82     static constexpr LogType type = LogType::LOG_APP;
83     static string_view g_version = "";
84     static string g_lastErrorMessage = "";
85     static int32_t g_lastErrorCode = 0;
86     static mutex g_callThroughLock;
87     static mutex g_captureLock;
88     static mutex g_recordRunningLock;
89     static set<string> g_runningCaptures;
90 
91 #define EXTENSION_API_CHECK(cond, errorMessage, errorCode) \
92 do { \
93     if (!(cond)) { \
94         g_lastErrorMessage = (errorMessage); \
95         g_lastErrorCode = static_cast<int32_t>(errorCode); \
96         LOG_E("EXTENSION_API_CHECK failed: %{public}s", g_lastErrorMessage.c_str()); \
97         return RETCODE_FAIL; \
98     } \
99 } while (0)
100 
WriteToBuffer(ReceiveBuffer & buffer,string_view data)101     static RetCode WriteToBuffer(ReceiveBuffer &buffer, string_view data)
102     {
103         EXTENSION_API_CHECK(buffer.data != nullptr, "Illegal buffer pointer", ERR_BAD_ARG);
104         EXTENSION_API_CHECK(buffer.size != nullptr, "Illegal outLen pointer", ERR_BAD_ARG);
105         EXTENSION_API_CHECK(buffer.capacity > data.length(), "Char buffer capacity is not enough", ERR_BAD_ARG);
106         memcpy_s(buffer.data, buffer.capacity, data.data(), data.length());
107         *(buffer.size) = data.length();
108         buffer.data[*(buffer.size)] = 0;
109         return RETCODE_SUCCESS;
110     }
111 
GetUiTestVersion(ReceiveBuffer buffer)112     static RetCode GetUiTestVersion(ReceiveBuffer buffer)
113     {
114         return WriteToBuffer(buffer, g_version);
115     }
116 
PrintLog(int32_t level,Text label,Text format,va_list ap)117     static RetCode PrintLog(int32_t level, Text label, Text format, va_list ap)
118     {
119         constexpr uint32_t domain = 0xD003100;
120         EXTENSION_API_CHECK(level >= LogRank::DEBUG && level <= LogRank::ERROR, "Illegal log level", ERR_BAD_ARG);
121         EXTENSION_API_CHECK(label.data != nullptr && format.data != nullptr, "Illegal log tag/format", ERR_BAD_ARG);
122         char buf[LOG_BUF_SIZE];
123         EXTENSION_API_CHECK(vsprintf_s(buf, sizeof(buf), format.data, ap) >= 0, format.data, ERR_BAD_ARG);
124         HiLogPrint(type, static_cast<LogLevel>(level), domain, label.data, "%{public}s", buf);
125         return RETCODE_SUCCESS;
126     }
127 
GetAndClearLastError(int32_t * codeOut,ReceiveBuffer msgOut)128     static RetCode GetAndClearLastError(int32_t *codeOut, ReceiveBuffer msgOut)
129     {
130         *codeOut = g_lastErrorCode;
131         auto ret = WriteToBuffer(msgOut, g_lastErrorMessage);
132         // clear error
133         g_lastErrorCode = ErrCode::NO_ERROR;
134         g_lastErrorMessage = "";
135         return ret;
136     }
137 
138 // input-errors of call-through api should also be passed through
139 #define CALL_THROUGH_CHECK(cond, message, code, asFatalError, fatalPtr) \
140 do { \
141     if (!(cond)) { \
142         LOG_E("Check condition (%{public}s) failed: %{public}s", #cond, string(message).c_str()); \
143         json errorJson; \
144         errorJson["code"] = (code); \
145         errorJson["message"] = (message); \
146         json replyJson; \
147         replyJson["exception"] = move(errorJson); \
148         WriteToBuffer(out, replyJson.dump()); \
149         if ((asFatalError) && (fatalPtr) != nullptr) { \
150             *(fatalPtr) = true; \
151         } \
152         return RETCODE_FAIL; \
153     } \
154 } while (0)
155 
CallThroughMessage(Text in,ReceiveBuffer out,bool * fatalError)156     static RetCode CallThroughMessage(Text in, ReceiveBuffer out, bool *fatalError)
157     {
158         auto ptr = fatalError;
159         CALL_THROUGH_CHECK(g_callThroughLock.try_lock(), "Disallow concurrent use", ERR_API_USAGE, false, ptr);
160         unique_lock<mutex> guard(g_callThroughLock, std::adopt_lock);
161         using namespace nlohmann;
162         using VT = nlohmann::detail::value_t;
163         static auto server = FrontendApiServer::Get();
164         *fatalError = false;
165         CALL_THROUGH_CHECK(in.data != nullptr, "Null message", ERR_BAD_ARG, true, ptr);
166         CALL_THROUGH_CHECK(out.data != nullptr, "Null output buffer", ERR_BAD_ARG, true, ptr);
167         CALL_THROUGH_CHECK(out.size != nullptr, "Null output size pointer", ERR_BAD_ARG, true, ptr);
168         CALL_THROUGH_CHECK(fatalError != nullptr, "Null fatalError output pointer", ERR_BAD_ARG, true, ptr);
169         auto message = json::parse(in.data, nullptr, false);
170         CALL_THROUGH_CHECK(!message.is_discarded(), "Illegal messsage, parse json failed", ERR_BAD_ARG, true, ptr);
171         auto hasProps = message.contains("api") && message.contains("this") && message.contains("args");
172         CALL_THROUGH_CHECK(hasProps, "Illegal messsage, api/this/args property missing", ERR_BAD_ARG, true, ptr);
173         auto api = message["api"];
174         auto caller = message["this"];
175         auto params = message["args"];
176         auto nullThis = caller.type() == VT::null;
177         CALL_THROUGH_CHECK(api.type() == VT::string, "Illegal api value type", ERR_BAD_ARG, true, ptr);
178         CALL_THROUGH_CHECK(caller.type() == VT::string || nullThis, "Illegal thisRef type", ERR_BAD_ARG, true, ptr);
179         CALL_THROUGH_CHECK(params.type() == VT::array, "Illegal api args type", ERR_BAD_ARG, true, ptr);
180         auto call = ApiCallInfo {
181             .apiId_ = api.get<string>(),
182             .callerObjRef_ = nullThis ? "" : caller.get<string>(),
183             .paramList_ = move(params)
184         };
185         auto reply = ApiReplyInfo();
186         server.Call(call, reply);
187         const auto errCode = reply.exception_.code_;
188         const auto isFatalErr = errCode == ErrCode::INTERNAL_ERROR || errCode == ErrCode::ERR_INTERNAL;
189         CALL_THROUGH_CHECK(errCode == ErrCode::NO_ERROR, reply.exception_.message_.c_str(), errCode, isFatalErr, ptr);
190         json result;
191         result["result"] = move(reply.resultValue_);
192         return WriteToBuffer(out, result.dump());
193     }
194 
SetCallbackMessageHandler(DataCallback handler)195     static RetCode SetCallbackMessageHandler(DataCallback handler)
196     {
197         EXTENSION_API_CHECK(handler != nullptr, "Null callback handler!", ERR_BAD_ARG);
198         FrontendApiServer::Get().SetCallbackHandler([handler](const ApiCallInfo& in, ApiReplyInfo& out) {
199             nlohmann::json msgJson;
200             msgJson["api"] = in.apiId_;
201             msgJson["this"] = in.callerObjRef_;
202             msgJson["args"] = in.paramList_;
203             auto msg = msgJson.dump();
204             handler(Text {msg.data(), msg.length()});
205         });
206         return RETCODE_SUCCESS;
207     }
208 
AtomicTouch(int32_t stage,int32_t px,int32_t py)209     static RetCode AtomicTouch(int32_t stage, int32_t px, int32_t py)
210     {
211         static auto driver = UiDriver();
212         EXTENSION_API_CHECK(stage >= ActionStage::DOWN && stage <= ActionStage::UP, "Illegal stage", ERR_BAD_ARG);
213         auto touch = GenericAtomicAction(static_cast<ActionStage>(stage), Point(px, py));
214         auto err = ApiCallErr(NO_ERROR);
215         UiOpArgs uiOpArgs;
216         driver.PerformTouch(touch, uiOpArgs, err);
217         EXTENSION_API_CHECK(err.code_ == NO_ERROR, err.message_, err.code_);
218         return RETCODE_SUCCESS;
219     }
220 
StopCapture(Text name)221     static RetCode StopCapture(Text name)
222     {
223         EXTENSION_API_CHECK(name.data != nullptr, "Illegal name/callback", ERR_BAD_ARG);
224         unique_lock<mutex> guard(g_captureLock);
225         if (g_runningCaptures.find(name.data) == g_runningCaptures.end()) {
226             return RETCODE_SUCCESS;
227         }
228         if (strcmp(name.data, "copyScreen") == 0) {
229             StopScreenCopy();
230         } else if (strcmp(name.data, "recordUiAction") == 0) {
231             UiDriverRecordStop();
232             g_recordRunningLock.lock(); // this cause waiting for recordThread exit
233             g_recordRunningLock.unlock();
234         } else if (strcmp(name.data, "dumpLayout") != 0) {
235             EXTENSION_API_CHECK(false, string("Illegal capture type: ") + name.data, ERR_BAD_ARG);
236         }
237         g_runningCaptures.erase(name.data);
238         return RETCODE_SUCCESS;
239     }
240 
StartCapture(Text name,DataCallback callback,Text optJson)241     static RetCode StartCapture(Text name, DataCallback callback, Text optJson)
242     {
243         static auto driver = UiDriver();
244         EXTENSION_API_CHECK(name.data != nullptr && callback != nullptr, "Illegal name/callback", ERR_BAD_ARG);
245         nlohmann::json options = nullptr;
246         if (optJson.data != nullptr) {
247             options = nlohmann::json::parse(optJson.data, nullptr, false);
248             EXTENSION_API_CHECK(!options.is_discarded(), "Illegal optJson format", ERR_BAD_ARG);
249         }
250         unique_lock<mutex> guard(g_captureLock);
251         const auto running = g_runningCaptures.find(name.data) != g_runningCaptures.end();
252         EXTENSION_API_CHECK(!running, string("Capture already running: ") + name.data, -1);
253         g_runningCaptures.insert(name.data);
254         guard.unlock();
255         if (strcmp(name.data, "dumpLayout") == 0) {
256             nlohmann::json tree;
257             ApiCallErr err(NO_ERROR);
258             driver.DumpUiHierarchy(tree, false, false, err);
259             g_runningCaptures.erase("dumpLayout"); // dumpLayout is sync&once
260             EXTENSION_API_CHECK(err.code_ == NO_ERROR, err.message_, err.code_);
261             auto layout = tree.dump(-1, ' ', false, nlohmann::detail::error_handler_t::replace);
262             callback(Text{layout.c_str(), layout.length()});
263         } else if (strcmp(name.data, "copyScreen") == 0) {
264             float scale = 1.0f;
265             if (options.type() == nlohmann::detail::value_t::object && options.contains("scale")) {
266                 nlohmann::json val = options["scale"];
267                 EXTENSION_API_CHECK(val.type() == detail::value_t::number_float, "Illegal scale value", ERR_BAD_ARG);
268                 scale = val.get<float>();
269             }
270             StartScreenCopy(scale, [callback](uint8_t *data, size_t len) {
271                 callback(Text{reinterpret_cast<const char *>(data), len});
272                 free(data);
273             });
274         } else if (strcmp(name.data, "recordUiAction") == 0) {
275             StopCapture(name);
276             auto recordThread = thread([callback]() {
277                 g_recordRunningLock.lock();
278                 UiDriverRecordStart([callback](nlohmann::json record) {
279                     auto data = record.dump(-1, ' ', false, nlohmann::detail::error_handler_t::replace);
280                     callback(Text{data.c_str(), data.length()});
281                     }, "");
282                 g_recordRunningLock.unlock();
283             });
284             recordThread.detach();
285         } else {
286             EXTENSION_API_CHECK(false, string("Illegal capture type: ") + name.data, ERR_BAD_ARG);
287         }
288         return RETCODE_SUCCESS;
289     }
290 
InitLowLevelFunctions(LowLevelFunctions * out)291     static RetCode InitLowLevelFunctions(LowLevelFunctions *out)
292     {
293         EXTENSION_API_CHECK(out != nullptr, "Null LowLevelFunctions recveive pointer", ERR_BAD_ARG);
294         out->callThroughMessage = CallThroughMessage;
295         out->setCallbackMessageHandler = SetCallbackMessageHandler;
296         out->atomicTouch = AtomicTouch;
297         out->startCapture = StartCapture;
298         out->stopCapture = StopCapture;
299         return RETCODE_SUCCESS;
300     }
301 
ExecuteExtension(string_view version)302     bool ExecuteExtension(string_view version)
303     {
304         g_version = version;
305         constexpr string_view extensionPath = "/data/local/tmp/agent.so";
306         if (!OHOS::FileExists(extensionPath.data())) {
307             LOG_E("Client nativeCode not exist");
308             return false;
309         }
310         auto handle = dlopen(extensionPath.data(), RTLD_LAZY);
311         if (handle == nullptr) {
312             LOG_E("Dlopen %{public}s failed: %{public}s", extensionPath.data(), strerror(errno));
313             return false;
314         }
315         auto symInit = dlsym(handle, UITEST_EXTENSION_CALLBACK_ONINIT);
316         if (symInit == nullptr) {
317             LOG_E("Dlsym failed:" UITEST_EXTENSION_CALLBACK_ONINIT);
318             dlclose(handle);
319             return false;
320         }
321         auto symRun = dlsym(handle, UITEST_EXTENSION_CALLBACK_ONRUN);
322         if (symRun == nullptr) {
323             LOG_E("Dlsym failed: " UITEST_EXTENSION_CALLBACK_ONRUN);
324             dlclose(handle);
325             return false;
326         }
327         auto initFunction = reinterpret_cast<UiTestExtensionOnInitCallback>(symInit);
328         auto runFunction = reinterpret_cast<UiTestExtensionOnRunCallback>(symRun);
329         auto port = UiTestPort {
330             .getUiTestVersion = GetUiTestVersion,
331             .printLog = PrintLog,
332             .getAndClearLastError = GetAndClearLastError,
333             .initLowLevelFunctions = InitLowLevelFunctions,
334         };
335         if (initFunction(port, 0, nullptr) != RETCODE_SUCCESS) {
336             LOG_I("Initialize UiTest extension failed");
337             dlclose(handle);
338             return false;
339         }
340         auto ret = runFunction() == RETCODE_SUCCESS;
341         dlclose(handle);
342         return ret;
343     }
344 } // namespace OHOS::uitest
345