Line data Source code
1 : /*
2 : * Copyright (C) 2025 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 :
6 55 : import cockpit from "cockpit";
7 : import { superuser } from 'superuser';
8 : import { InstallProgressCB, MissingPackages, PackageManager, ProgressCB, ResolveError, InstallProgressType, UpdateDetail, Update, Severity, History } from './packagemanager-abstract';
9 :
10 55 : let _dbus_client: cockpit.DBusClient | null = null;
11 :
12 : /**
13 : * Get dnf5daemon D-Bus client
14 : *
15 : * This will get lazily initialized and re-initialized after dnf5daemon
16 : * disconnects (due to a crash or idle timeout).
17 : */
18 8 : function dbus_client() {
19 8 : if (_dbus_client === null) {
20 8 : _dbus_client = cockpit.dbus("org.rpm.dnf.v0", { superuser: "try", track: true });
21 0 : _dbus_client.addEventListener("close", () => {
22 0 : console.warn("dnf5daemon went away from D-Bus");
23 0 : _dbus_client = null;
24 0 : });
25 8 : }
26 :
27 8 : return _dbus_client;
28 8 : }
29 :
30 : // Reconnect when privileges change
31 51 : superuser.addEventListener("changed", () => {
32 51 : if (_dbus_client)
33 9 : _dbus_client.close();
34 51 : _dbus_client = null;
35 51 : });
36 :
37 8 : async function open_session(): Promise<string> {
38 8 : const [session] = await call("/org/rpm/dnf/v0", "org.rpm.dnf.v0.SessionManager",
39 8 : "open_session", [{}]) as string[];
40 8 : return session;
41 8 : }
42 :
43 8 : function close_session(session: string) {
44 8 : return call("/org/rpm/dnf/v0", "org.rpm.dnf.v0.SessionManager",
45 8 : "close_session", [session]);
46 8 : }
47 :
48 : interface ListPackage {
49 : arch: { t: "s", v: string }
50 : download_size: { t: "t", "v": number }
51 : id: { t: "i"; v: number };
52 : is_installed: { t: "b"; v: boolean };
53 : name: { t: "s"; v: string };
54 : release: { t: "s"; v: string };
55 : version: { t: "s"; v: string };
56 : nevra?: { t: "s"; v: string };
57 : }
58 :
59 : interface CollectionPackage {
60 : // arch
61 : a: { t: "s"; v: string };
62 : // epoch
63 : e: { t: "s"; v: string };
64 : // name
65 : n: { t: "s"; v: string };
66 : nevra: { t: "s"; v: string };
67 : // release
68 : r: { t: "s"; v: string };
69 : // version
70 : v: { t: "s"; v: string };
71 : }
72 :
73 : type AdvisoryType = "bugfix" | "enhancement" | "security" | "newpackage";
74 :
75 : interface ListAdvisory {
76 : advisoryid: { t: "i", v: number };
77 : name: { t: "s", v: string };
78 : description: { t: "s", v: string };
79 : status: { t: "s", v: string };
80 : severity: { t: "s", v: Severity };
81 : type: { t: "s", v: AdvisoryType };
82 : collections: { t: "aa{sv}", v: [ { packages: { t: "aa{sv}", v: CollectionPackage[] } } ] }
83 : // Array of id, type, title, url
84 : references: { t: "a(ssss)", v: [string, string, string, string] }
85 : }
86 :
87 : interface ResolvePackage {
88 : arch: { t: "s"; v: string };
89 : download_size: { t: "t"; v: number };
90 : epoch: { t: "s"; v: string };
91 : evr: { t: "s"; v: string };
92 : from_repo_id: { t: "s"; v: string };
93 : id: { t: "i"; v: number };
94 : install_size: { t: "t"; v: number };
95 : name: { t: "s"; v: string };
96 : reason: { t: "s"; v: string };
97 : release: { t: "s"; v: string };
98 : repo_id: { t: "s"; v: string };
99 : version: { t: "s"; v: string };
100 : }
101 :
102 : interface TransactionProblem {
103 : action: { t: "u", v: number };
104 : additional_data: { t: "as", "v": string[] };
105 : goal_job_settings: { t: "a{vs}", "v": { to_repo_ids: { t: "as", v: string[] } } };
106 : problem: { t: "u", v: number };
107 : spec: { t: "s", v: "appstream-data" };
108 : }
109 :
110 : interface RepoListResult {
111 : cache_updated: { t: "x", "v": number };
112 : }
113 :
114 : enum GoalProblem {
115 : ALREADY_INSTALLED = (1 << 12)
116 : }
117 :
118 : // TransactionItemType
119 : type object_type = "Package" | "Group" | "Environment" | "Module" | "Skipped";
120 : // TransactionItemAction
121 : type action = "Install" | "Upgrade" | "Downgrade" | "Reinstall" | "Remove" | "Replaced" | "Reset" | "Enable" | "Disable" | "Reason Change" | "Switch"
122 : // TransactionItemReason
123 : type reason = "User" | "Dependency" | "Clean" | "Group" | "None" | "Weak Dependency" | "External User"
124 : type TransactionItem = [
125 : object_type,
126 : action,
127 : reason,
128 : unknown,
129 : ResolvePackage
130 : ]
131 :
132 : type InstallResolveResult = [TransactionItem[], number]
133 : type RemoveResolveResult = [TransactionItem[], number]
134 : type UpgradeResolveResult = [TransactionItem[], number]
135 :
136 : type SignalCB = (_path: string, _iface: string, signal: string, args: unknown[]) => void
137 :
138 : /**
139 : * Call a dnf5daemon method
140 : */
141 8 : function call(objectPath: string, iface: string, method: string, args?: unknown[], opts?: cockpit.DBusCallOptions) {
142 8 : return dbus_client().call(objectPath, iface, method, args, opts);
143 8 : }
144 :
145 55 : export class Dnf5DaemonManager implements PackageManager {
146 : name: string;
147 :
148 18 : constructor() {
149 18 : this.name = 'dnf5daemon';
150 18 : }
151 :
152 8 : private async with_session(executor: (session: string) => Promise<void>, signal_handler?: SignalCB): Promise<void> {
153 8 : let session: string | null = null;
154 8 : let subscription: { remove: () => void } | null = null;
155 :
156 8 : const client = dbus_client();
157 8 : if (signal_handler)
158 7 : subscription = client.subscribe({}, signal_handler);
159 :
160 8 : try {
161 8 : session = await open_session();
162 8 : await executor(session);
163 8 : } finally {
164 8 : if (session)
165 8 : await close_session(session);
166 8 : if (subscription)
167 7 : subscription.remove();
168 8 : }
169 8 : }
170 :
171 4 : async check_missing_packages(pkgnames: string[], progress_cb?: ProgressCB): Promise<MissingPackages> {
172 4 : const data: MissingPackages = {
173 4 : extra_names: [],
174 4 : missing_ids: [],
175 4 : missing_names: [],
176 4 : unavailable_names: [],
177 4 : remove_names: [],
178 4 : download_size: 0,
179 4 : };
180 :
181 4 : if (pkgnames.length === 0)
182 0 : return data;
183 :
184 4 : async function resolve(session: string) {
185 4 : const installed_names = new Set();
186 4 : const seen_names = new Set();
187 4 : const package_attrs = ["name", "is_installed"];
188 :
189 4 : const [results] = await call(session, "org.rpm.dnf.v0.rpm.Rpm", "list", [
190 4 : {
191 4 : package_attrs: { t: 'as', v: package_attrs },
192 4 : scope: { t: 's', v: "all" },
193 4 : patterns: { t: 'as', v: pkgnames }
194 4 : }
195 4 : ]) as ListPackage[][];
196 :
197 4 : for (const pkg of results) {
198 4 : if (seen_names.has(pkg.name.v))
199 4 : continue;
200 :
201 1 : if (pkg.is_installed.v) {
202 1 : installed_names.add(pkg.name.v);
203 0 : } else {
204 3 : data.missing_ids.push(pkg.name.v);
205 3 : data.missing_names.push(pkg.name.v);
206 3 : }
207 :
208 4 : seen_names.add(pkg.name.v);
209 4 : }
210 :
211 4 : pkgnames.forEach((name: string) => {
212 3 : if (!installed_names.has(name) && data.missing_names.indexOf(name) == -1)
213 1 : data.unavailable_names.push(name);
214 4 : });
215 4 : }
216 :
217 4 : async function simulate(session: string) {
218 1 : if (data.missing_ids.length === 0 || data.unavailable_names.length > 0) {
219 2 : return null;
220 2 : }
221 :
222 3 : await call(session, "org.rpm.dnf.v0.rpm.Rpm", "install", [pkgnames, {}]);
223 3 : const [transaction_items, result] = await call(session, "org.rpm.dnf.v0.Goal", "resolve", [{}]) as InstallResolveResult;
224 1 : if (result !== 0) {
225 1 : const [problem] = await call(session, "org.rpm.dnf.v0.Goal", "get_transaction_problems_string", []);
226 1 : throw new ResolveError(`Resolving install failed with result=${result}. ${problem}`);
227 1 : }
228 :
229 3 : for (const transaction_item of transaction_items) {
230 3 : const [object_type, action, reason, _transaction_item_attributes, pkg] = transaction_item;
231 3 : const name = pkg.name.v;
232 :
233 0 : if (object_type !== "Package") {
234 0 : console.error(`Simulated install for ${pkgnames} resolved an unexpected object_type=${object_type}`);
235 0 : continue;
236 0 : }
237 :
238 3 : data.download_size += pkg.download_size.v;
239 :
240 1 : if (reason == "Dependency") {
241 1 : if (data.missing_names.indexOf(name) == -1)
242 1 : data.extra_names.push(name);
243 1 : }
244 :
245 1 : if (action == "Replaced") {
246 1 : if (data.remove_names.indexOf(name) == -1)
247 1 : data.remove_names.push(name);
248 1 : }
249 3 : }
250 :
251 : // Call reset() as we don't intend to complete the transaction using `do_transaction`
252 3 : await call(session, "org.rpm.dnf.v0.Goal", "reset", []);
253 4 : }
254 :
255 0 : function signal_emitted(_path: string, _iface: string, _signal: string, _args: unknown[]) {
256 : // HACK: dnf5daemon doesn't give us an useful progress indicator so the progress percentage is hardcoded to 0.
257 0 : if (progress_cb) {
258 0 : progress_cb({
259 0 : waiting: false,
260 0 : percentage: 0,
261 0 : cancel: null,
262 0 : });
263 0 : }
264 0 : }
265 :
266 4 : await this.refresh(false);
267 4 : await this.with_session(async (session) => {
268 4 : try {
269 4 : await resolve(session);
270 4 : await simulate(session);
271 1 : } catch (err) {
272 1 : console.warn("check_missing_packages", err);
273 1 : throw err;
274 1 : }
275 4 : }, signal_emitted);
276 :
277 4 : return data;
278 4 : }
279 :
280 4 : async install_missing_packages(data: MissingPackages, progress_cb?: InstallProgressCB): Promise<void> {
281 4 : if (!data || data.missing_ids.length === 0)
282 4 : return;
283 :
284 3 : let last_info: number;
285 3 : let last_progress = 0;
286 3 : let last_name: string;
287 3 : let total_packages: number;
288 :
289 3 : function signal_emitted(_path: string, _iface: string, signal: string, args: unknown[]) {
290 3 : switch (signal) {
291 : // download_add_new(o session_object_path, s download_id, s description, x total_to_download)
292 3 : case 'download_add_new': {
293 3 : last_info = InstallProgressType.DOWNLOADING;
294 3 : last_name = args[2] as string;
295 3 : break;
296 3 : }
297 : // download_progress(o session_object_path, s download_id, x total_to_download, x downloaded)
298 3 : case 'download_progress': {
299 3 : last_info = InstallProgressType.DOWNLOADING;
300 3 : break;
301 3 : }
302 : // download_end(o session_object_path, s download_id, u transfer_status, s message)
303 3 : case 'download_end':
304 3 : last_info = 0;
305 3 : last_name = "";
306 3 : break;
307 : // transaction_before_begin(o session_object_path, t total)
308 3 : case 'transaction_before_begin':
309 3 : [, total_packages] = args as [string, number];
310 3 : last_info = InstallProgressType.INSTALLING;
311 3 : break;
312 : // transaction_elem_progress(o session_object_path, s nevra, t processed, t total)
313 3 : case 'transaction_elem_progress': {
314 3 : let processed = 0;
315 3 : [, last_name, processed,] = args as [string, string, number, number];
316 3 : last_progress = processed / total_packages * 100;
317 3 : break;
318 3 : }
319 3 : }
320 :
321 3 : if (progress_cb)
322 3 : progress_cb({
323 3 : cancel: null,
324 3 : info: last_info,
325 3 : package: last_name,
326 3 : percentage: last_progress,
327 3 : waiting: false,
328 3 : });
329 3 : }
330 :
331 3 : await this.with_session(async (session) => {
332 3 : try {
333 3 : await call(session, "org.rpm.dnf.v0.rpm.Rpm", "install", [data.missing_names, {}]);
334 3 : const [, resolve_result] = await call(session, "org.rpm.dnf.v0.Goal", "resolve", [{}]);
335 :
336 0 : if (resolve_result !== 0) {
337 0 : const [problem] = await call(session, "org.rpm.dnf.v0.Goal", "get_transaction_problems_string", []);
338 0 : throw new ResolveError(`Resolving install failed with result=${resolve_result} ${problem}`);
339 0 : }
340 3 : await call(session, "org.rpm.dnf.v0.Goal", "do_transaction", [{}]);
341 0 : } catch (err) {
342 0 : console.warn("install error", err);
343 0 : }
344 3 : }, signal_emitted);
345 4 : }
346 :
347 7 : async refresh(_force: boolean, _progress_cb?: ProgressCB): Promise<void> {
348 7 : await this.with_session(async (session) => {
349 : // refresh dnf5daemon state
350 7 : await call(session, "org.rpm.dnf.v0.Base", "read_all_repos", []);
351 7 : const [, resolve_result] = await call(session, "org.rpm.dnf.v0.Goal", "resolve", [{}]) as [unknown[], number];
352 0 : if (resolve_result !== 0) {
353 0 : const [problem] = await call(session, "org.rpm.dnf.v0.Goal", "get_transaction_problems_string", []);
354 0 : throw new ResolveError(`Resolving read_all_repos failed with result=${resolve_result} - ${problem}`);
355 0 : }
356 :
357 7 : await call(session, "org.rpm.dnf.v0.Goal", "do_transaction", [{}]);
358 7 : });
359 7 : }
360 :
361 4 : async is_installed(pkgnames: string[]): Promise<boolean> {
362 4 : const uninstalled = new Set(pkgnames);
363 :
364 4 : await this.with_session(async (session) => {
365 4 : const package_attrs = ["name", "is_installed"];
366 :
367 4 : const [results] = await call(session, "org.rpm.dnf.v0.rpm.Rpm", "list", [
368 4 : {
369 4 : package_attrs: { t: 'as', v: package_attrs },
370 4 : scope: { t: 's', v: "all" },
371 4 : patterns: { t: 'as', v: pkgnames }
372 4 : }
373 4 : ]) as ListPackage[][];
374 :
375 4 : for (const pkg of results) {
376 4 : if (pkg.is_installed.v) {
377 4 : uninstalled.delete(pkg.name.v);
378 4 : }
379 4 : }
380 4 : });
381 :
382 4 : return uninstalled.size === 0;
383 4 : }
384 :
385 3 : async install_packages(pkgnames: string[], progress_cb?: ProgressCB): Promise<void> {
386 3 : let last_progress = 0;
387 3 : let total_packages: number;
388 :
389 3 : function signal_emitted(_path: string, _iface: string, signal: string, args: unknown[]) {
390 3 : switch (signal) {
391 3 : case 'transaction_before_begin':
392 3 : [, total_packages] = args as [string, number];
393 3 : break;
394 3 : case 'transaction_elem_progress': {
395 3 : const [, _last_name, processed,] = args as [string, string, number, number];
396 3 : last_progress = processed / total_packages * 100;
397 3 : break;
398 3 : }
399 3 : }
400 :
401 3 : if (progress_cb) {
402 3 : progress_cb({
403 3 : waiting: false,
404 3 : percentage: last_progress,
405 3 : cancel: null,
406 3 : });
407 3 : }
408 3 : }
409 :
410 3 : await this.with_session(async (session) => {
411 3 : await call(session, "org.rpm.dnf.v0.rpm.Rpm", "install", [pkgnames, {}]);
412 3 : const [_transaction_items, result] = await call(session, "org.rpm.dnf.v0.Goal", "resolve", [{}]) as InstallResolveResult;
413 0 : if (result !== 0) {
414 0 : const [problems] = await call(session, "org.rpm.dnf.v0.Goal", "get_transaction_problems", []) as TransactionProblem[][];
415 0 : if (problems.every((p: TransactionProblem) => p.problem.v == GoalProblem.ALREADY_INSTALLED)) {
416 0 : await call(session, "org.rpm.dnf.v0.Goal", "reset", []);
417 0 : return;
418 0 : }
419 :
420 0 : const [problem] = await call(session, "org.rpm.dnf.v0.Goal", "get_transaction_problems_string", []);
421 0 : throw new ResolveError(`Resolving install failed with result=${result}. ${problem}`);
422 0 : }
423 3 : await call(session, "org.rpm.dnf.v0.Goal", "do_transaction", [{}]);
424 3 : }, signal_emitted);
425 3 : }
426 :
427 2 : async remove_packages(pkgnames: string[], progress_cb?: ProgressCB): Promise<void> {
428 2 : let last_progress = 0;
429 2 : let total_packages: number;
430 :
431 2 : function signal_emitted(_path: string, _iface: string, signal: string, args: unknown[]) {
432 2 : switch (signal) {
433 2 : case 'transaction_before_begin':
434 2 : [, total_packages] = args as [string, number];
435 2 : break;
436 2 : case 'transaction_elem_progress': {
437 2 : const [, _last_name, processed,] = args as [string, string, number, number];
438 2 : last_progress = processed / total_packages * 100;
439 2 : break;
440 2 : }
441 2 : }
442 :
443 2 : if (progress_cb) {
444 2 : progress_cb({
445 2 : waiting: false,
446 2 : percentage: last_progress,
447 2 : cancel: null,
448 2 : });
449 2 : }
450 2 : }
451 :
452 2 : await this.with_session(async (session) => {
453 2 : await call(session, "org.rpm.dnf.v0.rpm.Rpm", "remove", [pkgnames, {}]);
454 2 : const [_transaction_items, result] = await call(session, "org.rpm.dnf.v0.Goal", "resolve", [{}]) as RemoveResolveResult;
455 0 : if (result !== 0) {
456 0 : const [problem] = await call(session, "org.rpm.dnf.v0.Goal", "get_transaction_problems_string", []);
457 0 : throw new ResolveError(`Resolving remove failed with result=${result}. ${problem}`);
458 0 : }
459 2 : await call(session, "org.rpm.dnf.v0.Goal", "do_transaction", [{}]);
460 2 : }, signal_emitted);
461 2 : }
462 :
463 3 : async find_file_packages(files: string[], progress_cb?: ProgressCB): Promise<string[]> {
464 3 : const installed: string[] = [];
465 :
466 3 : await this.with_session(async (session) => {
467 3 : const package_attrs = ["name"];
468 :
469 3 : const [results] = await call(session, "org.rpm.dnf.v0.rpm.Rpm", "list", [
470 3 : {
471 3 : package_attrs: { t: 'as', v: package_attrs },
472 3 : scope: { t: 's', v: "installed" },
473 3 : patterns: { t: 'as', v: files },
474 3 : with_filenames: { t: 'b', v: true },
475 3 : }
476 3 : ]) as ListPackage[][];
477 3 : for (const result of results) {
478 3 : installed.push(result.name.v);
479 3 : }
480 :
481 : // HACK: no usable progress event, but we need to send something to make refresh work.
482 3 : if (progress_cb)
483 3 : progress_cb({ percentage: 100, waiting: false, cancel: null });
484 3 : });
485 :
486 3 : return installed;
487 3 : }
488 :
489 3 : async get_updates<T extends boolean>(detail: T, _progress_cb?: ProgressCB): Promise<T extends true ? UpdateDetail[] : Update[]> {
490 3 : const update_map = new Map<string, Update | UpdateDetail>();
491 3 : const package_attrs = ["name", "version", "arch", "epoch", "nevra"];
492 :
493 3 : await this.with_session(async (session) => {
494 3 : const pkgnames = [];
495 3 : const [results] = await call(session, "org.rpm.dnf.v0.rpm.Rpm", "list", [
496 3 : {
497 3 : package_attrs: { t: 'as', v: package_attrs },
498 3 : scope: { t: 's', v: "upgrades" },
499 3 : }
500 3 : ]) as ListPackage[][];
501 :
502 0 : for (const result of results) {
503 0 : cockpit.assert(result.nevra, "nevra not set");
504 :
505 0 : pkgnames.push(result.name.v);
506 0 : update_map.set(result.nevra.v, {
507 0 : id: result.name.v,
508 0 : name: result.name.v,
509 0 : arch: result.arch.v,
510 0 : version: result.version.v,
511 0 : });
512 0 : }
513 :
514 0 : if (detail) {
515 0 : const advisory_attrs = ["advisoryid", "name", "title", "type", "severity", "description", "references", "collections", "message"];
516 0 : const [advisories] = await call(session, "org.rpm.dnf.v0.Advisory", "list", [
517 0 : {
518 0 : advisory_attrs: { t: 'as', v: advisory_attrs },
519 0 : availability: { t: 's', v: "upgrades" },
520 0 : contains_pkgs: { t: 'as', v: pkgnames },
521 0 : }
522 0 : ]) as ListAdvisory[][];
523 :
524 0 : for (const advisory of advisories) {
525 0 : for (const collection of advisory.collections.v) {
526 0 : for (const pkg of collection.packages.v) {
527 0 : let update = update_map.get(pkg.nevra.v);
528 0 : if (!update)
529 0 : continue;
530 :
531 0 : const bug_urls: string[] = [];
532 0 : const cve_urls: string[] = [];
533 0 : const vendor_urls: string[] = [];
534 :
535 0 : for (const [, type, _title, url] of advisory.references.v) {
536 0 : switch (type) {
537 0 : case "bugzilla":
538 0 : bug_urls.push(url);
539 0 : break;
540 0 : case "cve":
541 0 : cve_urls.push(url);
542 0 : break;
543 0 : case "vendor":
544 0 : vendor_urls.push(url);
545 0 : break;
546 0 : }
547 0 : }
548 :
549 : // Map the advisory type to the severity which PackageKit uses
550 : // Critical == Security upate
551 : // Important == Bug fix
552 : // Moderate == Enhancement
553 0 : let severity = Severity.LOW;
554 0 : switch (advisory.type.v) {
555 0 : case "bugfix":
556 0 : severity = Severity.IMPORTANT;
557 0 : break;
558 0 : case "enhancement":
559 0 : severity = Severity.MODERATE;
560 0 : break;
561 0 : case "security":
562 0 : severity = Severity.CRITICAL;
563 0 : break;
564 0 : }
565 :
566 0 : update = {
567 0 : ...update,
568 0 : description: advisory.description.v,
569 0 : severity,
570 0 : markdown: false,
571 0 : bug_urls,
572 0 : cve_urls,
573 0 : vendor_urls,
574 0 : };
575 0 : update_map.set(pkg.nevra.v, update);
576 :
577 0 : break;
578 0 : }
579 0 : }
580 0 : }
581 0 : }
582 3 : });
583 :
584 3 : return Array.from(update_map.values()) as T extends true ? UpdateDetail[] : Update[];
585 3 : }
586 :
587 0 : async update_packages(updates: Update[] | UpdateDetail[], progress_cb?: ProgressCB, _transaction_path?: string): Promise<void> {
588 0 : const pkgnames = updates.map(update => update.id);
589 0 : let last_progress = 0;
590 0 : let total_packages: number;
591 :
592 0 : function signal_emitted(_path: string, _iface: string, signal: string, args: unknown[]) {
593 0 : switch (signal) {
594 0 : case 'transaction_before_begin':
595 0 : [, total_packages] = args as [string, number];
596 0 : break;
597 0 : case 'transaction_elem_progress': {
598 0 : const [, _last_name, processed,] = args as [string, string, number, number];
599 0 : last_progress = processed / total_packages * 100;
600 0 : break;
601 0 : }
602 0 : }
603 :
604 0 : if (progress_cb) {
605 0 : progress_cb({
606 0 : waiting: false,
607 0 : percentage: last_progress,
608 0 : cancel: null,
609 0 : });
610 0 : }
611 0 : }
612 :
613 0 : await this.with_session(async (session) => {
614 0 : await call(session, "org.rpm.dnf.v0.rpm.Rpm", "upgrade", [pkgnames, {}]);
615 0 : const [_transaction_items, result] = await call(session, "org.rpm.dnf.v0.Goal", "resolve", [{}]) as UpgradeResolveResult;
616 0 : if (result !== 0) {
617 0 : const [problem] = await call(session, "org.rpm.dnf.v0.Goal", "get_transaction_problems_string", []);
618 0 : throw new ResolveError(`Resolving upgrade failed with result=${result}. ${problem}`);
619 0 : }
620 0 : await call(session, "org.rpm.dnf.v0.Goal", "do_transaction", [{}]);
621 0 : }, signal_emitted);
622 0 : }
623 :
624 0 : async get_backend(): Promise<string> {
625 0 : return "dnf5";
626 0 : }
627 :
628 0 : async get_last_refresh_time(): Promise<number> {
629 0 : let last_time = 0;
630 0 : await this.with_session(async (session) => {
631 : // Bug? Does this need load repo? As the result was somehow -1 at one point.
632 0 : const [results] = await call(session, "org.rpm.dnf.v0.rpm.Repo", "list", [{ repo_attrs: { t: 'as', v: ['cache_updated'] } }]) as RepoListResult[][];
633 0 : for (const result of results) {
634 0 : if (result.cache_updated.v > last_time)
635 0 : last_time = result.cache_updated.v;
636 0 : }
637 0 : });
638 :
639 0 : const now = parseInt(await cockpit.spawn(["date", "+%s"]), 10);
640 0 : return now - last_time;
641 0 : }
642 :
643 0 : async get_history(): Promise<History[]> {
644 : // TODO: https://github.com/rpm-software-management/dnf5/issues/2538
645 0 : throw new Error("not implemented");
646 0 : return [];
647 0 : }
648 :
649 0 : async is_available(_pkgnames: string[]): Promise<boolean> {
650 : // TODO: requires RHEL to have dnf5 to run TestUpdatesSubscriptions.testNoUpdates
651 0 : throw new Error("not implemented");
652 0 : return false;
653 0 : }
654 55 : }
|