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 : }
|