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
33using namespace BPackageKit::BPrivate;
34
35
36PackageManager::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
48PackageManager::~PackageManager()
49{
50}
51
52
53void
54PackageManager::SetInteractive(bool interactive)
55{
56	fInteractive = interactive;
57	fDecisionProvider.SetInteractive(interactive);
58}
59
60
61void
62PackageManager::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
72void
73PackageManager::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
137void
138PackageManager::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
158void
159PackageManager::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
173void
174PackageManager::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
190void
191PackageManager::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
267void
268PackageManager::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
285void
286PackageManager::ProgressPackageChecksumStarted(const char* title)
287{
288	printf("%s...", title);
289}
290
291
292void
293PackageManager::ProgressPackageChecksumComplete(const char* title)
294{
295	printf("done.\n");
296}
297
298
299void
300PackageManager::ProgressStartApplyingChanges(InstalledRepository& repository)
301{
302	printf("[%s] Applying changes ...\n", repository.Name().String());
303}
304
305
306void
307PackageManager::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
330void
331PackageManager::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
340void
341PackageManager::_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