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