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