LCOV - code coverage report
Current view: top level - pkg/systemd/overview-cards - tuned-dialog.jsx Coverage Total Hit
Test: cockpit Lines: 83.5 % 243 203
Test Date: 2026-07-02 14:11:36

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2021 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5              : 
       6           35 : import cockpit from "cockpit";
       7           35 : 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           35 : const _ = cockpit.gettext;
      26              : 
      27           24 : function poll_tuned_state(tuned, tunedService) {
      28           24 :     return Promise.all([
      29           24 :         tuned.call('/Tuned', 'com.redhat.tuned.control', 'is_running', []),
      30           24 :         tuned.call('/Tuned', 'com.redhat.tuned.control', 'active_profile', []),
      31           24 :         tuned.call('/Tuned', 'com.redhat.tuned.control', 'recommend_profile', [])
      32           24 :     ])
      33           22 :             .then(([[is_running], [active_result], [recommended]]) => {
      34            2 :                 const active = is_running ? active_result : "none";
      35           22 :                 return ({ state: "running", active, recommended });
      36           22 :             })
      37            0 :             .catch((ex) => {
      38            0 :                 if (!tunedService.exists)
      39            0 :                     return ({ state: "not-installed" });
      40            0 :                 else if (tunedService.state != "running")
      41            0 :                     return ({ state: "not-running" });
      42              :                 else
      43            0 :                     return Promise.reject(ex);
      44            0 :             });
      45           24 : }
      46              : 
      47           35 : export const TunedPerformanceProfile = () => {
      48           35 :     const Dialogs = useDialogs();
      49           35 :     const [btnText, setBtnText] = useState();
      50           35 :     const [state, setState] = useState();
      51           35 :     const [status, setStatus] = useState();
      52           35 :     const oldServiceState = useRef(null);
      53              : 
      54           35 :     const tunedService = useInit(() => service.proxy("tuned.service"));
      55              : 
      56           24 :     async function update() {
      57           24 :         const tuned = cockpit.dbus("com.redhat.tuned", { superuser: "try" });
      58           24 :         try {
      59           24 :             const { state, active, recommended } = await poll_tuned_state(tuned, tunedService);
      60           23 :             let status;
      61              : 
      62           23 :             if (state == "not-installed")
      63            4 :                 status = _("Tuned is not available");
      64           23 :             else if (state == "not-running")
      65            4 :                 status = _("Tuned is not running");
      66           23 :             else if (active == "none")
      67            4 :                 status = _("Tuned is off");
      68           23 :             else if (active == recommended)
      69            4 :                 status = _("This system is using the recommended profile");
      70              :             else
      71            4 :                 status = _("This system is using a custom profile");
      72            4 :             setBtnText(state == "running" ? active : _("none"));
      73           24 :             setState(state);
      74           24 :             setStatus(status);
      75            4 :         } catch (ex) {
      76            4 :             console.warn("failed to poll tuned", ex);
      77              : 
      78            4 :             setBtnText("error");
      79            4 :             setStatus(_("Communication with tuned has failed"));
      80            4 :         }
      81           23 :         tuned.close();
      82           24 :     }
      83              : 
      84           35 :     useEvent(superuser, "reconnect", update);
      85           24 :     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           24 :         if (oldServiceState.current !== tunedService.state) {
      89           24 :             oldServiceState.current = tunedService.state;
      90           24 :             update();
      91           24 :         }
      92           24 :     });
      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           35 :     return (
     101           35 :         <Tooltip id="tuned-status-tooltip" content={status}>
     102           35 :             <Button id="tuned-status-button"
     103           35 :                     isAriaDisabled={btnText == "error" || state == "not-installed" || !superuser.allowed}
     104           35 :                     isInline
     105           35 :                     onClick={showDialog}
     106           35 :                     variant='link'>
     107           35 :                 {btnText}
     108           35 :             </Button>
     109           35 :         </Tooltip>
     110              :     );
     111           35 : };
     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 : };
        

Generated by: LCOV version 2.0-1