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