1 /* 2 * Copyright (c) 2025 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 <set> 17 #include <string> 18 #include <unistd.h> 19 #include <uv.h> 20 #include "nlohmann/json.hpp" 21 #include "common_utils.h" 22 #include "callback_code_napi.h" 23 24 namespace OHOS::perftest { 25 using namespace nlohmann; 26 using namespace std; 27 28 static constexpr size_t NAPI_MAX_BUF_LEN = 1024; 29 30 LockRelease(string errorMessage)31 void ThreadLock::LockRelease(string errorMessage) 32 { 33 errMsg = errorMessage; 34 ready = true; 35 condition.notify_all(); 36 } 37 38 /** Cached reference of callbackId and JsCallbackFunction, key is the unique id.*/ 39 static map<string, napi_ref> g_jsRefs; 40 static size_t g_incJsCbId = 0; 41 Get()42 CallbackCodeNapi &CallbackCodeNapi::Get() 43 { 44 static CallbackCodeNapi instance; 45 return instance; 46 } 47 GetFunctionFromParam(napi_env env,napi_value jsParam,json & param,string funcName)48 static void GetFunctionFromParam(napi_env env, napi_value jsParam, json& param, string funcName) 49 { 50 if (param.contains(funcName)) { 51 LOG_D("%{public}s has been defined", funcName.c_str()); 52 return; 53 } 54 bool funcExisted; 55 NAPI_CALL_RETURN_VOID(env, napi_has_named_property(env, jsParam, funcName.c_str(), &funcExisted)); 56 if (funcExisted) { 57 napi_value funcValue = nullptr; 58 NAPI_CALL_RETURN_VOID(env, napi_get_named_property(env, jsParam, funcName.c_str(), &funcValue)); 59 napi_valuetype valueType = napi_undefined; 60 NAPI_CALL_RETURN_VOID(env, napi_typeof(env, funcValue, &valueType)); 61 if (valueType != napi_function) { 62 LOG_E("GetFunctionFromParam failed, invalid callback function argument"); 63 return; 64 } 65 const auto jsCbId = string("js_callback#") + to_string(++g_incJsCbId); 66 // hold the const to avoid it be recycled, it's needed in performing callback 67 napi_ref ref = nullptr; 68 NAPI_CALL_RETURN_VOID(env, napi_create_reference(env, funcValue, 1, &ref)); 69 LOG_I("CbId = %{public}s, CbRef = %{public}p", jsCbId.c_str(), ref); 70 g_jsRefs.insert({ jsCbId, ref }); // store jsCbId instread of the function body 71 param[funcName] = jsCbId; 72 } 73 } 74 PreprocessCallback(napi_env env,ApiCallInfo & call,napi_value jsThis,napi_value jsParam,ApiCallErr & err)75 void CallbackCodeNapi::PreprocessCallback(napi_env env, ApiCallInfo &call, napi_value jsThis, 76 napi_value jsParam, ApiCallErr &err) 77 { 78 DCHECK(env != nullptr); 79 DCHECK(jsThis != nullptr); 80 napi_valuetype paramType = napi_undefined; 81 NAPI_CALL_RETURN_VOID(env, napi_typeof(env, jsParam, ¶mType)); 82 if (paramType != napi_object) { 83 err = ApiCallErr(ERR_INVALID_INPUT, "PreprocessCallback failed, Invalid type of param"); 84 return; 85 } 86 auto ¶mList = call.paramList_; 87 if (paramList.size() < 1 || paramList.at(0).type() != detail::value_t::object) { 88 err = ApiCallErr(ERR_INVALID_INPUT, "Missing PerfTestStrategy argument"); 89 return; 90 } 91 GetFunctionFromParam(env, jsParam, paramList.at(0), ACTION_CODE_FUNC_NAME); 92 GetFunctionFromParam(env, jsParam, paramList.at(0), RESET_CODE_FUNC_NAME); 93 } 94 InitCallbackContext(napi_env env,const ApiCallInfo & in,ApiReplyInfo & out,shared_ptr<CodeCallbackContext> ctx)95 void CallbackCodeNapi::InitCallbackContext(napi_env env, const ApiCallInfo &in, ApiReplyInfo &out, 96 shared_ptr<CodeCallbackContext> ctx) 97 { 98 LOG_I("Handler api callback: %{public}s", in.apiId_.c_str()); 99 DCHECK(env != nullptr); 100 DCHECK(in.paramList_.size() == TWO); 101 DCHECK(in.paramList_.at(INDEX_ZERO).type() == detail::value_t::string); 102 DCHECK(in.paramList_.at(INDEX_ONE).type() == detail::value_t::number_unsigned); 103 auto callbackId = in.paramList_.at(INDEX_ZERO).get<string>(); 104 auto timeout = in.paramList_.at(INDEX_ONE).get<uint32_t>(); 105 LOG_I("Begin to callback function: callback=%{public}s", callbackId.c_str()); 106 auto findCallback = g_jsRefs.find(callbackId); 107 if (findCallback == g_jsRefs.end()) { 108 out.exception_ = ApiCallErr(ERR_CALLBACK_FAILED, "JsCallbackFunction is not referenced: " + callbackId); 109 LOG_E("%{public}s", out.exception_.message_.c_str()); 110 return; 111 } 112 auto cb = [](napi_env env, napi_callback_info info) -> napi_value { 113 LOG_D("ExecuteCallback jscallback return start"); 114 size_t argc_in = 1; 115 napi_value argv_in[1] = {nullptr}; 116 void* param_in = nullptr; 117 NAPI_CALL_BASE(env, napi_get_cb_info(env, info, &argc_in, argv_in, nullptr, ¶m_in), nullptr); 118 ThreadLock* threadLock = reinterpret_cast<ThreadLock*>(param_in); 119 if (threadLock == nullptr) { 120 LOG_E("ExecuteCallback get threadLock failed"); 121 return nullptr; 122 } 123 NAPI_CALL_BASE(env, napi_get_value_bool(env, argv_in[0], &threadLock->res), nullptr); 124 threadLock->LockRelease(""); 125 LOG_I("ExecuteCallback jscallback return end, res = %{public}d", threadLock->res); 126 return nullptr; 127 }; 128 ctx->env = env; 129 ctx->callbackId = callbackId; 130 ctx->callbackRef = findCallback->second; 131 ctx->timeout = timeout; 132 ctx->cb = cb; 133 } 134 BindPromiseCallbacks(napi_env env,napi_value promise,ThreadLock * threadLock)135 void CallbackCodeNapi::BindPromiseCallbacks(napi_env env, napi_value promise, ThreadLock* threadLock) 136 { 137 bool is_promise = false; 138 NAPI_CALL_RETURN_VOID(env, napi_is_promise(env, promise, &is_promise)); 139 if (!is_promise) { 140 return; 141 } 142 napi_value catch_property; 143 NAPI_CALL_RETURN_VOID(env, napi_get_named_property(env, promise, "catch", &catch_property)); 144 auto catch_handle = [](napi_env env, napi_callback_info info) -> napi_value { 145 size_t argc_in = 1; 146 napi_value argv_in[1] = {nullptr}; 147 void* param_in = nullptr; 148 NAPI_CALL_BASE(env, napi_get_cb_info(env, info, &argc_in, argv_in, nullptr, ¶m_in), nullptr); 149 napi_value error_str; 150 napi_coerce_to_string(env, argv_in[0], &error_str); 151 size_t buf_size = 0; 152 char buf[NAPI_MAX_BUF_LEN] = {0}; 153 napi_get_value_string_utf8(env, error_str, buf, NAPI_MAX_BUF_LEN, &buf_size); 154 ThreadLock* threadLock = reinterpret_cast<ThreadLock*>(param_in); 155 if (threadLock == nullptr) { 156 LOG_E("ExecuteCallback get threadLock failed"); 157 return nullptr; 158 } 159 threadLock->LockRelease("Exception raised during call js_cb_function: " + string(buf, buf_size)); 160 return nullptr; 161 }; 162 napi_value catch_callback; 163 NAPI_CALL_RETURN_VOID(env, napi_create_function(env, "CatchCallack", NAPI_AUTO_LENGTH, catch_handle, 164 threadLock, &catch_callback)); 165 NAPI_CALL_RETURN_VOID(env, napi_call_function(env, promise, catch_property, 1, &catch_callback, nullptr)); 166 } 167 WaitforCallbackFinish(napi_env env,shared_ptr<CodeCallbackContext> context,ThreadLock * threadLock,ApiReplyInfo & out)168 void CallbackCodeNapi::WaitforCallbackFinish(napi_env env, shared_ptr<CodeCallbackContext> context, 169 ThreadLock* threadLock, ApiReplyInfo &out) 170 { 171 unique_lock<mutex> lock(threadLock->mutex); 172 const auto timeout = chrono::milliseconds(context->timeout); 173 if (!threadLock->condition.wait_for(lock, timeout, [&threadLock] { return threadLock->ready; })) { 174 LOG_E("Jscallback call timeout, has been waiting for %{public}d ms", context->timeout); 175 out.exception_ = ApiCallErr(ERR_CALLBACK_FAILED, "Code execution has been timeout."); 176 return; 177 } 178 LOG_I("Jscallback call has finished, res = %{public}d", threadLock->res); 179 if (!threadLock->errMsg.empty()) { 180 out.exception_ = ApiCallErr(ERR_CALLBACK_FAILED, threadLock->errMsg); 181 LOG_E("%{public}s", out.exception_.message_.c_str()); 182 } else if (!threadLock->res) { 183 out.exception_ = ApiCallErr(ERR_CALLBACK_FAILED, "Callback execution return false"); 184 } 185 delete threadLock; 186 threadLock = nullptr; 187 } 188 189 /**Handle api callback from server end.*/ ExecuteCallback(napi_env env,const ApiCallInfo & in,ApiReplyInfo & out)190 void CallbackCodeNapi::ExecuteCallback(napi_env env, const ApiCallInfo &in, ApiReplyInfo &out) 191 { 192 shared_ptr<CodeCallbackContext> context = make_shared<CodeCallbackContext>(); 193 InitCallbackContext(env, in, out, context); 194 if (out.exception_.code_ != NO_ERROR) { 195 LOG_W("InitCallbackContext failed, cannot perform callback"); 196 return; 197 } 198 ThreadLock* threadLock = new (nothrow) ThreadLock(); 199 auto task = [context, threadLock, this]() { 200 napi_value argv[1] = { nullptr }; 201 napi_value jsCallback = nullptr; 202 napi_get_reference_value(context->env, context->callbackRef, &jsCallback); 203 napi_create_function(context->env, "Complete", NAPI_AUTO_LENGTH, context->cb, threadLock, &argv[0]); 204 napi_value promise; 205 napi_call_function(context->env, nullptr, jsCallback, 1, argv, &promise); 206 // accessing callback from js-caller, do check and warn 207 auto hasError = false; 208 napi_is_exception_pending(context->env, &hasError); 209 if (hasError) { 210 napi_value exception; 211 napi_get_and_clear_last_exception(context->env, &exception); 212 napi_value error_str; 213 napi_coerce_to_string(context->env, exception, &error_str); 214 size_t buf_size = 0; 215 char buf[NAPI_MAX_BUF_LEN] = {0}; 216 napi_get_value_string_utf8(context->env, error_str, buf, NAPI_MAX_BUF_LEN, &buf_size); 217 if (threadLock == nullptr) { 218 return; 219 } 220 threadLock->LockRelease("Exception raised during call js_cb_function: " + string(buf, buf_size)); 221 return; 222 } 223 this->BindPromiseCallbacks(context->env, promise, threadLock); 224 }; 225 if (napi_send_event(env, task, napi_eprio_immediate) != napi_status::napi_ok) { 226 LOG_E("Exception raised during call js_cb_function!"); 227 } 228 WaitforCallbackFinish(env, context, threadLock, out); 229 } 230 DestroyCallbacks(napi_env env,const ApiCallInfo & in,ApiReplyInfo & out)231 void CallbackCodeNapi::DestroyCallbacks(napi_env env, const ApiCallInfo &in, ApiReplyInfo &out) 232 { 233 DCHECK(in.paramList_.size() == ONE); 234 DCHECK(in.paramList_.at(INDEX_ZERO).type() == detail::value_t::array); 235 for (auto& callbackId : in.paramList_.at(INDEX_ZERO)) { 236 auto findCallback = g_jsRefs.find(callbackId); 237 if (findCallback == g_jsRefs.end()) { 238 continue; 239 } 240 napi_delete_reference(env, findCallback->second); 241 g_jsRefs.erase(callbackId); 242 } 243 } 244 HandleCallbackEvent(napi_env env,const ApiCallInfo & in,ApiReplyInfo & out)245 void CallbackCodeNapi::HandleCallbackEvent(napi_env env, const ApiCallInfo &in, ApiReplyInfo &out) 246 { 247 if (in.apiId_ == "PerfTest.run") { 248 ExecuteCallback(env, in, out); 249 } else if (in.apiId_ == "PerfTest.destroy") { 250 DestroyCallbacks(env, in, out); 251 } else { 252 out.exception_ = ApiCallErr(ERR_INTERNAL, "Api dose not support callback: " + in.apiId_); 253 LOG_E("%{public}s", out.exception_.message_.c_str()); 254 } 255 } 256 } 257