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 <js_runtime.h> 17 #include <event_runner.h> 18 #include <napi/native_api.h> 19 #include <napi/native_node_api.h> 20 #include <json.hpp> 21 #include <file_ex.h> 22 #include <future> 23 #include <set> 24 #include <unistd.h> 25 #include "ui_driver.h" 26 #include "common_utilities_hpp.h" 27 #include "screen_copy.h" 28 #include "ui_record.h" 29 #include "js_client_loader.h" 30 31 namespace OHOS::uitest { 32 using namespace std; 33 constexpr string_view JS_CODE_PATH = "data/local/tmp/app.abc"; 34 constexpr string_view CAPTURE_SCREEN = "copyScreen"; 35 constexpr string_view CAPTURE_LAYOUT = "dumpLayout"; 36 constexpr string_view CAPTURE_UIACTION = "recordUiAction"; 37 38 class CaptureContext { 39 public: 40 CaptureContext() = default; 41 // capture options passed from js 42 float scale = 1.0; // for screen cap 43 // capture enviroment and data buffer 44 string type; 45 napi_env napiEnv = nullptr; 46 napi_ref cbRef = nullptr; 47 uint8_t *data = nullptr; 48 size_t dataLen = 0; 49 static constexpr size_t DATA_CAPACITY = 2 * 1024 * 1024; 50 }; 51 NopNapiFinalizer(napi_env,void *,void *)52 static void NopNapiFinalizer(napi_env /**env*/, void* /**finalize_data*/, void* /**finalize_hint*/) {} 53 CallbackCaptureResultToJs(const CaptureContext & context)54 static void CallbackCaptureResultToJs(const CaptureContext& context) 55 { 56 uv_loop_s *loop = nullptr; 57 NAPI_CALL_RETURN_VOID(context.napiEnv, napi_get_uv_event_loop(context.napiEnv, &loop)); 58 auto work = new uv_work_t; 59 auto contextCopy = new CaptureContext(); 60 *contextCopy = context; // copy-assign 61 work->data = contextCopy; 62 (void)uv_queue_work(loop, work, [](uv_work_t *) {}, [](uv_work_t* work, int status) { 63 auto ctx = (CaptureContext*)work->data; 64 auto env = ctx->napiEnv; 65 napi_handle_scope scope = nullptr; 66 napi_open_handle_scope(env, &scope); 67 if (scope == nullptr) { 68 return; 69 } 70 napi_value callback = nullptr; 71 napi_get_reference_value(env, ctx->cbRef, &callback); 72 // use "napi_create_external_arraybuffer" to shared data with JS without new and copy 73 napi_value arrBuf = nullptr; 74 napi_create_external_arraybuffer(env, ctx->data, ctx->dataLen, NopNapiFinalizer, nullptr, &arrBuf); 75 napi_value undefined = nullptr; 76 napi_get_undefined(env, &undefined); 77 LOG_I("Invoke js callback"); 78 napi_value argv[1] = { arrBuf }; 79 napi_call_function(env, undefined, callback, 1, argv, &arrBuf); 80 auto errorPending = false; 81 napi_is_exception_pending(env, &errorPending); 82 if (errorPending) { 83 LOG_W("Exception raised during invoke js callback"); 84 napi_get_and_clear_last_exception(env, &arrBuf); 85 } 86 if (ctx->type == CAPTURE_SCREEN) { 87 free(ctx->data); 88 } 89 if (work != nullptr) { 90 delete work; 91 } 92 napi_close_handle_scope(env, scope); 93 delete ctx; 94 }); 95 } 96 RecordStart(CaptureContext & context,string type)97 static void RecordStart(CaptureContext& context, string type) 98 { 99 static uint8_t uiActionBuf[CaptureContext::DATA_CAPACITY] = {0}; 100 std::string modeOpt; 101 auto handler = [type, context](nlohmann::json data) { 102 const auto jsonStr = data.dump(); 103 jsonStr.copy((char *)uiActionBuf, jsonStr.length()); 104 uiActionBuf[jsonStr.length()] = 0; 105 auto ctx = context; 106 ctx.data = uiActionBuf; 107 ctx.dataLen = jsonStr.length(); 108 CallbackCaptureResultToJs(ctx); 109 }; 110 LOG_I("Start record uiaction"); 111 UiDriverRecordStart(handler, modeOpt); 112 } 113 ScreenCopyStart(CaptureContext & context,string type)114 static void ScreenCopyStart(CaptureContext& context, string type) 115 { 116 auto handler = [type, context](uint8_t * data, size_t len) { 117 auto ctx = context; 118 ctx.data = data; 119 ctx.dataLen = len; 120 CallbackCaptureResultToJs(ctx); 121 }; 122 StartScreenCopy(context.scale, handler); 123 } 124 UpdateCaptureState(CaptureContext && context,bool active)125 static void UpdateCaptureState(CaptureContext&& context, bool active) 126 { 127 static auto driver = UiDriver(); 128 static set<string> runningCaptures; 129 static mutex stateLock; 130 static uint8_t dumpLayoutBuf[CaptureContext::DATA_CAPACITY] = {0}; 131 auto &type = context.type; 132 stateLock.lock(); 133 const auto running = runningCaptures.find(type) != runningCaptures.end(); 134 if (running && active) { 135 LOG_W("Capture %{public}s already running, call stop first!", context.type.c_str()); 136 stateLock.unlock(); 137 return; 138 } 139 if (active) { 140 runningCaptures.insert(type); 141 } else { 142 runningCaptures.erase(type); 143 } 144 stateLock.unlock(); 145 if (type == CAPTURE_SCREEN && !active) { 146 StopScreenCopy(); 147 } else if (type == CAPTURE_SCREEN && active) { 148 ScreenCopyStart(context, type); 149 } else if (type == CAPTURE_LAYOUT && active) { 150 auto dom = nlohmann::json(); 151 auto err = ApiCallErr(NO_ERROR); 152 driver.DumpUiHierarchy(dom, false, err); 153 if (err.code_ == NO_ERROR) { 154 const auto jsonStr = dom.dump(); 155 jsonStr.copy((char *)dumpLayoutBuf, jsonStr.length()); 156 dumpLayoutBuf[jsonStr.length()] = 0; 157 context.data = dumpLayoutBuf; 158 context.dataLen = jsonStr.length(); 159 CallbackCaptureResultToJs(context); 160 } else { 161 LOG_W("DumpLayout failed: %{public}s", err.message_.c_str()); 162 } 163 stateLock.lock(); 164 runningCaptures.erase(type); 165 stateLock.unlock(); 166 } else if (type == CAPTURE_UIACTION && active) { 167 RecordStart(context, type); 168 } else if (type == CAPTURE_UIACTION && !active) { 169 UiDriverRecordStop(); 170 } 171 } 172 ParseCaptureOptions(napi_env env,string_view type,napi_value opt,CaptureContext & out)173 static void ParseCaptureOptions(napi_env env, string_view type, napi_value opt, CaptureContext& out) 174 { 175 constexpr size_t OPTION_MAX_LEN = 256; 176 napi_value global = nullptr; 177 napi_value jsonProp = nullptr; 178 napi_value jsonFunc = nullptr; 179 NAPI_CALL_RETURN_VOID(env, napi_get_global(env, &global)); 180 NAPI_CALL_RETURN_VOID(env, napi_get_named_property(env, global, "JSON", &jsonProp)); 181 NAPI_CALL_RETURN_VOID(env, napi_get_named_property(env, jsonProp, "stringify", &jsonFunc)); 182 napi_value jsStr = nullptr; 183 napi_value argv[1] = {opt}; 184 NAPI_CALL_RETURN_VOID(env, napi_call_function(env, jsonProp, jsonFunc, 1, argv, &jsStr)); 185 size_t len = 0; 186 char buf[OPTION_MAX_LEN] = {0}; 187 NAPI_CALL_RETURN_VOID(env, napi_get_value_string_utf8(env, jsStr, buf, OPTION_MAX_LEN, &len)); 188 auto cppStr = string(buf, len); 189 auto optJson = nlohmann::json::parse(cppStr, nullptr, false); 190 NAPI_ASSERT_RETURN_VOID(env, !optJson.is_discarded(), "Bad option json string"); 191 LOG_I("CaptureOption=%{public}s", cppStr.c_str()); 192 if (optJson.contains("scale") && optJson["scale"].type() == nlohmann::detail::value_t::number_float) { 193 out.scale = optJson["scale"].get<float>(); 194 } 195 } 196 197 // JS-function-proto: startCapture(type: string, resultCb: (data:ArrayBuffer)=>void, options?:any):void 198 // JS-function-proto: stopCapture(type: string): void 199 template<bool kOn = true> SetCaptureEventJsCallback(napi_env env,napi_callback_info info)200 static napi_value SetCaptureEventJsCallback(napi_env env, napi_callback_info info) 201 { 202 static const set<string> TYPES {string(CAPTURE_SCREEN), string(CAPTURE_LAYOUT), string(CAPTURE_UIACTION)}; 203 constexpr size_t MIN_ARGC = 1; 204 constexpr size_t MAX_ARGC = 3; 205 constexpr size_t BUF_SIZE = 32; 206 static napi_value argv[MAX_ARGC] = {nullptr}; 207 static char buf[BUF_SIZE] = { 0 }; 208 size_t argc = MAX_ARGC; 209 NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr)); 210 NAPI_ASSERT(env, argc >= MIN_ARGC, "Illegal argument count"); 211 napi_valuetype type = napi_undefined; 212 NAPI_CALL(env, napi_typeof(env, argv[0], &type)); 213 NAPI_ASSERT(env, type == napi_string, "Illegal arg[0], string required"); 214 size_t typeLen = 0; 215 NAPI_CALL(env, napi_get_value_string_utf8(env, argv[0], buf, BUF_SIZE - 1, &typeLen)); 216 auto capType = string(buf, typeLen); 217 auto support = TYPES.find(capType) != TYPES.end(); 218 NAPI_ASSERT(env, support == true, "Invalid event name"); 219 CaptureContext context; 220 if constexpr(kOn) { 221 NAPI_ASSERT(env, argc > MIN_ARGC, "Need callback function argument"); 222 NAPI_CALL(env, napi_typeof(env, argv[1], &type)); 223 NAPI_ASSERT(env, type == napi_function, "Illegal arg[1], function required"); 224 napi_ref callbackRef = nullptr; 225 NAPI_CALL(env, napi_create_reference(env, argv[1], 1, &callbackRef)); 226 context.cbRef = callbackRef; 227 if (argc > MIN_ARGC + 1) { 228 NAPI_CALL(env, napi_typeof(env, argv[MIN_ARGC + 1], &type)); 229 NAPI_ASSERT(env, type == napi_object, "Illegal options argument"); 230 ParseCaptureOptions(env, capType, argv[MIN_ARGC + 1], context); 231 } 232 } 233 LOG_I("Update context for capture: %{public}s, active=%{public}d", capType.c_str(), kOn); 234 context.type = move(capType); 235 context.napiEnv = env; 236 auto asyncJob = thread(UpdateCaptureState, move(context), kOn); 237 asyncJob.detach(); 238 LOG_I("Return"); 239 return nullptr; 240 } 241 BindAddonProperties(napi_env env,string_view version)242 static bool BindAddonProperties(napi_env env, string_view version) 243 { 244 if (env == nullptr) { 245 LOG_E("Internal error, napi_env is nullptr"); 246 return false; 247 } 248 napi_value kit = nullptr; 249 NAPI_CALL_BASE(env, napi_create_object(env, &kit), false); 250 napi_value global = nullptr; 251 NAPI_CALL_BASE(env, napi_get_global(env, &global), false); 252 napi_value serverVersion = nullptr; 253 NAPI_CALL_BASE(env, napi_create_string_utf8(env, version.data(), version.length(), &serverVersion), false); 254 napi_property_descriptor kitProps[] = { 255 DECLARE_NAPI_STATIC_PROPERTY("serverVersion", serverVersion), 256 DECLARE_NAPI_STATIC_FUNCTION("startCapture", SetCaptureEventJsCallback<true>), 257 DECLARE_NAPI_STATIC_FUNCTION("stopCapture", SetCaptureEventJsCallback<false>), 258 }; 259 NAPI_CALL_BASE(env, napi_define_properties(env, kit, sizeof(kitProps)/sizeof(kitProps[0]), kitProps), false); 260 261 napi_property_descriptor globalProps[] = { 262 DECLARE_NAPI_STATIC_PROPERTY("uitestAddonKit", kit), 263 }; 264 NAPI_CALL_BASE(env, napi_define_properties(env, global, 1, globalProps), false); 265 return true; 266 } 267 RunJsClient(string_view serverVersion)268 bool RunJsClient(string_view serverVersion) 269 { 270 LOG_I("Enter RunJsClient, serverVersion=%{public}s", serverVersion.data()); 271 if (!OHOS::FileExists(JS_CODE_PATH.data())) { 272 LOG_E("Client jsCode not exist"); 273 return false; 274 } 275 OHOS::AbilityRuntime::Runtime::Options opt; 276 opt.lang = OHOS::AbilityRuntime::Runtime::Language::JS; 277 opt.loadAce = false; 278 opt.preload = false; 279 opt.isStageModel = true; // stage model with jsbundle packing 280 opt.isBundle = true; 281 opt.eventRunner = OHOS::AppExecFwk::EventRunner::Create(false); 282 if (opt.eventRunner == nullptr) { 283 LOG_E("Create event runner failed"); 284 return false; 285 } 286 auto runtime = OHOS::AbilityRuntime::JsRuntime::Create(opt); 287 if (runtime == nullptr) { 288 LOG_E("Create JsRuntime failed"); 289 return false; 290 } 291 if (!BindAddonProperties(reinterpret_cast<napi_env>(&(runtime->GetNativeEngine())), serverVersion)) { 292 LOG_E("Bind addon functions failed"); 293 return false; 294 } 295 map<string, vector<string>> napiLibPath; 296 napiLibPath.insert({"default", {"data/local/tmp"}}); 297 runtime->SetAppLibPath(napiLibPath); 298 // execute single .abc file 299 auto execAbcRet = runtime->RunScript(JS_CODE_PATH.data(), "", false); 300 LOG_I("Run client jsCode, ret=%{public}d", execAbcRet); 301 pthread_setname_np(pthread_self(), "event_runner"); 302 auto runnerRet = opt.eventRunner->Run(); 303 LOG_I("Event runner exit, code=%{public}d", runnerRet); 304 return EXIT_SUCCESS; 305 } 306 } // namespace OHOS::uitest 307