diff --git a/files.js b/files.js index 8e55ccd4e995..8a5239558bbd 100644 --- a/files.js +++ b/files.js @@ -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", @@ -117,6 +118,7 @@ const info = { "playground/notifications-receiver.html", "playground/journal.html", "playground/remote.html", + "playground/dnf5daemon.html", "selinux/index.html", diff --git a/pkg/lib/dnf5daemon.js b/pkg/lib/dnf5daemon.js new file mode 100644 index 000000000000..80ff2a86e82b --- /dev/null +++ b/pkg/lib/dnf5daemon.js @@ -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 . + */ + +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; +} diff --git a/pkg/playground/dnf5daemon.html b/pkg/playground/dnf5daemon.html new file mode 100644 index 000000000000..4c7dad8f9b4e --- /dev/null +++ b/pkg/playground/dnf5daemon.html @@ -0,0 +1,14 @@ + + + + + dnf5daemon + + + + + + +
+ + diff --git a/pkg/playground/dnf5daemon.js b/pkg/playground/dnf5daemon.js new file mode 100644 index 000000000000..b5afcbeed074 --- /dev/null +++ b/pkg/playground/dnf5daemon.js @@ -0,0 +1,94 @@ +import cockpit from "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 ( + + + +

dnf5daemon example

+

daemon available?: { exists ? : }

+ + Refresh database + + + {isRefreshing && } + + + {refreshData.missing_names && refreshData.missing_names.length !== 0 && + +

Missing packages

+ + {refreshData.missing_names.map((name, idx) => { + return }>{name}; + })} + +
+ + } + {events.length !== 0 && + +

Events

+ + {events.map((evt, idx) => { + return }>{evt}; + })} + +
+ } +
+
+
+
+ + ); +}; + +document.addEventListener("DOMContentLoaded", async () => { + const dnf5daemon_exists = await PK.detect(); + console.log("dnf5daemon", dnf5daemon_exists); + + const root = createRoot(document.getElementById("dnf5daemon")); + root.render(); +}); diff --git a/pkg/playground/manifest.json b/pkg/playground/manifest.json index 372902c9ec0d..bb51993c5f85 100644 --- a/pkg/playground/manifest.json +++ b/pkg/playground/manifest.json @@ -44,6 +44,9 @@ }, "remote": { "label": "Remote channel" + }, + "dnf5daemon": { + "label": "dnf5daemon example" } }, "preload": [ "preloaded" ],