• 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 <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