1 /*
2 * Copyright (c) 2021-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 #include "bootevent.h"
16
17 #include <stdbool.h>
18 #include "init_module_engine.h"
19 #include "init_group_manager.h"
20 #include "init_cmdexecutor.h"
21 #include "trigger_manager.h"
22 #include "init_log.h"
23 #include "plugin_adapter.h"
24 #include "init_hook.h"
25 #include "init_service.h"
26 #include "bootstage.h"
27 #include "securec.h"
28 #include "init_utils.h"
29 #include "init_cmds.h"
30 #include "config_policy_utils.h"
31
32 #ifdef WITH_SELINUX
33 #include <policycoreutils.h>
34 #endif
35
GetBootSwitchEnable(const char * paramName)36 static int GetBootSwitchEnable(const char *paramName)
37 {
38 char bootEventOpen[6] = ""; // 6 is length of bool value
39 uint32_t len = sizeof(bootEventOpen);
40 SystemReadParam(paramName, bootEventOpen, &len);
41 if (strcmp(bootEventOpen, "true") == 0 || strcmp(bootEventOpen, "1") == 0) {
42 return 1;
43 }
44 return 0;
45 }
46
47 static int g_bootEventNum = 0;
48
49 static bool g_isBootCompleted = false;
50
51 static ListNode bootEventList = {&bootEventList, &bootEventList};
52
BootEventParaListCompareProc(ListNode * node,void * data)53 static int BootEventParaListCompareProc(ListNode *node, void *data)
54 {
55 BOOT_EVENT_PARAM_ITEM *item = (BOOT_EVENT_PARAM_ITEM *)node;
56 if (strncmp(item->paramName, BOOT_EVENT_PARA_PREFIX, BOOT_EVENT_PARA_PREFIX_LEN) != 0) {
57 return -1;
58 }
59 if (strcmp(item->paramName + BOOT_EVENT_PARA_PREFIX_LEN, (const char *)data) == 0) {
60 return 0;
61 }
62 return -1;
63 }
64
ParseBooteventCompareProc(ListNode * node,void * data)65 static int ParseBooteventCompareProc(ListNode *node, void *data)
66 {
67 BOOT_EVENT_PARAM_ITEM *item = (BOOT_EVENT_PARAM_ITEM *)node;
68 if (strcmp(item->paramName, (const char *)data) == 0) {
69 return 0;
70 }
71 return -1;
72 }
73
AddBootEventItem(BOOT_EVENT_PARAM_ITEM * item,const char * paramName)74 static int AddBootEventItem(BOOT_EVENT_PARAM_ITEM *item, const char *paramName)
75 {
76 OH_ListInit(&item->node);
77 for (int i = 0; i < BOOTEVENT_MAX; i++) {
78 item->timestamp[i].tv_nsec = 0;
79 item->timestamp[i].tv_sec = 0;
80 }
81 item->paramName = strdup(paramName);
82 if (item->paramName == NULL) {
83 free(item);
84 return -1;
85 }
86 item->flags = BOOTEVENT_TYPE_SERVICE;
87 OH_ListAddTail(&bootEventList, (ListNode *)&item->node);
88 g_bootEventNum++;
89 return 0;
90 }
91
AddBootEventItemByName(const char * paramName)92 static int AddBootEventItemByName(const char *paramName)
93 {
94 BOOT_EVENT_PARAM_ITEM *item = (BOOT_EVENT_PARAM_ITEM *)calloc(1, sizeof(BOOT_EVENT_PARAM_ITEM));
95 if (item == NULL) {
96 return -1;
97 }
98
99 return AddBootEventItem(item, paramName);
100 }
101
SetServiceBooteventHookMgr(const char * serviceName,const char * paramName,int state)102 static void SetServiceBooteventHookMgr(const char *serviceName, const char *paramName, int state)
103 {
104 #ifndef STARTUP_INIT_TEST
105 SERVICE_BOOTEVENT_CTX context;
106 context.serviceName = serviceName;
107 context.reserved = paramName;
108 context.state = state;
109 HookMgrExecute(GetBootStageHookMgr(), INIT_SERVICE_BOOTEVENT, (void*)(&context), NULL);
110 #endif
111 }
112
113
AddServiceBootEvent(const char * serviceName,const char * paramName)114 static int AddServiceBootEvent(const char *serviceName, const char *paramName)
115 {
116 ServiceExtData *extData = NULL;
117 ListNode *found = NULL;
118 if ((paramName == NULL) || (strncmp(paramName, BOOT_EVENT_PARA_PREFIX, BOOT_EVENT_PARA_PREFIX_LEN) != 0)) {
119 return -1;
120 }
121 found = OH_ListFind(&bootEventList, (void *)paramName, ParseBooteventCompareProc);
122 if (found != NULL) {
123 return -1;
124 }
125 // Find an empty bootevent data position
126 for (int i = HOOK_ID_BOOTEVENT; i < HOOK_ID_BOOTEVENT_MAX; i++) {
127 extData = AddServiceExtData(serviceName, i, NULL, sizeof(BOOT_EVENT_PARAM_ITEM));
128 if (extData != NULL) {
129 break;
130 }
131 }
132
133 INIT_CHECK(extData != NULL, return -1);
134
135 BOOT_EVENT_PARAM_ITEM *item = (BOOT_EVENT_PARAM_ITEM *)extData->data;
136
137 if (AddBootEventItem(item, paramName) != 0) {
138 DelServiceExtData(serviceName, extData->dataId);
139 return -1;
140 }
141
142 SetServiceBooteventHookMgr(serviceName, paramName, 1);
143 return 0;
144 }
145
AddInitBootEvent(const char * bootEventName)146 static void AddInitBootEvent(const char *bootEventName)
147 {
148 BOOT_EVENT_PARAM_ITEM *found = NULL;
149 found = (BOOT_EVENT_PARAM_ITEM *)OH_ListFind(&bootEventList, (void *)bootEventName, ParseBooteventCompareProc);
150 if (found != NULL) {
151 (void)clock_gettime(CLOCK_MONOTONIC, &(found->timestamp[BOOTEVENT_READY]));
152 return;
153 }
154
155 BOOT_EVENT_PARAM_ITEM *item = calloc(1, sizeof(BOOT_EVENT_PARAM_ITEM));
156 INIT_CHECK(item != NULL, return);
157
158 OH_ListInit(&item->node);
159
160 (void)clock_gettime(CLOCK_MONOTONIC, &(item->timestamp[BOOTEVENT_FORK]));
161
162 item->paramName = strdup(bootEventName);
163 INIT_CHECK(item->paramName != NULL, free(item);
164 return);
165
166 item->flags = BOOTEVENT_TYPE_JOB;
167 OH_ListAddTail(&bootEventList, (ListNode *)&item->node);
168 return;
169 }
170
171 #define BOOT_EVENT_BOOT_COMPLETED "bootevent.boot.completed"
172
BootEventDestroy(ListNode * node)173 static void BootEventDestroy(ListNode *node)
174 {
175 BOOT_EVENT_PARAM_ITEM *bootEvent = (BOOT_EVENT_PARAM_ITEM *)node;
176 INIT_CHECK(bootEvent->paramName == NULL, free((void *)bootEvent->paramName));
177 free((void *)bootEvent);
178 }
179
AddItemToJson(cJSON * root,const char * name,double startTime,int pid,double durTime)180 static int AddItemToJson(cJSON *root, const char *name, double startTime, int pid, double durTime)
181 {
182 cJSON *obj = cJSON_CreateObject(); // release obj at traverse done
183 INIT_CHECK_RETURN_VALUE(obj != NULL, -1);
184 cJSON_AddStringToObject(obj, "name", name);
185 cJSON_AddNumberToObject(obj, "ts", startTime);
186 cJSON_AddStringToObject(obj, "ph", "X");
187 cJSON_AddNumberToObject(obj, "pid", pid);
188 cJSON_AddNumberToObject(obj, "tid", pid);
189 cJSON_AddNumberToObject(obj, "dur", durTime);
190 cJSON_AddItemToArray(root, obj);
191 return 0;
192 }
193
BootEventTraversal(ListNode * node,void * root)194 static int BootEventTraversal(ListNode *node, void *root)
195 {
196 static int start = 0;
197 BOOT_EVENT_PARAM_ITEM *item = (BOOT_EVENT_PARAM_ITEM *)node;
198 double forkTime = (double)item->timestamp[BOOTEVENT_FORK].tv_sec * MSECTONSEC +
199 (double)item->timestamp[BOOTEVENT_FORK].tv_nsec / USTONSEC;
200 double readyTime = (double)item->timestamp[BOOTEVENT_READY].tv_sec * MSECTONSEC +
201 (double)item->timestamp[BOOTEVENT_READY].tv_nsec / USTONSEC;
202 double durTime = readyTime - forkTime;
203 if (item->pid == 0) {
204 if (durTime < SAVEINITBOOTEVENTMSEC) {
205 return 0;
206 }
207 item->pid = 1; // 1 is init pid
208 }
209 if (start == 0) {
210 // set trace start time 0
211 INIT_CHECK_RETURN_VALUE(AddItemToJson((cJSON *)root, item->paramName, 0,
212 1, 0) == 0, -1);
213 start++;
214 }
215 INIT_CHECK_RETURN_VALUE(AddItemToJson((cJSON *)root, item->paramName, forkTime,
216 item->pid, durTime > 0 ? durTime : 0) == 0, -1);
217 return 0;
218 }
219
CreateBootEventFile(const char * file,mode_t mode)220 static int CreateBootEventFile(const char *file, mode_t mode)
221 {
222 if (access(file, F_OK) == 0) {
223 INIT_LOGW("File %s already exist", file);
224 return 0;
225 }
226 if (errno != ENOENT) {
227 INIT_LOGW("Failed to access %s, err = %d", file, errno);
228 return -1;
229 }
230 CheckAndCreateDir(file);
231 int fd = open(file, O_CREAT, mode);
232 if (fd < 0) {
233 INIT_LOGE("Failed create %s, err=%d", file, errno);
234 return -1;
235 }
236 close(fd);
237 #ifdef WITH_SELINUX
238 INIT_LOGI("start to restorecon selinux");
239 (void)RestoreconRecurse(BOOTEVENT_OUTPUT_PATH);
240 #endif
241 return 0;
242 }
243
SaveServiceBootEvent()244 static int SaveServiceBootEvent()
245 {
246 INIT_CHECK(GetBootSwitchEnable("persist.init.bootuptrace.enable"), return 0);
247
248 int ret = CreateBootEventFile(BOOTEVENT_OUTPUT_PATH "bootup.trace", S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);
249 INIT_CHECK_RETURN_VALUE(ret == 0, -1);
250 FILE *tmpFile = fopen(BOOTEVENT_OUTPUT_PATH "bootup.trace", "wr");
251 INIT_CHECK_RETURN_VALUE(tmpFile != NULL, -1);
252 cJSON *root = cJSON_CreateArray();
253 INIT_CHECK(root != NULL, (void)fclose(tmpFile);
254 return -1);
255
256 OH_ListTraversal(&bootEventList, (void *)root, BootEventTraversal, 0);
257 char *buff = cJSON_Print(root);
258 if (buff == NULL) {
259 cJSON_Delete(root);
260 (void)fclose(tmpFile);
261 return -1;
262 }
263 INIT_CHECK_ONLY_ELOG(fprintf(tmpFile, "%s\n", buff) >= 0, "save boot event file failed");
264 free(buff);
265 cJSON_Delete(root);
266 (void)fflush(tmpFile);
267 (void)fclose(tmpFile);
268 return 0;
269 }
270
ReportSysEvent(void)271 static void ReportSysEvent(void)
272 {
273 INIT_CHECK(GetBootSwitchEnable("persist.init.bootevent.enable"), return);
274 #ifndef STARTUP_INIT_TEST
275 InitModuleMgrInstall("eventmodule");
276 InitModuleMgrUnInstall("eventmodule");
277 #endif
278 return;
279 }
280
BootCompleteClearAll(void)281 static void BootCompleteClearAll(void)
282 {
283 InitGroupNode *node = GetNextGroupNode(NODE_TYPE_SERVICES, NULL);
284 while (node != NULL) {
285 if (node->data.service == NULL) {
286 node = GetNextGroupNode(NODE_TYPE_SERVICES, node);
287 continue;
288 }
289 for (int i = HOOK_ID_BOOTEVENT; i < HOOK_ID_BOOTEVENT_MAX; i++) {
290 ServiceExtData *extData = GetServiceExtData(node->name, i);
291 if (extData == NULL) {
292 return;
293 }
294 free(((BOOT_EVENT_PARAM_ITEM *)extData->data)->paramName);
295 OH_ListRemove(&((BOOT_EVENT_PARAM_ITEM *)extData->data)->node);
296 DelServiceExtData(node->name, i);
297 }
298 }
299
300 // clear init boot event
301 OH_ListRemoveAll(&bootEventList, BootEventDestroy);
302 g_bootEventNum = 0;
303 }
304
WriteBooteventSysParam(const char * paramName)305 static void WriteBooteventSysParam(const char *paramName)
306 {
307 char buf[64];
308 long long uptime;
309 char name[PARAM_NAME_LEN_MAX];
310
311 uptime = GetUptimeInMicroSeconds(NULL);
312
313 INIT_CHECK_ONLY_ELOG(snprintf_s(buf, sizeof(buf), sizeof(buf) - 1, "%lld", uptime) >= 0,
314 "snprintf_s buf failed");
315 INIT_CHECK_ONLY_ELOG(snprintf_s(name, sizeof(name), sizeof(name) - 1, "ohos.boot.time.%s", paramName) >= 0,
316 "snprintf_s name failed");
317 SystemWriteParam(name, buf);
318 }
319
BootEventParaFireByName(const char * paramName)320 static int BootEventParaFireByName(const char *paramName)
321 {
322 BOOT_EVENT_PARAM_ITEM *found = NULL;
323
324 char *bootEventValue = strrchr(paramName, '.');
325 INIT_CHECK(bootEventValue != NULL, return 0);
326 bootEventValue[0] = '\0';
327
328 WriteBooteventSysParam(paramName);
329
330 found = (BOOT_EVENT_PARAM_ITEM *)OH_ListFind(&bootEventList, (void *)paramName, BootEventParaListCompareProc);
331 if (found == NULL) {
332 return 0;
333 }
334
335 // Already fired
336 if (found->timestamp[BOOTEVENT_READY].tv_sec > 0) {
337 return 0;
338 }
339 INIT_CHECK_RETURN_VALUE(clock_gettime(CLOCK_MONOTONIC,
340 &(found->timestamp[BOOTEVENT_READY])) == 0, 0);
341
342 g_bootEventNum--;
343 SetServiceBooteventHookMgr(NULL, paramName, 2); // 2: bootevent service has ready
344 // Check if all boot event params are fired
345 if (g_bootEventNum > 0) {
346 return 0;
347 }
348 // All parameters are fired, set boot completed now ...
349 INIT_LOGI("All boot events are fired, boot complete now ...");
350 SystemWriteParam(BOOT_EVENT_BOOT_COMPLETED, "true");
351 g_isBootCompleted = true;
352 SaveServiceBootEvent();
353 // report complete event
354 ReportSysEvent();
355 BootCompleteClearAll();
356 #ifndef STARTUP_INIT_TEST
357 HookMgrExecute(GetBootStageHookMgr(), INIT_BOOT_COMPLETE, NULL, NULL);
358 #endif
359 RemoveCmdExecutor("bootevent", -1);
360 return 1;
361 }
362
363 #define BOOT_EVENT_FIELD_NAME "bootevents"
ServiceParseBootEventHook(SERVICE_PARSE_CTX * serviceParseCtx)364 static void ServiceParseBootEventHook(SERVICE_PARSE_CTX *serviceParseCtx)
365 {
366 int cnt;
367 cJSON *bootEvents = cJSON_GetObjectItem(serviceParseCtx->serviceNode, BOOT_EVENT_FIELD_NAME);
368
369 // No boot events in config file
370 if (bootEvents == NULL) {
371 return;
372 }
373 SERVICE_INFO_CTX ctx = {0};
374 ctx.serviceName = serviceParseCtx->serviceName;
375 HookMgrExecute(GetBootStageHookMgr(), INIT_SERVICE_CLEAR, (void *)&ctx, NULL);
376 // Single boot event in config file
377 if (!cJSON_IsArray(bootEvents)) {
378 if (AddServiceBootEvent(serviceParseCtx->serviceName,
379 cJSON_GetStringValue(bootEvents)) != 0) {
380 INIT_LOGI("Add service bootEvent failed %s", serviceParseCtx->serviceName);
381 return;
382 }
383 return;
384 }
385
386 // Multiple boot events in config file
387 cnt = cJSON_GetArraySize(bootEvents);
388 for (int i = 0; i < cnt; i++) {
389 cJSON *item = cJSON_GetArrayItem(bootEvents, i);
390 if (AddServiceBootEvent(serviceParseCtx->serviceName,
391 cJSON_GetStringValue(item)) != 0) {
392 INIT_LOGI("Add service bootEvent failed %s", serviceParseCtx->serviceName);
393 continue;
394 }
395 }
396 }
397
398 static int g_finished = 0;
DoBootEventCmd(int id,const char * name,int argc,const char ** argv)399 static int DoBootEventCmd(int id, const char *name, int argc, const char **argv)
400 {
401 if (g_finished) {
402 return 0;
403 }
404
405 PLUGIN_CHECK(argc >= 1, return -1, "Invalid parameter");
406 if (strcmp(argv[0], "init") == 0) {
407 if (argc < 2) { // 2 args
408 return 0;
409 }
410 AddInitBootEvent(argv[1]);
411 } else {
412 // argv[0] samgr.ready.true
413 g_finished = BootEventParaFireByName(argv[0]);
414 }
415 return 0;
416 }
417
AddReservedBooteventsByFile(const char * name)418 static void AddReservedBooteventsByFile(const char *name)
419 {
420 char buf[MAX_PATH_LEN];
421
422 FILE *file = fopen(name, "r");
423 if (file == NULL) {
424 return;
425 }
426
427 while (fgets((void *)buf, sizeof(buf) - 1, file)) {
428 buf[sizeof(buf) - 1] = '\0';
429 char *end = strchr(buf, '\r');
430 if (end != NULL) {
431 *end = '\0';
432 }
433 end = strchr(buf, '\n');
434 if (end != NULL) {
435 *end = '\0';
436 }
437 INIT_LOGI("Got priv-app bootevent: %s", buf);
438 AddBootEventItemByName(buf);
439 }
440 (void)fclose(file);
441 }
442
AddReservedBootevents(void)443 static void AddReservedBootevents(void)
444 {
445 CfgFiles *files = GetCfgFiles("etc/init/priv_app.bootevents");
446 for (int i = MAX_CFG_POLICY_DIRS_CNT - 1; files && i >= 0; i--) {
447 if (files->paths[i]) {
448 AddReservedBooteventsByFile(files->paths[i]);
449 }
450 }
451 FreeCfgFiles(files);
452 }
453
DoUnsetBootEventCmd(int id,const char * name,int argc,const char ** argv)454 static int DoUnsetBootEventCmd(int id, const char *name, int argc, const char **argv)
455 {
456 if ((argc < 1) || (argv[0] == NULL) || (strlen(argv[0]) <= strlen(BOOT_EVENT_PARA_PREFIX)) ||
457 (strncmp(argv[0], BOOT_EVENT_PARA_PREFIX, strlen(BOOT_EVENT_PARA_PREFIX)) != 0)) {
458 return INIT_EPARAMETER;
459 }
460 const char *eventName = argv[0] + strlen(BOOT_EVENT_PARA_PREFIX);
461 BOOT_EVENT_PARAM_ITEM *item =
462 (BOOT_EVENT_PARAM_ITEM *)OH_ListFind(&bootEventList, (void *)eventName, BootEventParaListCompareProc);
463 PLUGIN_CHECK(item != NULL, return INIT_EPARAMETER, "item NULL");
464
465 if (item->timestamp[BOOTEVENT_READY].tv_sec == 0) {
466 INIT_LOGW("%s not set", argv[0]);
467 return INIT_OK;
468 }
469
470 SystemWriteParam(argv[0], "false");
471 if (g_finished != 0) {
472 SystemWriteParam(BOOT_EVENT_BOOT_COMPLETED, "false");
473 g_isBootCompleted = false;
474 g_finished = 0;
475 }
476
477 item->timestamp[BOOTEVENT_READY].tv_sec = 0;
478 g_bootEventNum++;
479 INIT_LOGI("UnsetBootEvent %s g_bootEventNum:%d", argv[0], g_bootEventNum);
480 return INIT_OK;
481 }
482
ParamSetBootEventHook(const HOOK_INFO * hookInfo,void * cookie)483 static int ParamSetBootEventHook(const HOOK_INFO *hookInfo, void *cookie)
484 {
485 AddReservedBootevents();
486 AddCmdExecutor("bootevent", DoBootEventCmd);
487 AddCmdExecutor("unset_bootevent", DoUnsetBootEventCmd);
488 return 0;
489 }
490
SetServiceBootEventFork(SERVICE_INFO_CTX * serviceCtx)491 static void SetServiceBootEventFork(SERVICE_INFO_CTX *serviceCtx)
492 {
493 BOOT_EVENT_PARAM_ITEM *item;
494 for (int i = HOOK_ID_BOOTEVENT; i < HOOK_ID_BOOTEVENT_MAX; i++) {
495 ServiceExtData *extData = GetServiceExtData(serviceCtx->serviceName, i);
496 if (extData == NULL) {
497 return;
498 }
499 item = (BOOT_EVENT_PARAM_ITEM *)extData->data;
500 if (serviceCtx->reserved != NULL) {
501 item->pid = *((int *)serviceCtx->reserved);
502 }
503 INIT_CHECK_ONLY_RETURN(clock_gettime(CLOCK_MONOTONIC,
504 &(item->timestamp[BOOTEVENT_FORK])) == 0);
505 }
506 }
507
GetBootEventList(void)508 ListNode *GetBootEventList(void)
509 {
510 return &bootEventList;
511 }
512
IsBootCompleted(void)513 bool IsBootCompleted(void)
514 {
515 return g_isBootCompleted;
516 }
517
AddCmdBootEvent(INIT_CMD_INFO * cmdCtx)518 static void AddCmdBootEvent(INIT_CMD_INFO *cmdCtx)
519 {
520 INIT_TIMING_STAT *timeStat = (INIT_TIMING_STAT *)cmdCtx->reserved;
521 long long diff = InitDiffTime(timeStat);
522 // If not time cost, just ignore
523 if (diff < SAVEINITBOOTEVENTMSEC) {
524 return;
525 }
526 BOOT_EVENT_PARAM_ITEM *item = calloc(1, sizeof(BOOT_EVENT_PARAM_ITEM));
527 if (item == NULL) {
528 return;
529 }
530 OH_ListInit(&item->node);
531 item->timestamp[BOOTEVENT_FORK] = timeStat->startTime;
532 item->timestamp[BOOTEVENT_READY] = timeStat->endTime;
533 int cmdLen = strlen(cmdCtx->cmdName) + strlen(cmdCtx->cmdContent) + 1; // 2 args 1 '\0'
534 item->paramName = calloc(1, cmdLen);
535 if (item->paramName == NULL) {
536 free(item);
537 return;
538 }
539 INIT_CHECK_ONLY_ELOG(snprintf_s(item->paramName, cmdLen, cmdLen - 1, "%s%s",
540 cmdCtx->cmdName, cmdCtx->cmdContent) >= 0,
541 "combine cmd args failed");
542 item->flags = BOOTEVENT_TYPE_CMD;
543 OH_ListAddTail(&bootEventList, (ListNode *)&item->node);
544 }
545
RecordInitCmd(const HOOK_INFO * info,void * cookie)546 static int RecordInitCmd(const HOOK_INFO *info, void *cookie)
547 {
548 if (cookie == NULL) {
549 return 0;
550 }
551 AddCmdBootEvent((INIT_CMD_INFO *)cookie);
552 return 0;
553 }
554
MODULE_CONSTRUCTOR(void)555 MODULE_CONSTRUCTOR(void)
556 {
557 // Add hook to record time-cost commands
558 HOOK_INFO info = {INIT_CMD_RECORD, 0, RecordInitCmd, NULL};
559 HookMgrAddEx(GetBootStageHookMgr(), &info);
560
561 // Add hook to parse all services with bootevents
562 InitAddServiceParseHook(ServiceParseBootEventHook);
563
564 // Add hook to record start time for services with bootevents
565 InitAddServiceHook(SetServiceBootEventFork, INIT_SERVICE_FORK_AFTER);
566
567 InitAddGlobalInitHook(0, ParamSetBootEventHook);
568 }
569