Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions files.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const info = {
"playground/notifications-receiver.js",
"playground/journal.jsx",
"playground/remote.tsx",
"playground/dnf5daemon.js",

"selinux/selinux.js",
"shell/shell.jsx",
Expand Down Expand Up @@ -117,6 +118,7 @@ const info = {
"playground/notifications-receiver.html",
"playground/journal.html",
"playground/remote.html",
"playground/dnf5daemon.html",

"selinux/index.html",

Expand Down
150 changes: 150 additions & 0 deletions pkg/lib/dnf5daemon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2025 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see <https://www.gnu.org/licenses/>.
*/

import cockpit from "cockpit";
import { superuser } from 'superuser';

const _ = cockpit.gettext;

let _dbus_client = null;

/**
* Get dnf5daemon D-Bus client
*
* This will get lazily initialized and re-initialized after dnf5daemon
* disconnects (due to a crash or idle timeout).
*/
function dbus_client() {
if (_dbus_client === null) {
_dbus_client = cockpit.dbus("org.rpm.dnf.v0", { superuser: "try", track: true });
_dbus_client.addEventListener("close", () => {
console.log("dnf5daemon went away from D-Bus");
_dbus_client = null;
});
}

return _dbus_client;
}

// Reconnect when privileges change
superuser.addEventListener("changed", () => { _dbus_client = null });

/**
* Call a dnf5daemon method
*/
export function call(objectPath, iface, method, args, opts) {
return dbus_client().call(objectPath, iface, method, args, opts);
}

/**
* Figure out whether dnf5daemon is available and usable
*/
export function detect() {
function dbus_detect() {
return call("/org/rpm/dnf/v0", "org.freedesktop.DBus.Peer",
"Ping", [])
.then(() => true,
() => false);
}

return cockpit.spawn(["findmnt", "-T", "/usr", "-n", "-o", "VFS-OPTIONS"])
.then(options => {
if (options.split(",").indexOf("ro") >= 0)
return false;
else
return dbus_detect();
})
.catch(dbus_detect);
}

// TODO: close_session needs to be handled
// handle
// Cannot open new session - maximal number of simultaneously opened sessions achieved
export async function check_missing_packages(names, progress_cb) {
const data = {
missing_ids: [],
missing_names: [],
unavailable_names: [],
};

if (names.length === 0)
return data;

function open_session() {
return call("/org/rpm/dnf/v0", "org.rpm.dnf.v0.SessionManager",
"open_session", [{}]);
}

function close_session(session) {
return call("/org/rpm/dnf/v0", "org.rpm.dnf.v0.SessionManager",
"close_session", [session]);
}

async function refresh(session) {
// refresh dnf5daemon state
await call(session, "org.rpm.dnf.v0.Base", "read_all_repos", []);
const resolve_results = await call(session, "org.rpm.dnf.v0.Goal", "resolve", [{}]);
console.log(resolve_results);
const transaction_results = await call(session, "org.rpm.dnf.v0.Goal", "do_transaction", [{}]);
console.log(transaction_results);
}

async function list(session) {
const package_attrs = ["name", "version", "release", "arch"];

const result = await call(session, "org.rpm.dnf.v0.rpm.Rpm", "list", [{ package_attrs: { t: 'as', v: package_attrs }, scope: { t: 's', v: "installed" }, patterns: { t: 'as', v: ['bash'] } }]);
console.log("list result", result);
for (const [pkg] of result) {
console.log("pkg", pkg);
data.missing_ids.push(pkg.id.v);
data.missing_names.push(pkg.name.v);
}
}

function signal_emitted(path, iface, signal, args) {
console.log("signal_emitted", path, iface, signal, args);
if (progress_cb)
progress_cb(signal);
}

// TODO: decorator / helper for opening a session?
let session;
const client = dbus_client();
const subscription = client.subscribe({}, signal_emitted);

try {
[session] = await open_session();
console.log(session);
await refresh(session);
await list(session);

await close_session(session);
} catch (err) {
console.warn(err);
if (session)
await close_session(session);
}

subscription.remove();
console.log(subscription);
// HACK: close the client so subscribe matches are actually dropped.
client.close();

return data;
}
14 changes: 14 additions & 0 deletions pkg/playground/dnf5daemon.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>dnf5daemon</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="dnf5daemon.css" type="text/css" rel="stylesheet" />
<script src="../base1/cockpit.js"></script>
<script src="dnf5daemon.js"></script>
</head>
<body class="pf-v6-m-tabular-nums">
<div id="dnf5daemon"></div>
</body>
</html>
94 changes: 94 additions & 0 deletions pkg/playground/dnf5daemon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import cockpit from "cockpit";

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused import cockpit.
import React from 'react';
import { createRoot } from "react-dom/client";
import 'cockpit-dark-theme'; // once per page

import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
import { Card, CardBody, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
import { Content } from "@patternfly/react-core/dist/esm/components/Content/index.js";
import { List, ListItem } from "@patternfly/react-core/dist/esm/components/List/index.js";
import { Page, PageSection } from "@patternfly/react-core/dist/esm/components/Page/index.js";
import {
CheckIcon,
ExclamationCircleIcon,
} from '@patternfly/react-icons';

import * as PK from "../lib/dnf5daemon.js";

import '../lib/patternfly/patternfly-6-cockpit.scss';
import "../../node_modules/@patternfly/patternfly/components/Page/page.css";

const DnfPage = ({ exists }) => {
const [isRefreshing, setRefreshing] = React.useState(false);
const [events, setEvents] = React.useState([]);
const [refreshData, setRefreshData] = React.useState({});

const progressCallback = (signal_text) => {
setEvents(prevState => [...prevState, signal_text]);
};

const refreshDatabase = async () => {
setEvents([]);
setRefreshData({});
setRefreshing(true);
const data = await PK.check_missing_packages("bash", progressCallback);
console.log(data);
setRefreshData(data);
console.log(data);
setRefreshing(false);
};

const cancelRefreshDatabase = () => {
setRefreshing(false);
};
console.log("events", events);

return (
<Page id="accounts" className='no-masthead-sidebar'>
<PageSection hasBodyWrapper={false}>
<Content>
<h1>dnf5daemon example</h1>
<p>daemon available?: { exists ? <CheckIcon /> : <ExclamationCircleIcon /> }</p>
<Card>
<CardTitle>Refresh database</CardTitle>
<CardBody>
<Button variant="primary" onClick={() => refreshDatabase()} isLoading={isRefreshing}>Refresh</Button>
{isRefreshing && <Button variant="secondary" isDanger onClick={() => cancelRefreshDatabase()}>Cancel refresh</Button>}

</CardBody>
{refreshData.missing_names && refreshData.missing_names.length !== 0 &&
<CardBody>
<h4>Missing packages</h4>
<List isBordered>
{refreshData.missing_names.map((name, idx) => {
return <ListItem key={idx} icon={<CheckIcon />}>{name}</ListItem>;
})}
</List>
</CardBody>

}
{events.length !== 0 &&
<CardBody>
<h4>Events</h4>
<List isBordered>
{events.map((evt, idx) => {
return <ListItem key={idx} icon={<CheckIcon />}>{evt}</ListItem>;
})}
</List>
</CardBody>
}
</Card>
</Content>
</PageSection>
</Page>

);
};

document.addEventListener("DOMContentLoaded", async () => {
const dnf5daemon_exists = await PK.detect();
console.log("dnf5daemon", dnf5daemon_exists);

const root = createRoot(document.getElementById("dnf5daemon"));
root.render(<DnfPage exists={dnf5daemon_exists} />);
});
3 changes: 3 additions & 0 deletions pkg/playground/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@
},
"remote": {
"label": "Remote channel"
},
"dnf5daemon": {
"label": "dnf5daemon example"
}
},
"preload": [ "preloaded" ],
Expand Down
Loading