* Copyright (C) 2025 Red Hat, Inc.
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
import cockpit from "cockpit";
import { InstallProgressCB, MissingPackages, PackageManager, ProgressCB, InstallProgressType, UpdateProgressType, UpdateDetail, Update, ProgressData, History, UpdateProgressHandlers, TransactionExitStatus, UpdateError, UpdateNotify, RunningUpdate, Severity } from './packagemanager-abstract';
import { superuser } from 'superuser';
const _ = cockpit.gettext;
export const Enum = {
EXIT_SUCCESS: 1,
EXIT_FAILED: 2,
EXIT_CANCELLED: 3,
ROLE_REFRESH_CACHE: 13,
ROLE_UPDATE_PACKAGES: 22,
INFO_UNKNOWN: -1,
INFO_LOW: 3,
INFO_ENHANCEMENT: 4,
INFO_NORMAL: 5,
INFO_BUGFIX: 6,
INFO_IMPORTANT: 7,
INFO_SECURITY: 8,
INFO_DOWNLOADING: 10,
INFO_UPDATING: 11,
INFO_INSTALLING: 12,
INFO_REMOVING: 13,
INFO_REINSTALLING: 19,
INFO_DOWNGRADING: 20,
STATUS_WAIT: 1,
STATUS_DOWNLOAD: 8,
STATUS_INSTALL: 9,
STATUS_UPDATE: 10,
STATUS_CLEANUP: 11,
STATUS_SIGCHECK: 14,
STATUS_WAITING_FOR_LOCK: 30,
FILTER_INSTALLED: (1 << 2),
FILTER_NOT_INSTALLED: (1 << 3),
FILTER_NEWEST: (1 << 16),
FILTER_ARCH: (1 << 18),
FILTER_NOT_SOURCE: (1 << 21),
ERROR_ALREADY_INSTALLED: 9,
TRANSACTION_FLAG_SIMULATE: (1 << 2),
};
const UpdateProgressMap: Record<number, UpdateProgressType> = {
[Enum.STATUS_DOWNLOAD]: UpdateProgressType.DOWNLOADING,
[Enum.STATUS_INSTALL]: UpdateProgressType.INSTALLING,
[Enum.STATUS_UPDATE]: UpdateProgressType.UPDATING,
[Enum.STATUS_CLEANUP]: UpdateProgressType.CLEANUP,
[Enum.STATUS_SIGCHECK]: UpdateProgressType.SIGCHECK,
};
const InstallProgressMap = {
[Enum.INFO_DOWNLOADING]: InstallProgressType.DOWNLOADING,
[Enum.INFO_UPDATING]: InstallProgressType.UPDATING,
[Enum.INFO_REMOVING]: InstallProgressType.REMOVING,
[Enum.INFO_INSTALLING]: InstallProgressType.INSTALLING,
[Enum.INFO_REINSTALLING]: InstallProgressType.REINSTALLING,
[Enum.INFO_DOWNGRADING]: InstallProgressType.DOWNGRADING,
};
function debug(...args: unknown[]) {
if (window.debugging === 'all' || window.debugging?.includes('packagekit'))
console.debug(...args);
}
interface SignalHandlers {
Package?: (info: number, packageId: string, summary: string) => void;
UpdateDetail?: (packageId: string, updates: unknown, obsoletes: unknown,
vendor_urls: string[], bug_urls: string[], cve_urls: string[],
restart: boolean, update_text: string, changelog: string) => void;
ErrorCode?: (code: string, detail: string) => void;
Finished?: (exit: number) => void;
Details?: (details: { size: { v: number, t: string } }) => void;
Transaction?: (objPath: string, timeSpec: string, succeeded: string,
role: number, duration: string, data: string) => void;
}
const transactionInterface = "org.freedesktop.PackageKit.Transaction";
let _dbus_client: cockpit.DBusClient | null = null;
* Get PackageKit D-Bus client
*
* This will get lazily initialized and re-initialized after PackageKit
* disconnects (due to a crash or idle timeout).
*/
function dbus_client() {
if (_dbus_client === null) {
_dbus_client = cockpit.dbus("org.freedesktop.PackageKit", { superuser: "try", track: true });
_dbus_client.addEventListener("close", () => {
console.log("PackageKit went away from D-Bus");
_dbus_client = null;
});
}
return _dbus_client;
}
superuser.addEventListener("changed", () => { _dbus_client = null });
* Call a PackageKit method
*/
export function call(objectPath: string, iface: string, method: string, args?: unknown[] | null, opts?: cockpit.DBusCallOptions) {
return dbus_client().call(objectPath, iface, method, args, opts);
}
* Watch a running PackageKit transaction
*
* transactionPath (string): D-Bus object path of the PackageKit transaction
* signalHandlers, notifyHandler: As in method #transaction
* Returns: If notifyHandler is set, Cockpit promise that resolves when the watch got set up
*/
export function watchTransaction(transactionPath: string, signalHandlers: SignalHandlers, notifyHandler?: (props: Record<string, unknown>, path: string) => void) {
const subscriptions: { remove: () => void }[] = [];
let notifyReturn: cockpit.DeferredPromise<void> | undefined;
const client = dbus_client();
function onClose(_event: unknown, ex: unknown) {
console.warn("PackageKit went away during transaction", transactionPath, ":", JSON.stringify(ex));
if (signalHandlers.ErrorCode)
signalHandlers.ErrorCode("close", _("PackageKit crashed"));
if (signalHandlers.Finished)
signalHandlers.Finished(Enum.EXIT_FAILED);
}
client.addEventListener("close", onClose);
if (signalHandlers) {
for (const [member, handler] of Object.entries(signalHandlers)) {
if (handler) {
subscriptions.push(
client.subscribe({ interface: transactionInterface, path: transactionPath, member },
(_path, _iface, _signal, args) => handler(...args))
);
}
}
}
if (notifyHandler) {
notifyReturn = client.watch(transactionPath);
subscriptions.push(notifyReturn as unknown as { remove: () => void });
client.addEventListener("notify", (event: CustomEvent) => {
const data = event.detail as Record<string, Record<string, unknown>>;
const iface = data?.[transactionPath]?.[transactionInterface];
if (iface)
notifyHandler(iface as Record<string, unknown>, transactionPath);
});
}
subscriptions.push(client.subscribe(
{ interface: transactionInterface, path: transactionPath, member: "Finished" },
() => {
subscriptions.map(s => s.remove());
client.removeEventListener("close", onClose);
})
);
return notifyReturn;
}
* Run a PackageKit transaction
*
* method (string): D-Bus method name on the https://www.freedesktop.org/software/PackageKit/gtk-doc/Transaction.html interface
* If undefined, only a transaction will be created without calling a method on it
* arglist (array): "in" arguments of @method
* signalHandlers (object): maps PackageKit.Transaction signal names to handlers
* notifyHandler (function): handler for https://docs.cockpit-project.org/cockpit-guide/latest/guide/cockpit-dbus.html#cockpit-dbus-onnotify
* signals, called on property changes with (changed_properties, transaction_path)
* Returns: Promise that resolves with transaction path on success, or rejects on an error
*
* Note that most often you don't really need the transaction path, but want to
* listen to the "Finished" signal.
*
* Example:
* transaction("GetUpdates", [0], {
* Package: (info, packageId, _summary) => { ... },
* ErrorCode: (code, details) => { ... },
* },
* changedProps => { ... } // notify handler
* )
* .then(transactionPath => { ... })
* .catch(ex => { handle exception });
*/
export function transaction(method: string, arglist: unknown[], signalHandlers?: SignalHandlers, notifyHandler?: (props: Record<string, unknown>, path: string) => void): Promise<unknown> {
return call("/org/freedesktop/PackageKit", "org.freedesktop.PackageKit", "CreateTransaction", [])
.then(([transactionPath]) => {
if (!signalHandlers && !notifyHandler)
return transactionPath;
const watchPromise = watchTransaction(transactionPath as string, signalHandlers!, notifyHandler) || Promise.resolve();
return watchPromise.then(() => {
if (method) {
return call(transactionPath as string, transactionInterface, method, arglist)
.then(() => transactionPath);
} else {
return transactionPath;
}
});
});
}
export class TransactionError extends Error {
detail: string;
code: string;
constructor(code: string, detail: string) {
super(detail);
this.detail = detail;
this.code = code;
}
}
* Run a long cancellable PackageKit transaction
*
* method (string): D-Bus method name on the https://www.freedesktop.org/software/PackageKit/gtk-doc/Transaction.html interface
* arglist (array): "in" arguments of @method
* progress_cb: Callback that receives a {waiting, percentage, cancel} object regularly; if cancel is not null, it can
* be called to cancel the current transaction. if wait is true, PackageKit is waiting for its lock (i. e.
* on another package operation)
* signalHandlers, notifyHandler: As in method #transaction, but ErrorCode and Finished are handled internally
* Returns: Promise that resolves when the transaction finished successfully, or rejects with TransactionError
* on failure.
*/
export function cancellableTransaction(method: string, arglist: unknown[], progress_cb: ProgressCB | null | undefined, signalHandlers?: SignalHandlers): Promise<void> {
if (signalHandlers?.ErrorCode || signalHandlers?.Finished)
throw Error("cancellableTransaction handles ErrorCode and Finished signals internally");
return new Promise((resolve, reject) => {
let cancelled = false;
let status: number | undefined;
let allow_wait_status = false;
const progress_data: ProgressData = {
waiting: false,
percentage: 0,
cancel: null
};
function changed(props: Record<string, unknown>, transaction_path: string) {
function cancel() {
call(transaction_path, transactionInterface, "Cancel", []);
cancelled = true;
}
if (progress_cb) {
if ("Status" in props)
status = props.Status as number;
progress_data.waiting = allow_wait_status && (status === Enum.STATUS_WAIT || status === Enum.STATUS_WAITING_FOR_LOCK);
if ("AllowCancel" in props)
progress_data.cancel = props.AllowCancel ? cancel : null;
if ("Percentage" in props && (props.Percentage as number) <= 100)
progress_data.percentage = props.Percentage as number;
progress_cb(progress_data);
}
}
window.setTimeout(() => {
allow_wait_status = true;
changed({}, "");
}, 1000);
transaction(method, arglist,
Object.assign({
ErrorCode: (code: string, detail: string) => {
progress_cb = null;
reject(new TransactionError(cancelled ? "cancelled" : code, detail));
},
Finished: (_exit: number) => {
progress_cb = null;
resolve();
},
}, signalHandlers || {}),
changed)
.catch(ex => {
progress_cb = null;
reject(ex);
});
});
}
* On Debian the update_text starts with "== version ==" which is
* redundant; we don't want Markdown headings in the table
*
* @param {string} text - update_text to filter
*/
function removeHeading(text: string) {
if (text)
return text.trim().replace(/^== .* ==\n/, "")
.trim();
return text;
}
function parseCVEs(text: string) {
if (!text)
return [];
const cves = text.match(/CVE-\d{4}-\d+/g);
if (!cves)
return [];
return cves.map(n => "https://www.cve.org/CVERecord?id=" + n);
}
function deduplicate<T>(list: T[]): T[] {
return [...new Set(list)].sort();
}
function loadUpdateDetailsBatch(pkg_ids: string[], update_details: Record<string, Partial<UpdateDetail> & Update>, progress_cb?: ProgressCB) {
return cancellableTransaction("GetUpdateDetail", [pkg_ids], progress_cb, {
UpdateDetail: (packageId: string, _updates: unknown, _obsoletes: unknown,
vendor_urls: string[], bug_urls: string[], cve_urls: string[],
_restart: boolean, update_text: string, changelog: string ) => {
const u = update_details[packageId];
if (!u) {
console.warn("Mismatching update:", packageId);
return;
}
u.vendor_urls = vendor_urls;
u.description = removeHeading(update_text) || changelog;
if (update_text)
u.markdown = true;
u.bug_urls = deduplicate(bug_urls);
u.cve_urls = deduplicate(cve_urls && cve_urls.length > 0 ? cve_urls : parseCVEs(u.description));
if (u.cve_urls && u.cve_urls.length > 0)
u.severity = Severity.CRITICAL;
u.vendor_urls = vendor_urls || [];
debug("UpdateDetail:", u);
}
});
}
export class PackageKitManager implements PackageManager {
name: string;
constructor() {
this.name = "packagekit";
}
*
* First call check_missing_packages to determine whether something
* needs to be installed, then call install_missing_packages to
* actually install them.
*
* check_missing_packages resolves to an object that can be passed to
* install_missing_packages. It contains these fields:
*
* - missing_names: Packages that were requested, are currently not installed,
* and can be installed.
*
* - missing_ids: The full PackageKit IDs corresponding to missing_names
*
* - unavailable_names: Packages that were requested, are currently not installed,
* but can't be found in any repository.
*
* If unavailable_names is empty, a simulated installation of the missing packages
* is done and the result also contains these fields:
*
* - extra_names: Packages that need to be installed as dependencies of
* missing_names.
*
* - remove_names: Packages that need to be removed.
*
* - download_size: Bytes that need to be downloaded.
*/
async check_missing_packages(pkgnames: string[], progress_cb?: ProgressCB): Promise<MissingPackages> {
const install_ids: string[] = [];
const installed_names = new Set();
const data: MissingPackages = {
download_size: 0,
missing_ids: [],
missing_names: [],
unavailable_names: [],
extra_names: [],
remove_names: [],
};
if (pkgnames.length === 0)
return data;
await this.refresh(false, progress_cb);
await cancellableTransaction("Resolve",
[Enum.FILTER_ARCH | Enum.FILTER_NOT_SOURCE | Enum.FILTER_NEWEST, pkgnames],
progress_cb,
{
Package: (_info: number, package_id: string) => {
const parts = package_id.split(";");
const repos = parts[3].split(":");
if (repos.indexOf("installed") >= 0) {
installed_names.add(parts[0]);
} else {
data.missing_ids.push(package_id);
data.missing_names.push(parts[0]);
}
},
});
pkgnames.forEach(name => {
if (!installed_names.has(name) && data.missing_names.indexOf(name) === -1)
data.unavailable_names.push(name);
});
if (data.missing_ids.length > 0 && data.unavailable_names.length === 0) {
await cancellableTransaction("InstallPackages",
[Enum.TRANSACTION_FLAG_SIMULATE, data.missing_ids],
progress_cb,
{
Package: (info: number, package_id: string) => {
const name = package_id.split(";")[0];
if (info === Enum.INFO_REMOVING) {
data.remove_names.push(name);
} else if (info === Enum.INFO_INSTALLING ||
info === Enum.INFO_UPDATING) {
install_ids.push(package_id);
if (data.missing_names.indexOf(name) === -1)
data.extra_names.push(name);
}
}
});
data.missing_names.sort();
data.extra_names.sort();
data.remove_names.sort();
}
if (install_ids.length > 0) {
await cancellableTransaction("GetDetails",
[install_ids],
progress_cb,
{
Details: (details: { size: { v: number, t: string } }) => {
if (details.size)
data.download_size += details.size.v;
}
});
}
return data;
}
*
* In addition to the usual "waiting", "percentage", and "cancel"
* fields, the object reported by progress_cb also includes "info" and
* "package" from the "Package" signal.
*/
async install_missing_packages(data: MissingPackages, progress_cb?: InstallProgressCB): Promise<void> {
if (!data || data.missing_ids.length === 0)
return;
let last_progress: ProgressData | null = null;
let last_info = 0;
let last_name = "";
function report_progress() {
if (progress_cb && last_progress !== null)
progress_cb({
waiting: last_progress.waiting,
percentage: last_progress.percentage,
cancel: last_progress.cancel,
info: InstallProgressMap[last_info],
package: last_name
});
}
await cancellableTransaction("InstallPackages", [0, data.missing_ids],
(p: ProgressData) => {
last_progress = p;
report_progress();
},
{
Package: (info: number, id: string) => {
last_info = info;
last_name = id.split(";")[0];
report_progress();
}
});
}
async refresh(force: boolean, progress_cb?: ProgressCB): Promise<void> {
return cancellableTransaction("RefreshCache", [force], progress_cb);
}
async is_installed(pkgnames: string[]): Promise<boolean> {
const uninstalled = new Set(pkgnames);
if (uninstalled.size === 0)
return true;
await cancellableTransaction("Resolve",
[Enum.FILTER_ARCH | Enum.FILTER_NOT_SOURCE | Enum.FILTER_INSTALLED, pkgnames],
null,
{
Package: (_info: unknown, package_id: string) => {
const parts = package_id.split(";");
uninstalled.delete(parts[0]);
},
});
return uninstalled.size === 0;
}
async install_packages(pkgnames: string[], progress_cb?: ProgressCB): Promise<void> {
const flags = Enum.FILTER_ARCH | Enum.FILTER_NOT_SOURCE | Enum.FILTER_NEWEST;
const ids: string[] = [];
await cancellableTransaction("Resolve", [flags | Enum.FILTER_NOT_INSTALLED, Array.from(pkgnames)], null,
{
Package: (_info: unknown, package_id: string) => ids.push(package_id),
});
if (ids.length === 0)
return Promise.reject(new TransactionError("not-found", "Can't resolve package(s)"));
else
return cancellableTransaction("InstallPackages", [0, ids], progress_cb)
.catch(ex => {
if (ex.code !== Enum.ERROR_ALREADY_INSTALLED)
return Promise.reject(ex);
});
}
async remove_packages(pkgnames: string[], progress_cb?: ProgressCB): Promise<void> {
const ids: string[] = [];
await cancellableTransaction("Resolve", [Enum.FILTER_NOT_SOURCE | Enum.FILTER_INSTALLED | Enum.FILTER_NOT_SOURCE, pkgnames], null,
{
Package: (_info: unknown, package_id: string) => ids.push(package_id),
});
if (ids.length === 0)
return Promise.resolve();
return cancellableTransaction("RemovePackages", [0, ids, true, false], progress_cb);
}
async find_file_packages(files: string[], progress_cb?: ProgressCB): Promise<string[]> {
const installed: string[] = [];
await cancellableTransaction("SearchFiles",
[Enum.FILTER_ARCH | Enum.FILTER_NOT_SOURCE | Enum.FILTER_INSTALLED, files],
progress_cb,
{
Package: (_info: unknown, package_id: string) => {
const pkg = package_id.split(";")[0];
installed.push(pkg);
},
});
return installed;
}
* Get Updates
* updates = { id, name, version, arch }
* with details
* updates = { id, name, version, arch, severity, bug_urls, cve_urls, vendor_urls, description, markdown }
* @param {boolean} details - fetch detailed package information (security information)
*/
async get_updates<T extends boolean>(details: T, progress_cb?: ProgressCB): Promise<T extends true ? UpdateDetail[] : Update[]> {
const updates: Record<string, Partial<UpdateDetail> & Update> = {};
await cancellableTransaction(
"GetUpdates", [0],
progress_cb,
{
Package: (info: number, packageId: string, summary: string) => {
info = info & 0xff;
const id_fields = packageId.split(";");
if (info < Enum.INFO_LOW || info > Enum.INFO_SECURITY)
info = Enum.INFO_NORMAL;
if (info === Enum.INFO_LOW)
info = Severity.LOW;
else if (info === Enum.INFO_ENHANCEMENT)
info = Severity.MODERATE;
else if (info === Enum.INFO_SECURITY)
info = Severity.CRITICAL;
else if (info >= Enum.INFO_NORMAL)
info = Severity.IMPORTANT;
else
info = Severity.MODERATE;
updates[packageId] = { id: packageId, name: id_fields[0], version: id_fields[1], severity: info, arch: id_fields[2], summary };
}
});
const pkg_ids = Object.keys(updates);
if (details && pkg_ids.length > 0) {
const processBatch = async (remaining_ids: string[], current_batch_size: number) => {
if (remaining_ids.length === 0) {
return;
}
const batch = remaining_ids.slice(0, current_batch_size);
const next_ids = remaining_ids.slice(current_batch_size);
try {
await loadUpdateDetailsBatch(batch, updates, progress_cb);
await processBatch(next_ids, current_batch_size);
} catch (ex) {
console.warn("GetUpdateDetail failed with batch size", current_batch_size, ":", JSON.stringify(ex));
if (current_batch_size > 1) {
console.log("Reducing GetUpdateDetail batch size to 1 and retrying");
await processBatch(remaining_ids, 1);
} else {
console.warn("Failed to load update details for package:", batch[0]);
await processBatch(next_ids, 1);
}
}
};
await processBatch(pkg_ids, 500);
}
return Object.entries(updates).map(([key, u]) => ({ ...u, id: key })) as unknown as T extends true ? UpdateDetail[] : Update[];
}
async update_packages(updates: Update[] | UpdateDetail[], handlers: UpdateProgressHandlers): Promise<TransactionExitStatus> {
const update_ids = updates.map(u => u.id);
let transactionPath: string | null = null;
const errorMessages: string[] = [];
return new Promise<TransactionExitStatus>((resolve, reject) => {
transaction("UpdatePackages", [0, update_ids],
{
ErrorCode: (_code: string, details: string) => errorMessages.push(details),
Finished: (exit: number) => {
if (exit === Enum.EXIT_SUCCESS) {
resolve(TransactionExitStatus.SUCCESS);
} else if (exit === Enum.EXIT_CANCELLED) {
resolve(TransactionExitStatus.CANCELLED);
} else {
if (exit !== Enum.EXIT_FAILED)
errorMessages.push(cockpit.format(cockpit.gettext("PackageKit reported error code $0"), exit));
reject(new UpdateError(errorMessages));
}
},
Package: (status: number, packageId: string) => {
if (status in UpdateProgressMap)
handlers.on_package(UpdateProgressMap[status], packageId);
},
},
(notify: Record<string, unknown>, path: string) => {
transactionPath = path;
const props: UpdateNotify = {
cancel: notify.AllowCancel
? () => call(transactionPath!, transactionInterface, "Cancel", [])
: null,
};
if (notify.Percentage !== undefined)
props.percentage = notify.Percentage as number;
if (notify.RemainingTime !== undefined)
props.remaining_time = notify.RemainingTime as number;
if (notify.LastPackage !== undefined)
props.last_package = notify.LastPackage as string;
handlers.on_notify(props);
},
).catch(reject);
});
}
async get_running_update(handlers: UpdateProgressHandlers): Promise<RunningUpdate | null> {
let transactions: string[];
let roles: unknown[];
try {
[transactions] = await call("/org/freedesktop/PackageKit", "org.freedesktop.PackageKit", "GetTransactionList", []) as [string[]];
roles = await Promise.all(transactions.map(path => call(path, "org.freedesktop.DBus.Properties",
"Get", [transactionInterface, "Role"])));
} catch (ex) {
console.warn("GetTransactionList: failed to read PackageKit transaction roles:", String(ex));
return null;
}
for (let idx = 0; idx < roles.length; ++idx) {
const [role] = roles[idx] as [{ v: number }];
if (role.v !== Enum.ROLE_UPDATE_PACKAGES) {
continue;
}
const transactionPath = transactions[idx];
const errorMessages: string[] = [];
const promise = new Promise<TransactionExitStatus>((resolve, reject) => {
watchTransaction(transactionPath,
{
ErrorCode: (_code: string, details: string) => errorMessages.push(details),
Finished: (exit: number) => {
if (exit === Enum.EXIT_SUCCESS) {
resolve(TransactionExitStatus.SUCCESS);
} else if (exit === Enum.EXIT_CANCELLED) {
resolve(TransactionExitStatus.CANCELLED);
} else {
if (exit !== Enum.EXIT_FAILED)
errorMessages.push(cockpit.format(cockpit.gettext("PackageKit reported error code $0"), exit));
reject(new UpdateError(errorMessages));
}
},
Package: (status: number, packageId: string) => {
if (status in UpdateProgressMap)
handlers.on_package(UpdateProgressMap[status], packageId);
},
},
(notify: Record<string, unknown>) => {
const props: UpdateNotify = {
cancel: notify.AllowCancel ? () => call(transactionPath, transactionInterface, "Cancel", []) : null,
};
if (notify.Percentage !== undefined)
props.percentage = notify.Percentage as number;
if (notify.RemainingTime !== undefined)
props.remaining_time = notify.RemainingTime as number;
if (notify.LastPackage !== undefined)
props.last_package = notify.LastPackage as string;
handlers.on_notify(props);
},
);
});
return { promise };
}
return null;
}
async get_backend(): Promise<string> {
const [result] = await call("/org/freedesktop/PackageKit",
"org.freedesktop.DBus.Properties",
"Get", ["org.freedesktop.PackageKit", "BackendName"]) as [{ v: string, t: string }];
return result.v;
}
async get_last_refresh_time(): Promise<number> {
const [seconds] = await call("/org/freedesktop/PackageKit", "org.freedesktop.PackageKit", "GetTimeSinceAction", [Enum.ROLE_REFRESH_CACHE]) as [number];
return seconds;
}
async get_history(): Promise<History[]> {
const history = [] as History[];
await transaction("GetOldTransactions", [0], {
Transaction: (_objPath: string, timeSpec: string, _succeeded: string, role: number, _duration: string, data: string) => {
if (role !== Enum.ROLE_UPDATE_PACKAGES)
return;
* data looks like:
* downloading\tbash-completion;1:2.6-1.fc26;noarch;updates-testing
* updating\tbash-completion;1:2.6-1.fc26;noarch;updates-testing
* timeSpec will be one of:
* 2026-01-29T12:57:49.112827-08
* 2026-01-29T19:27:49.112827-01:30
* 2026-01-29T20:57:49.112827Z
* depending on timezone and PackageKit version
*/
let timestamp = Date.parse(timeSpec);
if (isNaN(timestamp)) {
* Neither Firefox's nor Chromium's parsers handle the short offset
* format (first one) as of 2026-01:
* https://bugzilla.mozilla.org/show_bug.cgi?id=2013444
* https://issues.chromium.org/issues/479862357
* so we hack around it by adding the :00
*/
const shortoff_regex = /[-+]\d\d$/;
if (shortoff_regex.exec(timeSpec)) {
const fixed_timeSpec = timeSpec + ":00";
timestamp = Date.parse(fixed_timeSpec);
}
}
if (isNaN(timestamp)) {
console.warn(`Transaction has an invalid timeSpec=${timeSpec}, skipping`);
return;
}
const pkgs = { timestamp, packages: {} } as History;
let empty = true;
data.split("\n").forEach(line => {
const fields = line.trim().split("\t");
if (fields.length >= 2) {
const pkgId = fields[1].split(";");
pkgs.packages[pkgId[0]] = pkgId[1];
empty = false;
}
});
if (!empty)
history.unshift(pkgs);
},
});
return history;
}
* Check a list of packages whether they are available.
*
* @param {string[]} pkgnames - names of packages which should be available in the repositories
* @return {Promise<boolean>} true if packages are available
*/
async is_available(pkgnames: string[]): Promise<boolean> {
const available = new Set();
if (pkgnames && pkgnames.length === 0)
return true;
await cancellableTransaction("Resolve",
[Enum.FILTER_ARCH | Enum.FILTER_NEWEST | Enum.FILTER_NOT_INSTALLED, pkgnames],
null,
{
Package: (_info: unknown, package_id: string) => {
const pkgname = package_id.split(";")[0];
available.add(pkgname);
},
});
return available.size === new Set(pkgnames).size;
}
}