Line data Source code
1 : /*
2 : * Copyright (C) 2021 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 :
6 92 : import cockpit from "cockpit";
7 92 : import React, { useState, useEffect, useRef } from 'react';
8 :
9 : import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
10 : import {
11 : Modal, ModalBody, ModalFooter, ModalHeader
12 : } from '@patternfly/react-core/dist/esm/components/Modal/index.js';
13 : import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js";
14 : import { Tooltip } from "@patternfly/react-core/dist/esm/components/Tooltip/index.js";
15 : import { ExternalLinkSquareAltIcon, HelpIcon } from '@patternfly/react-icons';
16 :
17 : import * as service from "service";
18 : import { EmptyStatePanel } from 'cockpit-components-empty-state.jsx';
19 : import { ModalError } from 'cockpit-components-inline-notification.jsx';
20 : import { ProfilesMenuDialogBody } from './profiles-menu-dialog-body';
21 : import { superuser } from 'superuser';
22 : import { useEvent, useInit } from "hooks";
23 : import { useDialogs } from "dialogs.jsx";
24 :
25 92 : const _ = cockpit.gettext;
26 :
27 71 : function poll_tuned_state(tuned, tunedService) {
28 71 : return Promise.all([
29 71 : tuned.call('/Tuned', 'com.redhat.tuned.control', 'is_running', []),
30 71 : tuned.call('/Tuned', 'com.redhat.tuned.control', 'active_profile', []),
31 71 : tuned.call('/Tuned', 'com.redhat.tuned.control', 'recommend_profile', [])
32 71 : ])
33 62 : .then(([[is_running], [active_result], [recommended]]) => {
34 8 : const active = is_running ? active_result : "none";
35 62 : return ({ state: "running", active, recommended });
36 62 : })
37 5 : .catch((ex) => {
38 5 : if (!tunedService.exists)
39 0 : return ({ state: "not-installed" });
40 4 : else if (tunedService.state != "running")
41 0 : return ({ state: "not-running" });
42 : else
43 4 : return Promise.reject(ex);
44 5 : });
45 71 : }
46 :
47 92 : export const TunedPerformanceProfile = () => {
48 92 : const Dialogs = useDialogs();
49 92 : const [btnText, setBtnText] = useState();
50 92 : const [state, setState] = useState();
51 92 : const [status, setStatus] = useState();
52 92 : const oldServiceState = useRef(null);
53 :
54 92 : const tunedService = useInit(() => service.proxy("tuned.service"));
55 :
56 71 : async function update() {
57 71 : const tuned = cockpit.dbus("com.redhat.tuned", { superuser: "try" });
58 71 : try {
59 71 : const { state, active, recommended } = await poll_tuned_state(tuned, tunedService);
60 64 : let status;
61 :
62 64 : if (state == "not-installed")
63 10 : status = _("Tuned is not available");
64 63 : else if (state == "not-running")
65 10 : status = _("Tuned is not running");
66 63 : else if (active == "none")
67 10 : status = _("Tuned is off");
68 63 : else if (active == recommended)
69 10 : status = _("This system is using the recommended profile");
70 : else
71 10 : status = _("This system is using a custom profile");
72 10 : setBtnText(state == "running" ? active : _("none"));
73 71 : setState(state);
74 71 : setStatus(status);
75 13 : } catch (ex) {
76 13 : console.warn("failed to poll tuned", ex);
77 :
78 13 : setBtnText("error");
79 13 : setStatus(_("Communication with tuned has failed"));
80 13 : }
81 67 : tuned.close();
82 71 : }
83 :
84 92 : useEvent(superuser, "reconnect", update);
85 71 : useEvent(tunedService, "changed", () => {
86 : // We get a flood of "changed" events sometimes without the
87 : // state actually changing. So let's protect against that.
88 71 : if (oldServiceState.current !== tunedService.state) {
89 71 : oldServiceState.current = tunedService.state;
90 71 : update();
91 71 : }
92 71 : });
93 :
94 1 : const showDialog = async () => {
95 1 : await Dialogs.run(TunedDialog, { tunedService });
96 : // Tuned does not send any change notifications...
97 1 : await update();
98 1 : };
99 :
100 92 : return (
101 92 : <Tooltip id="tuned-status-tooltip" content={status}>
102 92 : <Button id="tuned-status-button"
103 92 : isAriaDisabled={btnText == "error" || state == "not-installed" || !superuser.allowed}
104 92 : isInline
105 92 : onClick={showDialog}
106 92 : variant='link'>
107 92 : {btnText}
108 92 : </Button>
109 92 : </Tooltip>
110 : );
111 92 : };
112 :
113 1 : const TunedDialog = ({
114 1 : tunedService,
115 1 : dialogResult,
116 1 : }) => {
117 1 : const [tunedDbus, setTunedDbus] = useState(null);
118 1 : const [activeProfile, setActiveProfile] = useState();
119 1 : const [loading, setLoading] = useState(true);
120 1 : const [error, setError] = useState();
121 1 : const [profiles, setProfiles] = useState([]);
122 1 : const [selected, setSelected] = useState();
123 :
124 : /* Tuned doesn't implement the DBus.Properties interface, so
125 : * we occasionally poll for what we need.
126 : *
127 : * Tuned doesn't auto-activate on the bus, so we have to start
128 : * it explicitly when opening the dialog.
129 : */
130 :
131 1 : const setProfile = () => {
132 1 : const setService = () => {
133 : /* When the profile is none we disable tuned */
134 1 : const enable = (selected != "none");
135 1 : const action = enable ? "start" : "stop";
136 1 : return tunedDbus.call('/Tuned', 'com.redhat.tuned.control', action, [])
137 1 : .then(results => {
138 : /* Yup this is how tuned returns failures */
139 0 : if (!results[0]) {
140 0 : console.warn("Failed to " + action + " tuned:", results);
141 0 : if (results[1])
142 0 : return Promise.reject(results[1]);
143 0 : else if (enable)
144 0 : return Promise.reject(cockpit.format(_("Failed to enable tuned")));
145 : else
146 0 : return Promise.reject(cockpit.format(_("Failed to disable tuned")));
147 0 : }
148 :
149 : /* Now tell systemd about this change */
150 1 : if (enable && !tunedService.enabled)
151 1 : return tunedService.enable();
152 1 : else if (!enable && tunedService.enabled)
153 0 : return tunedService.disable();
154 : else
155 0 : return null;
156 1 : });
157 1 : };
158 :
159 1 : let promise;
160 :
161 1 : if (selected == "none") {
162 1 : promise = tunedDbus.call("/Tuned", 'com.redhat.tuned.control', 'disable', [])
163 1 : .then(results => {
164 : /* Yup this is how tuned returns failures */
165 0 : if (!results[0]) {
166 0 : console.warn("Failed to disable tuned profile:", results);
167 0 : return Promise.reject(_("Failed to disable tuned profile"));
168 0 : }
169 1 : });
170 1 : } else {
171 1 : promise = tunedDbus.call('/Tuned', 'com.redhat.tuned.control', 'switch_profile', [selected])
172 1 : .then(results => {
173 : /* Yup this is how tuned returns failures */
174 0 : if (!results[0][0]) {
175 0 : console.warn("Failed to switch profile:", results);
176 0 : return Promise.reject(results[0][1] || _("Failed to switch profile"));
177 0 : }
178 1 : });
179 1 : }
180 :
181 1 : return promise
182 1 : .then(setService)
183 1 : .then(() => dialogResult.resolve())
184 1 : .catch(setError);
185 1 : };
186 :
187 1 : useEffect(() => {
188 1 : const withInfo = (active, recommended, profiles) => {
189 1 : const model = [];
190 1 : profiles.forEach(p => {
191 1 : let name;
192 1 : let desc;
193 0 : if (typeof p === "string") {
194 0 : name = p;
195 0 : desc = "";
196 0 : } else {
197 1 : name = p[0];
198 1 : desc = p[1];
199 1 : }
200 1 : if (name != "none") {
201 1 : model.push({
202 1 : name,
203 1 : title: name,
204 1 : description: desc,
205 1 : active: name == active,
206 1 : recommended: name == recommended,
207 1 : });
208 1 : }
209 1 : });
210 :
211 1 : model.unshift({
212 1 : name: "none",
213 1 : title: _("None"),
214 1 : description: _("Disable tuned"),
215 1 : active: active == "none",
216 1 : recommended: recommended == "none",
217 1 : });
218 :
219 1 : setProfiles(model);
220 1 : setActiveProfile(active);
221 1 : setSelected(active);
222 1 : };
223 :
224 1 : const withTuned = (tunedDbus) => {
225 1 : const tunedProfiles = () => {
226 1 : return tunedDbus.call('/Tuned', 'com.redhat.tuned.control', 'profiles2', [])
227 1 : .then((result) => result[0])
228 0 : .catch(ex => {
229 0 : return tunedDbus.call('/Tuned', 'com.redhat.tuned.control', 'profiles', [])
230 0 : .then((result) => result[0]);
231 0 : });
232 1 : };
233 :
234 1 : return poll_tuned_state(tunedDbus, tunedService)
235 1 : .then(res => {
236 1 : const { state, active, recommended } = res;
237 0 : if (state != "running") {
238 0 : setError(_("Tuned has failed to start"));
239 0 : return;
240 0 : }
241 1 : return tunedProfiles()
242 1 : .then(profiles => {
243 1 : return withInfo(active, recommended, profiles);
244 1 : })
245 1 : .catch(setError);
246 1 : })
247 1 : .catch(setError);
248 1 : };
249 :
250 1 : let tuned = null;
251 :
252 1 : tunedService.start()
253 1 : .then(() => {
254 1 : tuned = cockpit.dbus("com.redhat.tuned", { superuser: "try" });
255 1 : setTunedDbus(tuned);
256 1 : })
257 1 : .then(() => withTuned(tuned))
258 1 : .catch(setError)
259 1 : .finally(() => setLoading(false));
260 :
261 1 : return () => {
262 1 : if (tuned)
263 1 : tuned.close();
264 1 : };
265 1 : }, [tunedService]);
266 :
267 1 : const help = (
268 1 : <Popover
269 1 : id="tuned-help"
270 1 : bodyContent={
271 1 : <div>
272 1 : {_("Tuned is a service that monitors your system and optimizes the performance under certain workloads. The core of Tuned are profiles, which tune your system for different use cases.")}
273 1 : </div>
274 : }
275 1 : footerContent={
276 1 : <Button component='a'
277 1 : rel="noopener noreferrer" target="_blank"
278 1 : variant='link'
279 1 : isInline
280 1 : icon={<ExternalLinkSquareAltIcon />} iconPosition="right"
281 1 : href="https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/monitoring_and_managing_system_status_and_performance/index">
282 1 : {_("Learn more")}
283 1 : </Button>
284 : }
285 : >
286 1 : <Button icon={<HelpIcon />} variant="plain" aria-label={_("Help")} />
287 1 : </Popover>
288 : );
289 :
290 1 : return (
291 1 : <Modal position="top" variant="medium"
292 1 : className="ct-m-stretch-body"
293 1 : isOpen
294 0 : onClose={() => dialogResult.resolve()}
295 : >
296 1 : <ModalHeader title={_("Change performance profile")}
297 1 : help={help}
298 1 : />
299 1 : <ModalBody>
300 0 : {error && <ModalError dialogError={typeof error == 'string' ? error : error.message} />}
301 1 : {loading && <EmptyStatePanel loading />}
302 1 : {activeProfile && <ProfilesMenuDialogBody active_profile={activeProfile}
303 1 : change_selected={setSelected}
304 1 : profiles={profiles} />}
305 1 : </ModalBody>
306 1 : <ModalFooter>
307 1 : <Button variant='primary' isDisabled={!selected} onClick={setProfile}>
308 1 : {_("Change profile")}
309 1 : </Button>
310 0 : <Button variant='link' onClick={() => dialogResult.resolve()}>
311 1 : {_("Cancel")}
312 1 : </Button>
313 1 : </ModalFooter>
314 1 : </Modal>
315 : );
316 1 : };
|