Line data Source code
1 : /*
2 : * Copyright (C) 2020 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 :
6 31 : import React, { useRef, useState } from "react";
7 : import { Breadcrumb, BreadcrumbItem } from "@patternfly/react-core/dist/esm/components/Breadcrumb/index.js";
8 : import { PageBreadcrumb, PageSection } from "@patternfly/react-core/dist/esm/components/Page/index.js";
9 : import { Stack } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
10 : import { ExclamationCircleIcon } from '@patternfly/react-icons';
11 :
12 : import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
13 : import { ServiceDetails } from "./service-details.jsx";
14 : import { LogsPanel } from "cockpit-components-logs-panel.jsx";
15 : import { superuser } from 'superuser';
16 : import { WithDialogs } from "dialogs.jsx";
17 :
18 31 : import cockpit from "cockpit";
19 : import { useObject } from "hooks";
20 :
21 : import s_bus from "./busnames.js";
22 :
23 31 : const _ = cockpit.gettext;
24 :
25 23 : function debug() {
26 1 : if (window.debugging == "all" || window.debugging?.includes("service-details")) // not-covered: debugging
27 1 : console.debug.apply(console, arguments); // not-covered: debugging
28 23 : }
29 :
30 23 : export const Service = ({ dbusClient, owner, unitId, unitIsValid, addTimerProperties, pinnedUnits }) => {
31 23 : const _path = useRef(null);
32 23 : const [error, setError] = useState(null);
33 23 : const [reloading, setReloading] = useState(false);
34 23 : const [unitProps, setUnitProps] = useState(null);
35 :
36 23 : const updateProperties = () => {
37 23 : const path = _path.current?.path;
38 23 : const promises = [dbusClient.call(path, s_bus.I_PROPS, "GetAll", [s_bus.I_UNIT])];
39 :
40 23 : if (unitId.endsWith(".timer"))
41 1 : promises.push(dbusClient.call(path, s_bus.I_PROPS, "GetAll", [s_bus.I_TIMER]));
42 20 : else if (unitId.endsWith(".socket"))
43 3 : promises.push(dbusClient.call(path, s_bus.I_PROPS, "GetAll", [s_bus.I_SOCKET]));
44 :
45 23 : Promise.all(promises)
46 23 : .then(replies => {
47 23 : const unit_props = replies[0][0];
48 :
49 : // unwrap variants
50 23 : for (const key in unit_props)
51 23 : unit_props[key] = unit_props[key].v;
52 :
53 23 : if (unitId.endsWith(".timer"))
54 1 : addTimerProperties(replies[1][0], unit_props);
55 20 : else if (unitId.endsWith(".socket"))
56 3 : unit_props.Listen = replies[1][0].Listen.v;
57 :
58 23 : unit_props.path = path;
59 :
60 23 : debug("Service detail", unitId, "updated properties:", JSON.stringify(unit_props));
61 23 : setUnitProps(unit_props);
62 23 : setError(null);
63 23 : })
64 0 : .catch(ex => setError(ex.toString())); // not-covered: unexpected OS error
65 23 : };
66 :
67 : // load object path and set up PropertiesChanged subscription whenever unitId changes
68 23 : useObject(
69 23 : () => {
70 23 : dbusClient.call(s_bus.O_MANAGER, s_bus.I_MANAGER, "LoadUnit", [unitId])
71 23 : .then(([path]) => {
72 23 : debug("Service detail", unitId, "loaded path", path);
73 :
74 1 : _path.current?.propSub.remove();
75 23 : const propSub = dbusClient.subscribe(
76 14 : { path, interface: s_bus.I_PROPS, member: "PropertiesChanged" }, () => {
77 14 : debug("Service detail", unitId, "PropertiesChanged; path", path);
78 14 : updateProperties();
79 14 : });
80 :
81 23 : _path.current = { unitId, path, propSub };
82 :
83 : // initial load
84 23 : updateProperties();
85 23 : })
86 2 : .catch(ex => setError(ex.toString())); // not-covered: unexpected OS error
87 23 : },
88 23 : null, [unitId]);
89 :
90 23 : useObject(
91 23 : () => dbusClient.subscribe(
92 23 : { interface: s_bus.I_MANAGER, member: "Reloading" },
93 7 : (_, _i, _s, [is_reloading]) => {
94 7 : debug("Service detail", unitId, "Reloading", is_reloading, "current unit", _path.current?.path);
95 7 : setReloading(is_reloading);
96 7 : if (!is_reloading && _path.current)
97 7 : updateProperties();
98 7 : }),
99 10 : sub => sub.remove(), []);
100 :
101 : /* We need this *only* to pick up failed Conditions after attempting to start a service, as the unit immediately gets
102 : * unloaded again and we don't get a PropertiesChanged signal. */
103 23 : useObject(
104 23 : () => dbusClient.subscribe(
105 13 : { interface: s_bus.I_MANAGER, member: "JobRemoved" }, (_p, _i, _s, [_job_id, _job_path, unit, result]) => {
106 13 : if (result === "done" && unitId === _path.current?.unitId) {
107 13 : debug("Service detail", unitId, "JobRemoved", _path.current?.path);
108 13 : updateProperties();
109 13 : }
110 13 : }),
111 10 : sub => sub.remove(), []);
112 :
113 : // render
114 23 : if (error)
115 3 : return <EmptyStatePanel title={_("Loading unit failed")} icon={ExclamationCircleIcon} paragraph={error} />; // not-covered: unexpected OS error
116 :
117 23 : if (unitProps === null)
118 23 : return <EmptyStatePanel loading title={_("Loading...")} paragraph={unitId} />;
119 :
120 : // resolve Alias name to primary ID
121 23 : const cur_unit_id = unitProps.Id;
122 :
123 2 : const unit_type = owner == "system" ? "UNIT" : "USER_UNIT";
124 23 : const match = [
125 23 : "_SYSTEMD_" + unit_type + "=" + cur_unit_id, "+",
126 23 : "COREDUMP_" + unit_type + "=" + cur_unit_id, "+",
127 23 : unit_type + "=" + cur_unit_id,
128 23 : ];
129 2 : const service_type = owner == "system" ? "service" : "user-service";
130 23 : const url = "/system/logs/#/?prio=debug&" + service_type + "=" + cur_unit_id;
131 23 : const load_state = unitProps.LoadState;
132 :
133 23 : return (
134 23 : <WithDialogs>
135 23 : <PageBreadcrumb hasBodyWrapper={false} stickyOnBreakpoint={{ default: "top" }}>
136 23 : <Breadcrumb>
137 23 : <BreadcrumbItem to={"#" + cockpit.location.href.replace(/\/[^?]*/, '')}>{_("Services")}</BreadcrumbItem>
138 23 : <BreadcrumbItem isActive>
139 23 : {cur_unit_id}
140 23 : </BreadcrumbItem>
141 23 : </Breadcrumb>
142 23 : </PageBreadcrumb>
143 23 : <PageSection hasBodyWrapper={false} id="service-details">
144 23 : <Stack hasGutter>
145 23 : <ServiceDetails unit={unitProps}
146 23 : owner={owner}
147 23 : permitted={superuser.allowed}
148 23 : loadingUnits={reloading}
149 23 : isValid={unitIsValid}
150 23 : pinnedUnits={pinnedUnits} />
151 7 : {(load_state === "loaded" || load_state === "masked") &&
152 23 : <LogsPanel title={_("Service logs")} match={match} emptyMessage={_("No log entries")} max={10} goto_url={url} search_options={{ prio: "debug", [service_type]: cur_unit_id }} />}
153 23 : </Stack>
154 23 : </PageSection>
155 23 : </WithDialogs>
156 : );
157 23 : };
|