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 282 : const InstallProgressMap = {
10 282 : [PK.Enum.INFO_DOWNLOADING]: InstallProgressType.DOWNLOADING,
11 282 : [PK.Enum.INFO_UPDATING]: InstallProgressType.UPDATING,
12 282 : [PK.Enum.INFO_REMOVING]: InstallProgressType.REMOVING,
13 282 : [PK.Enum.INFO_INSTALLING]: InstallProgressType.INSTALLING,
14 282 : [PK.Enum.INFO_REINSTALLING]: InstallProgressType.REINSTALLING,
15 282 : [PK.Enum.INFO_DOWNGRADING]: InstallProgressType.DOWNGRADING,
16 282 : };
17 :
18 282 : export class PackageKitManager implements PackageManager {
19 : name: string;
20 :
21 19 : constructor() {
22 19 : this.name = "packagekit";
23 19 : }
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 2 : async check_missing_packages(pkgnames: string[], progress_cb?: ProgressCB): Promise<MissingPackages> {
53 2 : const install_ids: string[] = [];
54 2 : const installed_names = new Set();
55 2 : const data: MissingPackages = {
56 2 : download_size: 0,
57 2 : missing_ids: [],
58 2 : missing_names: [],
59 2 : unavailable_names: [],
60 2 : extra_names: [],
61 2 : remove_names: [],
62 2 : };
63 :
64 2 : if (pkgnames.length === 0)
65 0 : return data;
66 :
67 2 : await this.refresh(false, progress_cb);
68 :
69 1 : await PK.cancellableTransaction("Resolve",
70 1 : [PK.Enum.FILTER_ARCH | PK.Enum.FILTER_NOT_SOURCE | PK.Enum.FILTER_NEWEST, pkgnames],
71 1 : progress_cb,
72 1 : {
73 1 : Package: (_info: number, package_id: string) => {
74 1 : const parts = package_id.split(";");
75 1 : const repos = parts[3].split(":");
76 0 : if (repos.indexOf("installed") >= 0) {
77 0 : installed_names.add(parts[0]);
78 0 : } else {
79 1 : data.missing_ids.push(package_id);
80 1 : data.missing_names.push(parts[0]);
81 1 : }
82 1 : },
83 1 : });
84 1 : pkgnames.forEach(name => {
85 1 : if (!installed_names.has(name) && data.missing_names.indexOf(name) == -1)
86 0 : data.unavailable_names.push(name);
87 1 : });
88 :
89 1 : if (data.missing_ids.length > 0 && data.unavailable_names.length === 0) {
90 1 : await PK.cancellableTransaction("InstallPackages",
91 1 : [PK.Enum.TRANSACTION_FLAG_SIMULATE, data.missing_ids],
92 1 : progress_cb,
93 1 : {
94 1 : Package: (info: number, package_id: string) => {
95 1 : 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 1 : install_ids.push(package_id);
101 1 : if (data.missing_names.indexOf(name) == -1)
102 0 : data.extra_names.push(name);
103 1 : }
104 1 : }
105 1 : });
106 1 : data.missing_names.sort();
107 1 : data.extra_names.sort();
108 1 : data.remove_names.sort();
109 1 : }
110 :
111 1 : if (install_ids.length > 0) {
112 1 : await PK.cancellableTransaction("GetDetails",
113 1 : [install_ids],
114 1 : progress_cb,
115 1 : {
116 1 : Details: (details: { size: { v: number, t: string } }) => {
117 1 : if (details.size)
118 1 : data.download_size += details.size.v;
119 1 : }
120 1 : });
121 1 : }
122 :
123 1 : return data;
124 2 : }
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 1 : async install_missing_packages(data: MissingPackages, progress_cb?: InstallProgressCB): Promise<void> {
133 1 : if (!data || data.missing_ids.length === 0)
134 1 : return;
135 :
136 1 : let last_progress: ProgressData | null = null;
137 1 : let last_info = 0;
138 1 : let last_name = "";
139 :
140 1 : function report_progess() {
141 1 : if (progress_cb && last_progress !== null)
142 1 : progress_cb({
143 1 : waiting: last_progress.waiting,
144 1 : percentage: last_progress.percentage,
145 1 : cancel: last_progress.cancel,
146 1 : 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 1 : package: last_name
150 1 : });
151 1 : }
152 :
153 1 : await PK.cancellableTransaction("InstallPackages", [0, data.missing_ids],
154 1 : (p: ProgressData) => {
155 1 : last_progress = p;
156 1 : report_progess();
157 1 : },
158 1 : {
159 1 : Package: (info: number, id: string) => {
160 1 : last_info = info;
161 1 : last_name = id.split(";")[0];
162 1 : report_progess();
163 1 : }
164 1 : });
165 1 : }
166 :
167 10 : async refresh(force: boolean, progress_cb?: ProgressCB): Promise<void> {
168 10 : return PK.refresh(force, progress_cb);
169 10 : }
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 19 : async get_updates<T extends boolean>(detail: T, progress_cb?: ProgressCB): Promise<T extends true ? UpdateDetail[] : Update[]> {
239 19 : const updates = await PK.get_updates(detail, progress_cb);
240 19 : return updates as unknown as T extends true ? UpdateDetail[] : Update[];
241 19 : }
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 19 : async get_backend(): Promise<string> {
248 19 : const [result] = await PK.call("/org/freedesktop/PackageKit",
249 19 : "org.freedesktop.DBus.Properties",
250 19 : "Get", ["org.freedesktop.PackageKit", "BackendName"]) as [{ v: string, t: string }];
251 19 : return result.v;
252 19 : }
253 :
254 17 : async get_last_refresh_time(): Promise<number> {
255 17 : const [seconds] = await PK.call("/org/freedesktop/PackageKit", "org.freedesktop.PackageKit", "GetTimeSinceAction", [PK.Enum.ROLE_REFRESH_CACHE]);
256 17 : return seconds;
257 17 : }
258 :
259 19 : async get_history(): Promise<History[]> {
260 19 : const history = [] as History[];
261 :
262 : // would be nice to filter only for "update-packages" role, but can't here
263 19 : await PK.transaction("GetOldTransactions", [0], {
264 10 : Transaction: (_objPath: string, timeSpec: string, _succeeded: string, role: number, _duration: string, data: string) => {
265 10 : if (role !== PK.Enum.ROLE_UPDATE_PACKAGES)
266 10 : 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 10 : let timestamp = Date.parse(timeSpec);
279 2 : 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 2 : const shortoff_regex = /[-+]\d\d$/;
288 2 : if (shortoff_regex.exec(timeSpec)) {
289 2 : const fixed_timeSpec = timeSpec + ":00";
290 : // console.debug(`Transaction has an invalid timeSpec=${timeSpec}, trying fixed=${fixed_timeSpec}`);
291 2 : timestamp = Date.parse(fixed_timeSpec);
292 2 : }
293 2 : }
294 :
295 1 : if (isNaN(timestamp)) {
296 1 : console.warn(`Transaction has an invalid timeSpec=${timeSpec}, skipping`);
297 1 : return;
298 1 : }
299 :
300 10 : const pkgs = { timestamp, packages: {} } as History;
301 10 : let empty = true;
302 10 : data.split("\n").forEach(line => {
303 10 : const fields = line.trim().split("\t");
304 10 : if (fields.length >= 2) {
305 10 : const pkgId = fields[1].split(";");
306 10 : pkgs.packages[pkgId[0]] = pkgId[1];
307 10 : empty = false;
308 10 : }
309 10 : });
310 :
311 10 : if (!empty)
312 10 : history.unshift(pkgs); // PK reports in time-ascending order, but we want the latest first
313 10 : },
314 19 : });
315 :
316 18 : return history;
317 19 : }
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 19 : async is_available(pkgnames: string[]): Promise<boolean> {
326 19 : const available = new Set();
327 :
328 19 : if (pkgnames && pkgnames.length === 0)
329 3 : return true;
330 :
331 19 : await PK.cancellableTransaction("Resolve",
332 19 : [PK.Enum.FILTER_ARCH | PK.Enum.FILTER_NEWEST | PK.Enum.FILTER_NOT_INSTALLED, pkgnames],
333 19 : null,
334 19 : {
335 0 : Package: (_info: unknown, package_id: string) => {
336 0 : const pkgname = package_id.split(";")[0];
337 0 : available.add(pkgname);
338 0 : },
339 19 : });
340 :
341 19 : return available.size === new Set(pkgnames).size;
342 19 : }
343 282 : }
|