xref: /haiku/src/bin/pkgman/PackageManager.cpp (revision 9f3bdf3d039430b5172c424def20ce5d9f7367d4)
1 /*
2  * Copyright 2013-2024, Haiku, Inc. All Rights Reserved.
3  * Distributed under the terms of the MIT License.
4  *
5  * Authors:
6  *		Axel Dörfler <axeld@pinc-software.de>
7  *		Rene Gollent <rene@gollent.com>
8  *		Ingo Weinhold <ingo_weinhold@gmx.de>
9  */
10 
11 
12 #include <StringForSize.h>
13 #include <StringForRate.h>
14 	// Must be first, or the BPrivate namespaces are confused
15 
16 #include "PackageManager.h"
17 
18 #include <InterfaceDefs.h>
19 
20 #include <sys/ioctl.h>
21 #include <unistd.h>
22 
23 #include <package/CommitTransactionResult.h>
24 #include <package/DownloadFileRequest.h>
25 #include <package/RefreshRepositoryRequest.h>
26 #include <package/solver/SolverPackage.h>
27 #include <package/solver/SolverProblem.h>
28 #include <package/solver/SolverProblemSolution.h>
29 
30 #include "pkgman.h"
31 
32 
33 using namespace BPackageKit::BPrivate;
34 
35 
36 PackageManager::PackageManager(BPackageInstallationLocation location,
37 	bool interactive)
38 	:
39 	BPackageManager(location, &fClientInstallationInterface, this),
40 	BPackageManager::UserInteractionHandler(),
41 	fDecisionProvider(interactive),
42 	fClientInstallationInterface(),
43 	fInteractive(interactive)
44 {
45 }
46 
47 
48 PackageManager::~PackageManager()
49 {
50 }
51 
52 
53 void
54 PackageManager::SetInteractive(bool interactive)
55 {
56 	fInteractive = interactive;
57 	fDecisionProvider.SetInteractive(interactive);
58 }
59 
60 
61 void
62 PackageManager::JobFailed(BSupportKit::BJob* job)
63 {
64 	BString error = job->ErrorString();
65 	if (error.Length() > 0) {
66 		error.ReplaceAll("\n", "\n*** ");
67 		fprintf(stderr, "%s", error.String());
68 	}
69 }
70 
71 
72 void
73 PackageManager::HandleProblems()
74 {
75 	printf("Encountered problems:\n");
76 
77 	int32 problemCount = fSolver->CountProblems();
78 	for (int32 i = 0; i < problemCount; i++) {
79 		// print problem and possible solutions
80 		BSolverProblem* problem = fSolver->ProblemAt(i);
81 		printf("problem %" B_PRId32 ": %s\n", i + 1,
82 			problem->ToString().String());
83 
84 		int32 solutionCount = problem->CountSolutions();
85 		for (int32 k = 0; k < solutionCount; k++) {
86 			const BSolverProblemSolution* solution = problem->SolutionAt(k);
87 			printf("  solution %" B_PRId32 ":\n", k + 1);
88 			int32 elementCount = solution->CountElements();
89 			for (int32 l = 0; l < elementCount; l++) {
90 				const BSolverProblemSolutionElement* element
91 					= solution->ElementAt(l);
92 				printf("    - %s\n", element->ToString().String());
93 			}
94 		}
95 
96 		if (!fInteractive)
97 			continue;
98 
99 		// let the user choose a solution
100 		printf("Please select a solution, skip the problem for now or quit.\n");
101 		for (;;) {
102 			if (solutionCount > 1)
103 				printf("select [1...%" B_PRId32 "/s/q]: ", solutionCount);
104 			else
105 				printf("select [1/s/q]: ");
106 
107 			char buffer[32];
108 			if (fgets(buffer, sizeof(buffer), stdin) == NULL
109 				|| strcmp(buffer, "q\n") == 0) {
110 				exit(1);
111 			}
112 
113 			if (strcmp(buffer, "s\n") == 0)
114 				break;
115 
116 			char* end;
117 			long selected = strtol(buffer, &end, 0);
118 			if (end == buffer || *end != '\n' || selected < 1
119 				|| selected > solutionCount) {
120 				printf("*** invalid input\n");
121 				continue;
122 			}
123 
124 			status_t error = fSolver->SelectProblemSolution(problem,
125 				problem->SolutionAt(selected - 1));
126 			if (error != B_OK)
127 				DIE(error, "failed to set solution");
128 			break;
129 		}
130 	}
131 
132 	if (problemCount > 0 && !fInteractive)
133 		exit(1);
134 }
135 
136 
137 void
138 PackageManager::ConfirmChanges(bool fromMostSpecific)
139 {
140 	printf("The following changes will be made:\n");
141 
142 	int32 count = fInstalledRepositories.CountItems();
143 	if (fromMostSpecific) {
144 		for (int32 i = count - 1; i >= 0; i--)
145 			_PrintResult(*fInstalledRepositories.ItemAt(i));
146 	} else {
147 		for (int32 i = 0; i < count; i++)
148 			_PrintResult(*fInstalledRepositories.ItemAt(i));
149 	}
150 
151 	if (!fDecisionProvider.YesNoDecisionNeeded(BString(), "Continue?", "yes",
152 			"no", "yes")) {
153 		exit(1);
154 	}
155 }
156 
157 
158 void
159 PackageManager::Warn(status_t error, const char* format, ...)
160 {
161 	va_list args;
162 	va_start(args, format);
163 	vfprintf(stderr, format, args);
164 	va_end(args);
165 
166 	if (error == B_OK)
167 		printf("\n");
168 	else
169 		printf(": %s\n", strerror(error));
170 }
171 
172 
173 void
174 PackageManager::ProgressPackageDownloadStarted(const char* packageName)
175 {
176 	fShowProgress = isatty(STDOUT_FILENO);
177 	fLastBytes = 0;
178 	fLastRateCalcTime = system_time();
179 	fDownloadRate = 0;
180 
181 	if (fShowProgress) {
182 		char percentString[32];
183 		fNumberFormat.FormatPercent(percentString, sizeof(percentString), 0.0);
184 		// Make sure there is enough space for '100 %' percent format
185 		printf("%6s", percentString);
186 	}
187 }
188 
189 
190 void
191 PackageManager::ProgressPackageDownloadActive(const char* packageName,
192 	float completionPercentage, off_t bytes, off_t totalBytes)
193 {
194 	if (bytes == totalBytes)
195 		fLastBytes = totalBytes;
196 	if (!fShowProgress)
197 		return;
198 
199 	// Do not update if nothing changed in the last 500ms
200 	if (bytes <= fLastBytes || (system_time() - fLastRateCalcTime) < 500000)
201 		return;
202 
203 	const bigtime_t time = system_time();
204 	if (time != fLastRateCalcTime) {
205 		fDownloadRate = (bytes - fLastBytes) * 1000000
206 			/ (time - fLastRateCalcTime);
207 	}
208 	fLastRateCalcTime = time;
209 	fLastBytes = bytes;
210 
211 	// Build the current file progress percentage and size string
212 	BString leftStr;
213 	BString rightStr;
214 
215 	int width = 70;
216 	struct winsize winSize;
217 	if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &winSize) == 0)
218 		width = std::min(winSize.ws_col - 2, 78);
219 
220 	if (width < 30) {
221 		// Not much space for anything, just draw a percentage
222 		fNumberFormat.FormatPercent(leftStr, completionPercentage);
223 	} else {
224 		BString dataString;
225 		fNumberFormat.FormatPercent(dataString, completionPercentage);
226 		// Make sure there is enough space for '100 %' percent format
227 		leftStr.SetToFormat("%6s %s", dataString.String(), packageName);
228 
229 		char byteBuffer[32];
230 		char totalBuffer[32];
231 		char rateBuffer[32];
232 		rightStr.SetToFormat("%s/%s  %s ",
233 				string_for_size(bytes, byteBuffer, sizeof(byteBuffer)),
234 				string_for_size(totalBytes, totalBuffer, sizeof(totalBuffer)),
235 				fDownloadRate == 0 ? "--.-" :
236 				string_for_rate(fDownloadRate, rateBuffer, sizeof(rateBuffer)));
237 
238 		if (leftStr.CountChars() + rightStr.CountChars() >= width)
239 		{
240 			// The full string does not fit! Try to make a shorter one.
241 			leftStr.ReplaceLast(".hpkg", "");
242 			leftStr.TruncateChars(width - rightStr.CountChars() - 2);
243 			leftStr.Append(B_UTF8_ELLIPSIS " ");
244 		}
245 
246 		int extraSpace = width - leftStr.CountChars() - rightStr.CountChars();
247 
248 		leftStr.Append(' ', extraSpace);
249 		leftStr.Append(rightStr);
250 	}
251 
252 	const int progChars = leftStr.CountBytes(0,
253 		(int)(width * completionPercentage));
254 
255 	// Set bg to green, fg to white, and print progress bar.
256 	// Then reset colors and print rest of text
257 	// And finally remove any stray chars at the end of the line
258 	printf("\r\x1B[42;37m%.*s\x1B[0m%s\x1B[K", progChars, leftStr.String(),
259 		leftStr.String() + progChars);
260 
261 	// Force terminal to update when the line is complete, to avoid flickering
262 	// because of updates at random times
263 	fflush(stdout);
264 }
265 
266 
267 void
268 PackageManager::ProgressPackageDownloadComplete(const char* packageName)
269 {
270 	if (fShowProgress) {
271 		// Erase the line, return to the start, and reset colors
272 		printf("\r\33[2K\r\x1B[0m");
273 	}
274 
275 	char byteBuffer[32];
276 	char percentString[32];
277 	fNumberFormat.FormatPercent(percentString, sizeof(percentString), 1.0);
278 	// Make sure there is enough space for '100 %' percent format
279 	printf("%6s %s [%s]\n", percentString, packageName,
280 		string_for_size(fLastBytes, byteBuffer, sizeof(byteBuffer)));
281 	fflush(stdout);
282 }
283 
284 
285 void
286 PackageManager::ProgressPackageChecksumStarted(const char* title)
287 {
288 	printf("%s...", title);
289 }
290 
291 
292 void
293 PackageManager::ProgressPackageChecksumComplete(const char* title)
294 {
295 	printf("done.\n");
296 }
297 
298 
299 void
300 PackageManager::ProgressStartApplyingChanges(InstalledRepository& repository)
301 {
302 	printf("[%s] Applying changes ...\n", repository.Name().String());
303 }
304 
305 
306 void
307 PackageManager::ProgressTransactionCommitted(InstalledRepository& repository,
308 	const BCommitTransactionResult& result)
309 {
310 	const char* repositoryName = repository.Name().String();
311 
312 	int32 issueCount = result.CountIssues();
313 	for (int32 i = 0; i < issueCount; i++) {
314 		const BTransactionIssue* issue = result.IssueAt(i);
315 		if (issue->PackageName().IsEmpty()) {
316 			printf("[%s] warning: %s\n", repositoryName,
317 				issue->ToString().String());
318 		} else {
319 			printf("[%s] warning: package %s: %s\n", repositoryName,
320 				issue->PackageName().String(), issue->ToString().String());
321 		}
322 	}
323 
324 	printf("[%s] Changes applied. Old activation state backed up in \"%s\"\n",
325 		repositoryName, result.OldStateDirectory().String());
326 	printf("[%s] Cleaning up ...\n", repositoryName);
327 }
328 
329 
330 void
331 PackageManager::ProgressApplyingChangesDone(InstalledRepository& repository)
332 {
333 	printf("[%s] Done.\n", repository.Name().String());
334 
335 	if (BPackageRoster().IsRebootNeeded())
336 		printf("A reboot is necessary to complete the installation process.\n");
337 }
338 
339 
340 void
341 PackageManager::_PrintResult(InstalledRepository& installationRepository)
342 {
343 	if (!installationRepository.HasChanges())
344 		return;
345 
346 	printf("  in %s:\n", installationRepository.Name().String());
347 
348 	PackageList& packagesToActivate
349 		= installationRepository.PackagesToActivate();
350 	PackageList& packagesToDeactivate
351 		= installationRepository.PackagesToDeactivate();
352 
353 	BStringList upgradedPackages;
354 	BStringList upgradedPackageVersions;
355 	for (int32 i = 0;
356 		BSolverPackage* installPackage = packagesToActivate.ItemAt(i);
357 		i++) {
358 		for (int32 j = 0;
359 			BSolverPackage* uninstallPackage = packagesToDeactivate.ItemAt(j);
360 			j++) {
361 			if (installPackage->Info().Name() == uninstallPackage->Info().Name()) {
362 				upgradedPackages.Add(installPackage->Info().Name());
363 				upgradedPackageVersions.Add(uninstallPackage->Info().Version().ToString());
364 				break;
365 			}
366 		}
367 	}
368 
369 	for (int32 i = 0; BSolverPackage* package = packagesToActivate.ItemAt(i);
370 		i++) {
371 		BString repository;
372 		if (dynamic_cast<MiscLocalRepository*>(package->Repository()) != NULL)
373 			repository = "local file";
374 		else
375 			repository.SetToFormat("repository %s", package->Repository()->Name().String());
376 
377 		int position = upgradedPackages.IndexOf(package->Info().Name());
378 		if (position >= 0) {
379 			printf("    upgrade package %s-%s to %s from %s\n",
380 				package->Info().Name().String(),
381 				upgradedPackageVersions.StringAt(position).String(),
382 				package->Info().Version().ToString().String(),
383 				repository.String());
384 		} else {
385 			printf("    install package %s-%s from %s\n",
386 				package->Info().Name().String(),
387 				package->Info().Version().ToString().String(),
388 				repository.String());
389 		}
390 	}
391 
392 	for (int32 i = 0; BSolverPackage* package = packagesToDeactivate.ItemAt(i);
393 		i++) {
394 		if (upgradedPackages.HasString(package->Info().Name()))
395 			continue;
396 		printf("    uninstall package %s\n", package->VersionedName().String());
397 	}
398 // TODO: Print file/download sizes. Unfortunately our package infos don't
399 // contain the file size. Which is probably correct. The file size (and possibly
400 // other information) should, however, be provided by the repository cache in
401 // some way. Extend BPackageInfo? Create a BPackageFileInfo?
402 }
403