LCOV - code coverage report
Current view: top level - pkg/lib/_internal - dnf5daemon.ts Coverage Total Hit
Test: cockpit Lines: 63.0 % 451 284
Test Date: 2026-07-17 14:32:59

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2025 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5              : 
       6          282 : 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          282 : 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           12 : function dbus_client() {
      19           12 :     if (_dbus_client === null) {
      20           12 :         _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           12 :     }
      26              : 
      27           12 :     return _dbus_client;
      28           12 : }
      29              : 
      30              : // Reconnect when privileges change
      31          277 : superuser.addEventListener("changed", () => {
      32          277 :     if (_dbus_client)
      33           53 :         _dbus_client.close();
      34          277 :     _dbus_client = null;
      35          277 : });
      36              : 
      37           12 : async function open_session(): Promise<string> {
      38           12 :     const [session] = await call("/org/rpm/dnf/v0", "org.rpm.dnf.v0.SessionManager",
      39           12 :                                  "open_session", [{}]) as string[];
      40           12 :     return session;
      41           12 : }
      42              : 
      43           11 : function close_session(session: string) {
      44           11 :     return call("/org/rpm/dnf/v0", "org.rpm.dnf.v0.SessionManager",
      45           11 :                 "close_session", [session]);
      46           11 : }
      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           12 : function call(objectPath: string, iface: string, method: string, args?: unknown[], opts?: cockpit.DBusCallOptions) {
     142           12 :     return dbus_client().call(objectPath, iface, method, args, opts);
     143           12 : }
     144              : 
     145          282 : export class Dnf5DaemonManager implements PackageManager {
     146              :     name: string;
     147              : 
     148           22 :     constructor() {
     149           22 :         this.name = 'dnf5daemon';
     150           22 :     }
     151              : 
     152           12 :     private async with_session(executor: (session: string) => Promise<void>, signal_handler?: SignalCB): Promise<void> {
     153           12 :         let session: string | null = null;
     154           12 :         let subscription: { remove: () => void } | null = null;
     155              : 
     156           12 :         const client = dbus_client();
     157           12 :         if (signal_handler)
     158           10 :             subscription = client.subscribe({}, signal_handler);
     159              : 
     160           12 :         try {
     161           12 :             session = await open_session();
     162           11 :             await executor(session);
     163           11 :         } finally {
     164           11 :             if (session)
     165           11 :                 await close_session(session);
     166           11 :             if (subscription)
     167           10 :                 subscription.remove();
     168           11 :         }
     169           12 :     }
     170              : 
     171            7 :     async check_missing_packages(pkgnames: string[], progress_cb?: ProgressCB): Promise<MissingPackages> {
     172            7 :         const data: MissingPackages = {
     173            7 :             extra_names: [],
     174            7 :             missing_ids: [],
     175            7 :             missing_names: [],
     176            7 :             unavailable_names: [],
     177            7 :             remove_names: [],
     178            7 :             download_size: 0,
     179            7 :         };
     180              : 
     181            7 :         if (pkgnames.length === 0)
     182            0 :             return data;
     183              : 
     184            7 :         async function resolve(session: string) {
     185            7 :             const installed_names = new Set();
     186            7 :             const seen_names = new Set();
     187            7 :             const package_attrs = ["name", "is_installed"];
     188              : 
     189            7 :             const [results] = await call(session, "org.rpm.dnf.v0.rpm.Rpm", "list", [
     190            7 :                 {
     191            7 :                     package_attrs: { t: 'as', v: package_attrs },
     192            7 :                     scope: { t: 's', v: "all" },
     193            7 :                     patterns: { t: 'as', v: pkgnames }
     194            7 :                 }
     195            7 :             ]) as ListPackage[][];
     196              : 
     197            7 :             for (const pkg of results) {
     198            7 :                 if (seen_names.has(pkg.name.v))
     199            7 :                     continue;
     200              : 
     201            1 :                 if (pkg.is_installed.v) {
     202            1 :                     installed_names.add(pkg.name.v);
     203            0 :                 } else {
     204            6 :                     data.missing_ids.push(pkg.name.v);
     205            6 :                     data.missing_names.push(pkg.name.v);
     206            6 :                 }
     207              : 
     208            7 :                 seen_names.add(pkg.name.v);
     209            7 :             }
     210              : 
     211            7 :             pkgnames.forEach((name: string) => {
     212            6 :                 if (!installed_names.has(name) && data.missing_names.indexOf(name) == -1)
     213            2 :                     data.unavailable_names.push(name);
     214            7 :             });
     215            7 :         }
     216              : 
     217            7 :         async function simulate(session: string) {
     218            2 :             if (data.missing_ids.length === 0 || data.unavailable_names.length > 0) {
     219            3 :                 return null;
     220            3 :             }
     221              : 
     222            6 :             await call(session, "org.rpm.dnf.v0.rpm.Rpm", "install", [pkgnames, {}]);
     223            6 :             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            6 :             for (const transaction_item of transaction_items) {
     230            6 :                 const [object_type, action, reason, _transaction_item_attributes, pkg] = transaction_item;
     231            6 :                 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            6 :                 data.download_size += pkg.download_size.v;
     239              : 
     240            2 :                 if (reason == "Dependency") {
     241            2 :                     if (data.missing_names.indexOf(name) == -1)
     242            2 :                         data.extra_names.push(name);
     243            2 :                 }
     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            6 :             }
     250              : 
     251              :             // Call reset() as we don't intend to complete the transaction using `do_transaction`
     252            6 :             await call(session, "org.rpm.dnf.v0.Goal", "reset", []);
     253            7 :         }
     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            7 :         await this.refresh(false);
     267            7 :         await this.with_session(async (session) => {
     268            7 :             try {
     269            7 :                 await resolve(session);
     270            7 :                 await simulate(session);
     271            1 :             } catch (err) {
     272            1 :                 console.warn("check_missing_packages", err);
     273            1 :                 throw err;
     274            1 :             }
     275            7 :         }, signal_emitted);
     276              : 
     277            7 :         return data;
     278            7 :     }
     279              : 
     280            7 :     async install_missing_packages(data: MissingPackages, progress_cb?: InstallProgressCB): Promise<void> {
     281            7 :         if (!data || data.missing_ids.length === 0)
     282            7 :             return;
     283              : 
     284            6 :         let last_info: number;
     285            6 :         let last_progress = 0;
     286            6 :         let last_name: string;
     287            6 :         let total_packages: number;
     288              : 
     289            6 :         function signal_emitted(_path: string, _iface: string, signal: string, args: unknown[]) {
     290            6 :             switch (signal) {
     291              :             // download_add_new(o session_object_path, s download_id, s description, x total_to_download)
     292            6 :             case 'download_add_new': {
     293            6 :                 last_info = InstallProgressType.DOWNLOADING;
     294            6 :                 last_name = args[2] as string;
     295            6 :                 break;
     296            6 :             }
     297              :             // download_progress(o session_object_path, s download_id, x total_to_download, x downloaded)
     298            6 :             case 'download_progress': {
     299            6 :                 last_info = InstallProgressType.DOWNLOADING;
     300            6 :                 break;
     301            6 :             }
     302              :             // download_end(o session_object_path, s download_id, u transfer_status, s message)
     303            6 :             case 'download_end':
     304            6 :                 last_info = 0;
     305            6 :                 last_name = "";
     306            6 :                 break;
     307              :             // transaction_before_begin(o session_object_path, t total)
     308            6 :             case 'transaction_before_begin':
     309            6 :                 [, total_packages] = args as [string, number];
     310            6 :                 last_info = InstallProgressType.INSTALLING;
     311            6 :                 break;
     312              :             // transaction_elem_progress(o session_object_path, s nevra, t processed, t total)
     313            6 :             case 'transaction_elem_progress': {
     314            6 :                 let processed = 0;
     315            6 :                 [, last_name, processed,] = args as [string, string, number, number];
     316            6 :                 last_progress = processed / total_packages * 100;
     317            6 :                 break;
     318            6 :             }
     319            6 :             }
     320              : 
     321            6 :             if (progress_cb)
     322            6 :                 progress_cb({
     323            6 :                     cancel: null,
     324            6 :                     info: last_info,
     325            6 :                     package: last_name,
     326            6 :                     percentage: last_progress,
     327            6 :                     waiting: false,
     328            6 :                 });
     329            6 :         }
     330              : 
     331            6 :         await this.with_session(async (session) => {
     332            6 :             try {
     333            6 :                 await call(session, "org.rpm.dnf.v0.rpm.Rpm", "install", [data.missing_names, {}]);
     334            6 :                 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            6 :                 await call(session, "org.rpm.dnf.v0.Goal", "do_transaction", [{}]);
     341            0 :             } catch (err) {
     342            0 :                 console.warn("install error", err);
     343            0 :             }
     344            6 :         }, signal_emitted);
     345            7 :     }
     346              : 
     347           10 :     async refresh(_force: boolean, _progress_cb?: ProgressCB): Promise<void> {
     348           10 :         await this.with_session(async (session) => {
     349              :             // refresh dnf5daemon state
     350           10 :             await call(session, "org.rpm.dnf.v0.Base", "read_all_repos", []);
     351           10 :             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           10 :             await call(session, "org.rpm.dnf.v0.Goal", "do_transaction", [{}]);
     358           10 :         });
     359           10 :     }
     360              : 
     361            5 :     async is_installed(pkgnames: string[]): Promise<boolean> {
     362            5 :         const uninstalled = new Set(pkgnames);
     363              : 
     364            5 :         await this.with_session(async (session) => {
     365            5 :             const package_attrs = ["name", "is_installed"];
     366              : 
     367            5 :             const [results] = await call(session, "org.rpm.dnf.v0.rpm.Rpm", "list", [
     368            5 :                 {
     369            5 :                     package_attrs: { t: 'as', v: package_attrs },
     370            5 :                     scope: { t: 's', v: "all" },
     371            5 :                     patterns: { t: 'as', v: pkgnames }
     372            5 :                 }
     373            5 :             ]) 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            5 :         });
     381              : 
     382            4 :         return uninstalled.size === 0;
     383            5 :     }
     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          282 : }
        

Generated by: LCOV version 2.0-1