1/* 2 * Copyright (c) 2024 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 16let util = requireNapi('util'); 17let fs = requireNapi('file.fs'); 18let hidebug = requireNapi('hidebug'); 19let cryptoFramework = requireNapi('security.cryptoFramework'); 20 21const ERROR_CODE_INVALID_PARAM = 401; 22const ERROR_MSG_INVALID_PARAM = 'Parameter error. Please check!'; 23 24let errMap = new Map(); 25errMap.set(ERROR_CODE_INVALID_PARAM, ERROR_MSG_INVALID_PARAM); 26 27class BusinessError extends Error { 28 constructor(code) { 29 let msg = ''; 30 if (errMap.has(code)) { 31 msg = errMap.get(code); 32 } else { 33 msg = ERROR_MSG_INNER_ERROR; 34 } 35 super(msg); 36 this.code = code; 37 } 38} 39 40let enabled = false; 41let gcObjList = []; 42 43let watchObjMap = new Map(); 44 45const registry = new FinalizationRegistry((hash) => { 46 gcObjList.push(hash); 47}); 48const MAX_FILE_NUM = 20; 49 50function flushLeakList() { 51 let leakObjList = []; 52 for (let [key, value] of watchObjMap) { 53 if (!gcObjList.includes(key)) { 54 leakObjList.push(value); 55 } 56 } 57 return leakObjList; 58} 59 60function createHeapDumpFile(fileName, filePath) { 61 hidebug.dumpJsHeapData(fileName); 62 let heapDumpFileName = fileName + '.heapsnapshot'; 63 let desFilePath = filePath + '/' + heapDumpFileName; 64 fs.moveFileSync('/data/storage/el2/base/files/' + heapDumpFileName, desFilePath, 0); 65 return getHeapDumpSHA256(desFilePath); 66} 67 68function getHeapDumpSHA256(filePath) { 69 let md = cryptoFramework.createMd('SHA256'); 70 let heapDumpFile = fs.openSync(filePath, fs.OpenMode.READ_WRITE); 71 let bufSize = 1024; 72 let readSize = 0; 73 let buf = new ArrayBuffer(bufSize); 74 let readOptions = { 75 offset: readSize, 76 length: bufSize 77 }; 78 let readLen = fs.readSync(heapDumpFile.fd, buf, readOptions); 79 80 while (readLen > 0) { 81 md.updateSync({ data: new Uint8Array(buf.slice(0, readLen)) }); 82 readSize += readLen; 83 readOptions.offset = readSize; 84 readLen = fs.readSync(heapDumpFile.fd, buf, readOptions); 85 } 86 fs.closeSync(heapDumpFile); 87 88 let digestOutPut = md.digestSync(); 89 return Array.from(digestOutPut.data, byte => ('0' + byte.toString(16)).slice(-2)).join('').toUpperCase(); 90} 91 92function deleteOldFile(filePath) { 93 let listFileOption = { 94 recursion: false, 95 listNum: 0, 96 filter: { 97 suffix: ['.heapsnapshot', '.jsleaklist'], 98 fileSizeOver: 0 99 } 100 }; 101 let files = fs.listFileSync(filePath, listFileOption); 102 if (files.length > MAX_FILE_NUM) { 103 const regex = /(\d+)\.(heapsnapshot|jsleaklist)/; 104 files.sort((a, b) => { 105 const matchA = a.match(regex); 106 const matchB = b.match(regex); 107 const timeStampA = matchA ? parseInt(matchA[1]) : 0; 108 const timeStampB = matchB ? parseInt(matchB[1]) : 0; 109 return timeStampA - timeStampB; 110 }); 111 for (let i = 0; i < files.length - MAX_FILE_NUM; i++) { 112 fs.unlinkSync(filePath + '/' + files[i]); 113 console.log(`File: ${files[i]} is deleted.`); 114 } 115 } 116} 117 118let jsLeakWatcher = { 119 watch: (obj, msg) => { 120 if (obj === undefined || obj === null || msg === undefined || msg === null) { 121 throw new BusinessError(ERROR_CODE_INVALID_PARAM); 122 } 123 if (!enabled) { 124 return; 125 } 126 let objMsg = { 127 hash: util.getHash(obj), 128 name: obj.constructor.name, 129 msg: msg 130 }; 131 watchObjMap.set(objMsg.hash, objMsg); 132 registry.register(obj, objMsg.hash); 133 }, 134 check: () => { 135 if (!enabled) { 136 return ''; 137 } 138 let leakObjList = flushLeakList(); 139 return JSON.stringify(leakObjList); 140 }, 141 dump: (filePath) => { 142 if (filePath === undefined || filePath === null) { 143 throw new BusinessError(ERROR_CODE_INVALID_PARAM); 144 } 145 let fileArray = new Array(2); 146 if (!enabled) { 147 return fileArray; 148 } 149 if (!fs.accessSync(filePath, fs.AccessModeType.EXIST)) { 150 throw new BusinessError(ERROR_CODE_INVALID_PARAM); 151 } 152 153 const fileTimeStamp = new Date().getTime().toString(); 154 try { 155 const heapDumpSHA256 = createHeapDumpFile(fileTimeStamp, filePath); 156 157 let file = fs.openSync(filePath + '/' + fileTimeStamp + '.jsleaklist', fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE); 158 let leakObjList = flushLeakList(); 159 let result = { 160 snapshot_hash: heapDumpSHA256, 161 leakObjList: leakObjList 162 }; 163 fs.writeSync(file.fd, JSON.stringify(result)); 164 fs.closeSync(file); 165 } catch (error) { 166 console.log('Dump heaoSnapShot or LeakList failed! ' + e); 167 return fileArray; 168 } 169 170 try { 171 deleteOldFile(filePath); 172 } catch (e) { 173 console.log('Delete old files failed! ' + e); 174 return fileArray; 175 } 176 177 fileArray[0] = fileTimeStamp + '.jsleaklist'; 178 fileArray[1] = fileTimeStamp + '.heapsnapshot'; 179 return fileArray; 180 }, 181 enable: (isEnable) => { 182 if (isEnable === undefined || isEnable === null) { 183 throw new BusinessError(ERROR_CODE_INVALID_PARAM); 184 } 185 enabled = isEnable; 186 if (!isEnable) { 187 gcObjList.length = 0; 188 watchObjMap.clear(); 189 } 190 } 191}; 192 193export default jsLeakWatcher; 194