LCOV - code coverage report
Current view: top level - pkg/lib/_internal - packagekit.ts Coverage Total Hit
Test: cockpit Lines: 26.0 % 231 60
Test Date: 2026-06-16 14:09:37

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2025 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5              : 
       6              : import { InstallProgressCB, MissingPackages, PackageManager, ProgressCB, InstallProgressType, UpdateDetail, Update, ProgressData, History } from './packagemanager-abstract';
       7              : import * as PK from "packagekit.js";
       8              : 
       9           55 : const InstallProgressMap = {
      10           55 :     [PK.Enum.INFO_DOWNLOADING]: InstallProgressType.DOWNLOADING,
      11           55 :     [PK.Enum.INFO_UPDATING]: InstallProgressType.UPDATING,
      12           55 :     [PK.Enum.INFO_REMOVING]: InstallProgressType.REMOVING,
      13           55 :     [PK.Enum.INFO_INSTALLING]: InstallProgressType.INSTALLING,
      14           55 :     [PK.Enum.INFO_REINSTALLING]: InstallProgressType.REINSTALLING,
      15           55 :     [PK.Enum.INFO_DOWNGRADING]: InstallProgressType.DOWNGRADING,
      16           55 : };
      17              : 
      18           55 : export class PackageKitManager implements PackageManager {
      19              :     name: string;
      20              : 
      21            1 :     constructor() {
      22            1 :         this.name = "packagekit";
      23            1 :     }
      24              : 
      25              :     /* Support for installing missing packages.
      26              :      *
      27              :      * First call check_missing_packages to determine whether something
      28              :      * needs to be installed, then call install_missing_packages to
      29              :      * actually install them.
      30              :      *
      31              :      * check_missing_packages resolves to an object that can be passed to
      32              :      * install_missing_packages.  It contains these fields:
      33              :      *
      34              :      * - missing_names:     Packages that were requested, are currently not installed,
      35              :      *                      and can be installed.
      36              :      *
      37              :      * - missing_ids:       The full PackageKit IDs corresponding to missing_names
      38              :      *
      39              :      * - unavailable_names: Packages that were requested, are currently not installed,
      40              :      *                      but can't be found in any repository.
      41              :      *
      42              :      * If unavailable_names is empty, a simulated installation of the missing packages
      43              :      * is done and the result also contains these fields:
      44              :      *
      45              :      * - extra_names:       Packages that need to be installed as dependencies of
      46              :      *                      missing_names.
      47              :      *
      48              :      * - remove_names:      Packages that need to be removed.
      49              :      *
      50              :      * - download_size:     Bytes that need to be downloaded.
      51              :      */
      52            0 :     async check_missing_packages(pkgnames: string[], progress_cb?: ProgressCB): Promise<MissingPackages> {
      53            0 :         const install_ids: string[] = [];
      54            0 :         const installed_names = new Set();
      55            0 :         const data: MissingPackages = {
      56            0 :             download_size: 0,
      57            0 :             missing_ids: [],
      58            0 :             missing_names: [],
      59            0 :             unavailable_names: [],
      60            0 :             extra_names: [],
      61            0 :             remove_names: [],
      62            0 :         };
      63              : 
      64            0 :         if (pkgnames.length === 0)
      65            0 :             return data;
      66              : 
      67            0 :         await this.refresh(false, progress_cb);
      68              : 
      69            0 :         await PK.cancellableTransaction("Resolve",
      70            0 :                                         [PK.Enum.FILTER_ARCH | PK.Enum.FILTER_NOT_SOURCE | PK.Enum.FILTER_NEWEST, pkgnames],
      71            0 :                                         progress_cb,
      72            0 :                                         {
      73            0 :                                             Package: (_info: number, package_id: string) => {
      74            0 :                                                 const parts = package_id.split(";");
      75            0 :                                                 const repos = parts[3].split(":");
      76            0 :                                                 if (repos.indexOf("installed") >= 0) {
      77            0 :                                                     installed_names.add(parts[0]);
      78            0 :                                                 } else {
      79            0 :                                                     data.missing_ids.push(package_id);
      80            0 :                                                     data.missing_names.push(parts[0]);
      81            0 :                                                 }
      82            0 :                                             },
      83            0 :                                         });
      84            0 :         pkgnames.forEach(name => {
      85            0 :             if (!installed_names.has(name) && data.missing_names.indexOf(name) == -1)
      86            0 :                 data.unavailable_names.push(name);
      87            0 :         });
      88              : 
      89            0 :         if (data.missing_ids.length > 0 && data.unavailable_names.length === 0) {
      90            0 :             await PK.cancellableTransaction("InstallPackages",
      91            0 :                                             [PK.Enum.TRANSACTION_FLAG_SIMULATE, data.missing_ids],
      92            0 :                                             progress_cb,
      93            0 :                                             {
      94            0 :                                                 Package: (info: number, package_id: string) => {
      95            0 :                                                     const name = package_id.split(";")[0];
      96            0 :                                                     if (info == PK.Enum.INFO_REMOVING) {
      97            0 :                                                         data.remove_names.push(name);
      98            0 :                                                     } else if (info == PK.Enum.INFO_INSTALLING ||
      99            0 :                                                              info == PK.Enum.INFO_UPDATING) {
     100            0 :                                                         install_ids.push(package_id);
     101            0 :                                                         if (data.missing_names.indexOf(name) == -1)
     102            0 :                                                             data.extra_names.push(name);
     103            0 :                                                     }
     104            0 :                                                 }
     105            0 :                                             });
     106            0 :             data.missing_names.sort();
     107            0 :             data.extra_names.sort();
     108            0 :             data.remove_names.sort();
     109            0 :         }
     110              : 
     111            0 :         if (install_ids.length > 0) {
     112            0 :             await PK.cancellableTransaction("GetDetails",
     113            0 :                                             [install_ids],
     114            0 :                                             progress_cb,
     115            0 :                                             {
     116            0 :                                                 Details: (details: { size: { v: number, t: string } }) => {
     117            0 :                                                     if (details.size)
     118            0 :                                                         data.download_size += details.size.v;
     119            0 :                                                 }
     120            0 :                                             });
     121            0 :         }
     122              : 
     123            0 :         return data;
     124            0 :     }
     125              : 
     126              :     /* Carry out what check_missing_packages has planned.
     127              :      *
     128              :      * In addition to the usual "waiting", "percentage", and "cancel"
     129              :      * fields, the object reported by progress_cb also includes "info" and
     130              :      * "package" from the "Package" signal.
     131              :      */
     132            0 :     async install_missing_packages(data: MissingPackages, progress_cb?: InstallProgressCB): Promise<void> {
     133            0 :         if (!data || data.missing_ids.length === 0)
     134            0 :             return;
     135              : 
     136            0 :         let last_progress: ProgressData | null = null;
     137            0 :         let last_info = 0;
     138            0 :         let last_name = "";
     139              : 
     140            0 :         function report_progess() {
     141            0 :             if (progress_cb && last_progress !== null)
     142            0 :                 progress_cb({
     143            0 :                     waiting: last_progress.waiting,
     144            0 :                     percentage: last_progress.percentage,
     145            0 :                     cancel: last_progress.cancel,
     146            0 :                     info: InstallProgressMap[last_info],
     147              :                     // Maps PackageKit state to our own PackageManager state temporary
     148              :                     // until all pkg/lib/packagekit use cases are supported by the PackageManager abstraction.
     149            0 :                     package: last_name
     150            0 :                 });
     151            0 :         }
     152              : 
     153            0 :         await PK.cancellableTransaction("InstallPackages", [0, data.missing_ids],
     154            0 :                                         (p: ProgressData) => {
     155            0 :                                             last_progress = p;
     156            0 :                                             report_progess();
     157            0 :                                         },
     158            0 :                                         {
     159            0 :                                             Package: (info: number, id: string) => {
     160            0 :                                                 last_info = info;
     161            0 :                                                 last_name = id.split(";")[0];
     162            0 :                                                 report_progess();
     163            0 :                                             }
     164            0 :                                         });
     165            0 :     }
     166              : 
     167            1 :     async refresh(force: boolean, progress_cb?: ProgressCB): Promise<void> {
     168            1 :         return PK.refresh(force, progress_cb);
     169            1 :     }
     170              : 
     171            0 :     async is_installed(pkgnames: string[]): Promise<boolean> {
     172            0 :         const uninstalled = new Set(pkgnames);
     173              : 
     174            0 :         if (uninstalled.size === 0)
     175            0 :             return true;
     176              : 
     177            0 :         await PK.cancellableTransaction("Resolve",
     178            0 :                                         [PK.Enum.FILTER_ARCH | PK.Enum.FILTER_NOT_SOURCE | PK.Enum.FILTER_INSTALLED, pkgnames],
     179            0 :                                         null,
     180            0 :                                         {
     181            0 :                                             Package: (_info: unknown, package_id: string) => {
     182            0 :                                                 const parts = package_id.split(";");
     183            0 :                                                 uninstalled.delete(parts[0]);
     184            0 :                                             },
     185            0 :                                         });
     186              : 
     187            0 :         return uninstalled.size === 0;
     188            0 :     }
     189              : 
     190            0 :     async install_packages(pkgnames: string[], progress_cb?: ProgressCB): Promise<void> {
     191            0 :         const flags = PK.Enum.FILTER_ARCH | PK.Enum.FILTER_NOT_SOURCE | PK.Enum.FILTER_NEWEST;
     192            0 :         const ids: string[] = [];
     193              : 
     194            0 :         await PK.cancellableTransaction("Resolve", [flags | PK.Enum.FILTER_NOT_INSTALLED, Array.from(pkgnames)], null,
     195            0 :                                         {
     196            0 :                                             Package: (_info: unknown, package_id: string) => ids.push(package_id),
     197            0 :                                         });
     198              : 
     199            0 :         if (ids.length === 0)
     200            0 :             return Promise.reject(new PK.TransactionError("not-found", "Can't resolve package(s)"));
     201              :         else
     202            0 :             return PK.cancellableTransaction("InstallPackages", [0, ids], progress_cb)
     203            0 :                     .catch(ex => {
     204            0 :                         if (ex.code != PK.Enum.ERROR_ALREADY_INSTALLED)
     205            0 :                             return Promise.reject(ex);
     206            0 :                     });
     207            0 :     }
     208              : 
     209            0 :     async remove_packages(pkgnames: string[], progress_cb?: ProgressCB): Promise<void> {
     210            0 :         const ids: string[] = [];
     211              : 
     212            0 :         await PK.cancellableTransaction("Resolve", [PK.Enum.FILTER_NOT_SOURCE | PK.Enum.FILTER_INSTALLED | PK.Enum.FILTER_NOT_SOURCE, pkgnames], null,
     213            0 :                                         {
     214            0 :                                             Package: (_info: unknown, package_id: string) => ids.push(package_id),
     215            0 :                                         });
     216              : 
     217            0 :         if (ids.length === 0)
     218            0 :             return Promise.resolve();
     219              : 
     220            0 :         return PK.cancellableTransaction("RemovePackages", [0, ids, true, false], progress_cb);
     221            0 :     }
     222              : 
     223            0 :     async find_file_packages(files: string[], progress_cb?: ProgressCB): Promise<string[]> {
     224            0 :         const installed: string[] = [];
     225            0 :         await PK.cancellableTransaction("SearchFiles",
     226            0 :                                         [PK.Enum.FILTER_ARCH | PK.Enum.FILTER_NOT_SOURCE | PK.Enum.FILTER_INSTALLED, files],
     227            0 :                                         progress_cb,
     228            0 :                                         {
     229            0 :                                             Package: (_info: unknown, package_id: string) => {
     230            0 :                                                 const pkg = package_id.split(";")[0];
     231            0 :                                                 installed.push(pkg);
     232            0 :                                             },
     233            0 :                                         });
     234              : 
     235            0 :         return installed;
     236            0 :     }
     237              : 
     238            1 :     async get_updates<T extends boolean>(detail: T, progress_cb?: ProgressCB): Promise<T extends true ? UpdateDetail[] : Update[]> {
     239            1 :         const updates = await PK.get_updates(detail, progress_cb);
     240            1 :         return updates as unknown as T extends true ? UpdateDetail[] : Update[];
     241            1 :     }
     242              : 
     243            0 :     async update_packages(updates: Update[] | UpdateDetail[], progress_cb?: ProgressCB, transaction_path?: string): Promise<void> {
     244            0 :         return PK.update_packages(updates, progress_cb, transaction_path);
     245            0 :     }
     246              : 
     247            1 :     async get_backend(): Promise<string> {
     248            1 :         const [result] = await PK.call("/org/freedesktop/PackageKit",
     249            1 :                                        "org.freedesktop.DBus.Properties",
     250            1 :                                        "Get", ["org.freedesktop.PackageKit", "BackendName"]) as [{ v: string, t: string }];
     251            1 :         return result.v;
     252            1 :     }
     253              : 
     254            0 :     async get_last_refresh_time(): Promise<number> {
     255            0 :         const [seconds] = await PK.call("/org/freedesktop/PackageKit", "org.freedesktop.PackageKit", "GetTimeSinceAction", [PK.Enum.ROLE_REFRESH_CACHE]);
     256            0 :         return seconds;
     257            0 :     }
     258              : 
     259            1 :     async get_history(): Promise<History[]> {
     260            1 :         const history = [] as History[];
     261              : 
     262              :         // would be nice to filter only for "update-packages" role, but can't here
     263            1 :         await PK.transaction("GetOldTransactions", [0], {
     264            1 :             Transaction: (_objPath: string, timeSpec: string, _succeeded: string, role: number, _duration: string, data: string) => {
     265            1 :                 if (role !== PK.Enum.ROLE_UPDATE_PACKAGES)
     266            1 :                     return;
     267              : 
     268              :                 /**
     269              :                  * data looks like:
     270              :                  * downloading\tbash-completion;1:2.6-1.fc26;noarch;updates-testing
     271              :                  * updating\tbash-completion;1:2.6-1.fc26;noarch;updates-testing
     272              :                  * timeSpec will be one of:
     273              :                  * 2026-01-29T12:57:49.112827-08
     274              :                  * 2026-01-29T19:27:49.112827-01:30
     275              :                  * 2026-01-29T20:57:49.112827Z
     276              :                  * depending on timezone and PackageKit version
     277              :                  */
     278            1 :                 let timestamp = Date.parse(timeSpec);
     279            0 :                 if (isNaN(timestamp)) {
     280              :                     /*
     281              :                     * Neither Firefox's nor Chromium's parsers handle the short offset
     282              :                     * format (first one) as of 2026-01:
     283              :                     * https://bugzilla.mozilla.org/show_bug.cgi?id=2013444
     284              :                     * https://issues.chromium.org/issues/479862357
     285              :                     * so we hack around it by adding the :00
     286              :                     */
     287            0 :                     const shortoff_regex = /[-+]\d\d$/;
     288            0 :                     if (shortoff_regex.exec(timeSpec)) {
     289            0 :                         const fixed_timeSpec = timeSpec + ":00";
     290              :                         // console.debug(`Transaction has an invalid timeSpec=${timeSpec}, trying fixed=${fixed_timeSpec}`);
     291            0 :                         timestamp = Date.parse(fixed_timeSpec);
     292            0 :                     }
     293            0 :                 }
     294              : 
     295            0 :                 if (isNaN(timestamp)) {
     296            0 :                     console.warn(`Transaction has an invalid timeSpec=${timeSpec}, skipping`);
     297            0 :                     return;
     298            0 :                 }
     299              : 
     300            1 :                 const pkgs = { timestamp, packages: {} } as History;
     301            1 :                 let empty = true;
     302            1 :                 data.split("\n").forEach(line => {
     303            1 :                     const fields = line.trim().split("\t");
     304            1 :                     if (fields.length >= 2) {
     305            1 :                         const pkgId = fields[1].split(";");
     306            1 :                         pkgs.packages[pkgId[0]] = pkgId[1];
     307            1 :                         empty = false;
     308            1 :                     }
     309            1 :                 });
     310              : 
     311            1 :                 if (!empty)
     312            1 :                     history.unshift(pkgs); // PK reports in time-ascending order, but we want the latest first
     313            1 :             },
     314            1 :         });
     315              : 
     316            1 :         return history;
     317            1 :     }
     318              : 
     319              :     /**
     320              :      * Check a list of packages whether they are available.
     321              :      *
     322              :      * @param {string[]} pkgnames - names of packages which should be available in the repositories
     323              :      * @return {Promise<boolean>} true if packages are available
     324              :      */
     325            1 :     async is_available(pkgnames: string[]): Promise<boolean> {
     326            1 :         const available = new Set();
     327              : 
     328            1 :         if (pkgnames && pkgnames.length === 0)
     329            1 :             return true;
     330              : 
     331            1 :         await PK.cancellableTransaction("Resolve",
     332            1 :                                         [PK.Enum.FILTER_ARCH | PK.Enum.FILTER_NEWEST | PK.Enum.FILTER_NOT_INSTALLED, pkgnames],
     333            1 :                                         null,
     334            1 :                                         {
     335            0 :                                             Package: (_info: unknown, package_id: string) => {
     336            0 :                                                 const pkgname = package_id.split(";")[0];
     337            0 :                                                 available.add(pkgname);
     338            0 :                                             },
     339            1 :                                         });
     340              : 
     341            1 :         return available.size === new Set(pkgnames).size;
     342            1 :     }
     343           55 : }
        

Generated by: LCOV version 2.0-1