1 /*
2 * Copyright 2013, Stephan Aßmus <superstippi@gmx.de>.
3 * Copyright 2017-2024, Andrew Lindesay <apl@lindesay.co.nz>.
4 * All rights reserved. Distributed under the terms of the MIT License.
5 */
6
7
8 #include "App.h"
9
10 #include <stdio.h>
11
12 #include <Alert.h>
13 #include <Catalog.h>
14 #include <Entry.h>
15 #include <Message.h>
16 #include <package/PackageDefs.h>
17 #include <package/PackageInfo.h>
18 #include <package/PackageRoster.h>
19 #include <Path.h>
20 #include <Roster.h>
21 #include <Screen.h>
22 #include <String.h>
23
24 #include "support.h"
25
26 #include "AppUtils.h"
27 #include "Logger.h"
28 #include "MainWindow.h"
29 #include "PackageUtils.h"
30 #include "ServerHelper.h"
31 #include "ServerSettings.h"
32 #include "SharedIcons.h"
33 #include "StorageUtils.h"
34
35
36 #undef B_TRANSLATION_CONTEXT
37 #define B_TRANSLATION_CONTEXT "App"
38
39
App()40 App::App()
41 :
42 BApplication("application/x-vnd.Haiku-HaikuDepot"),
43 fMainWindow(NULL),
44 fWindowCount(0),
45 fSettingsRead(false)
46 {
47 srand((unsigned int) time(NULL));
48 _CheckPackageDaemonRuns();
49 fIsFirstRun = _CheckIsFirstRun();
50 }
51
52
~App()53 App::~App()
54 {
55 // We cannot let global destructors cleanup static BitmapRef objects,
56 // since calling BBitmap destructors needs a valid BApplication still
57 // around. That's why we do it here.
58 SharedIcons::UnsetAllIcons();
59 }
60
61
62 bool
QuitRequested()63 App::QuitRequested()
64 {
65 if (fMainWindow != NULL
66 && fMainWindow->LockLooperWithTimeout(1500000) == B_OK) {
67 BMessage windowSettings;
68 fMainWindow->StoreSettings(windowSettings);
69
70 fMainWindow->UnlockLooper();
71
72 _StoreSettings(windowSettings);
73 }
74
75 return BApplication::QuitRequested();
76 }
77
78
79 void
ReadyToRun()80 App::ReadyToRun()
81 {
82 if (fWindowCount > 0)
83 return;
84
85 BMessage settings;
86 _LoadSettings(settings);
87
88 if (!_CheckTestFile()) {
89 Quit();
90 return;
91 }
92
93 _ClearCacheOnVersionChange();
94
95 fMainWindow = new MainWindow(settings);
96 _ShowWindow(fMainWindow);
97 }
98
99
100 bool
IsFirstRun()101 App::IsFirstRun()
102 {
103 return fIsFirstRun;
104 }
105
106
107 void
MessageReceived(BMessage * message)108 App::MessageReceived(BMessage* message)
109 {
110 switch (message->what) {
111 case MSG_MAIN_WINDOW_CLOSED:
112 {
113 BMessage windowSettings;
114 if (message->FindMessage(KEY_WINDOW_SETTINGS,
115 &windowSettings) == B_OK) {
116 _StoreSettings(windowSettings);
117 }
118
119 fWindowCount--;
120 if (fWindowCount == 0)
121 Quit();
122 break;
123 }
124
125 case MSG_CLIENT_TOO_OLD:
126 ServerHelper::AlertClientTooOld(message);
127 break;
128
129 case MSG_NETWORK_TRANSPORT_ERROR:
130 ServerHelper::AlertTransportError(message);
131 break;
132
133 case MSG_SERVER_ERROR:
134 ServerHelper::AlertServerJsonRpcError(message);
135 break;
136
137 case MSG_ALERT_SIMPLE_ERROR:
138 _AlertSimpleError(message);
139 break;
140
141 case MSG_SERVER_DATA_CHANGED:
142 fMainWindow->PostMessage(message);
143 break;
144
145 default:
146 BApplication::MessageReceived(message);
147 break;
148 }
149 }
150
151
152 void
RefsReceived(BMessage * message)153 App::RefsReceived(BMessage* message)
154 {
155 entry_ref ref;
156 int32 index = 0;
157 while (message->FindRef("refs", index++, &ref) == B_OK) {
158 BEntry entry(&ref, true);
159 _Open(entry);
160 }
161 }
162
163
164 enum arg_switch {
165 UNKNOWN_SWITCH,
166 NOT_SWITCH,
167 HELP_SWITCH,
168 WEB_APP_BASE_URL_SWITCH,
169 VERBOSITY_SWITCH,
170 FORCE_NO_NETWORKING_SWITCH,
171 PREFER_CACHE_SWITCH,
172 DROP_CACHE_SWITCH
173 };
174
175
176 static void
app_print_help()177 app_print_help()
178 {
179 fprintf(stdout, "HaikuDepot ");
180 fprintf(stdout, "[-u|--webappbaseurl <web-app-base-url>]\n");
181 fprintf(stdout, "[-v|--verbosity [off|info|debug|trace]\n");
182 fprintf(stdout, "[--nonetworking]\n");
183 fprintf(stdout, "[--prefercache]\n");
184 fprintf(stdout, "[--dropcache]\n");
185 fprintf(stdout, "[-h|--help]\n");
186 fprintf(stdout, "\n");
187 fprintf(stdout, "'-h' : causes this help text to be printed out.\n");
188 fprintf(stdout, "'-v' : allows for the verbosity level to be set.\n");
189 fprintf(stdout, "'-u' : allows for the haiku depot server url to be\n");
190 fprintf(stdout, " configured.\n");
191 fprintf(stdout, "'--nonetworking' : prevents network access.\n");
192 fprintf(stdout, "'--prefercache' : prefer to get data from cache rather\n");
193 fprintf(stdout, " then obtain data from the network.**\n");
194 fprintf(stdout, "'--dropcache' : drop cached data before performing\n");
195 fprintf(stdout, " bulk operations.**\n");
196 fprintf(stdout, "\n");
197 fprintf(stdout, "** = only applies to bulk operations.\n");
198 }
199
200
201 static arg_switch
app_resolve_switch(char * arg)202 app_resolve_switch(char *arg)
203 {
204 int arglen = strlen(arg);
205
206 if (arglen > 0 && arg[0] == '-') {
207
208 if (arglen > 3 && arg[1] == '-') { // long form
209 if (0 == strcmp(&arg[2], "webappbaseurl"))
210 return WEB_APP_BASE_URL_SWITCH;
211
212 if (0 == strcmp(&arg[2], "help"))
213 return HELP_SWITCH;
214
215 if (0 == strcmp(&arg[2], "verbosity"))
216 return VERBOSITY_SWITCH;
217
218 if (0 == strcmp(&arg[2], "nonetworking"))
219 return FORCE_NO_NETWORKING_SWITCH;
220
221 if (0 == strcmp(&arg[2], "prefercache"))
222 return PREFER_CACHE_SWITCH;
223
224 if (0 == strcmp(&arg[2], "dropcache"))
225 return DROP_CACHE_SWITCH;
226 } else {
227 if (arglen == 2) { // short form
228 switch (arg[1]) {
229 case 'u':
230 return WEB_APP_BASE_URL_SWITCH;
231
232 case 'h':
233 return HELP_SWITCH;
234
235 case 'v':
236 return VERBOSITY_SWITCH;
237 }
238 }
239 }
240
241 return UNKNOWN_SWITCH;
242 }
243
244 return NOT_SWITCH;
245 }
246
247
248 void
ArgvReceived(int32 argc,char * argv[])249 App::ArgvReceived(int32 argc, char* argv[])
250 {
251 for (int i = 1; i < argc;) {
252
253 // check to make sure that if there is a value for the switch,
254 // that the value is in fact supplied.
255
256 switch (app_resolve_switch(argv[i])) {
257 case VERBOSITY_SWITCH:
258 case WEB_APP_BASE_URL_SWITCH:
259 if (i == argc-1) {
260 fprintf(stdout, "unexpected end of arguments; missing "
261 "value for switch [%s]\n", argv[i]);
262 Quit();
263 return;
264 }
265 break;
266
267 default:
268 break;
269 }
270
271 // now process each switch.
272
273 switch (app_resolve_switch(argv[i])) {
274
275 case VERBOSITY_SWITCH:
276 if (!Logger::SetLevelByName(argv[i+1])) {
277 fprintf(stdout, "unknown log level [%s]\n", argv[i + 1]);
278 Quit();
279 }
280 i++; // also move past the log level value
281 break;
282
283 case HELP_SWITCH:
284 app_print_help();
285 Quit();
286 break;
287
288 case WEB_APP_BASE_URL_SWITCH:
289 if (ServerSettings::SetBaseUrl(BUrl(argv[i + 1])) != B_OK) {
290 fprintf(stdout, "malformed web app base url; %s\n",
291 argv[i + 1]);
292 Quit();
293 }
294 else {
295 fprintf(stdout, "did configure the web base url; %s\n",
296 argv[i + 1]);
297 }
298
299 i++; // also move past the url value
300
301 break;
302
303 case FORCE_NO_NETWORKING_SWITCH:
304 ServerSettings::SetForceNoNetwork(true);
305 break;
306
307 case PREFER_CACHE_SWITCH:
308 ServerSettings::SetPreferCache(true);
309 break;
310
311 case DROP_CACHE_SWITCH:
312 ServerSettings::SetDropCache(true);
313 break;
314
315 case NOT_SWITCH:
316 {
317 BEntry entry(argv[i], true);
318 _Open(entry);
319 break;
320 }
321
322 case UNKNOWN_SWITCH:
323 fprintf(stdout, "unknown switch; %s\n", argv[i]);
324 Quit();
325 break;
326 }
327
328 i++; // move on at least one arg
329 }
330 }
331
332
333 /*! This method will display an alert based on a message. This message arrives
334 from a number of possible background threads / processes in the application.
335 */
336
337 void
_AlertSimpleError(BMessage * message)338 App::_AlertSimpleError(BMessage* message)
339 {
340 BString alertTitle;
341 BString alertText;
342 int32 typeInt;
343
344 if (message->FindString(KEY_ALERT_TEXT, &alertText) != B_OK)
345 alertText = "?";
346
347 if (message->FindString(KEY_ALERT_TITLE, &alertTitle) != B_OK)
348 alertTitle = B_TRANSLATE("Error");
349
350 if (message->FindInt32(KEY_ALERT_TYPE, &typeInt) != B_OK)
351 typeInt = B_INFO_ALERT;
352
353 BAlert* alert = new BAlert(alertTitle, alertText, B_TRANSLATE("OK"),
354 NULL, NULL, B_WIDTH_AS_USUAL, static_cast<alert_type>(typeInt));
355
356 alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
357 alert->Go();
358 }
359
360
361 // #pragma mark - private
362
363
364 void
_Open(const BEntry & entry)365 App::_Open(const BEntry& entry)
366 {
367 BPath path;
368 if (!entry.Exists() || entry.GetPath(&path) != B_OK) {
369 fprintf(stderr, "Package file not found: %s\n", path.Path());
370 return;
371 }
372
373 // Try to parse package file via Package Kit
374 BPackageKit::BPackageInfo info;
375 status_t status = info.ReadFromPackageFile(path.Path());
376 if (status != B_OK) {
377 fprintf(stderr, "Failed to parse package file: %s\n",
378 strerror(status));
379 return;
380 }
381
382 // Transfer information into PackageInfo
383 PackageInfoRef package(new(std::nothrow) PackageInfo(info), true);
384 if (!package.IsSet()) {
385 fprintf(stderr, "Could not allocate PackageInfo\n");
386 return;
387 }
388
389 PackageLocalInfoRef localInfo = PackageUtils::NewLocalInfo(package);
390 localInfo->SetLocalFilePath(path.Path());
391
392 // Set if the package is active
393 //
394 // TODO(leavengood): It is very awkward having to check these two locations
395 // here, and in many other places in HaikuDepot. Why do clients of the
396 // package kit have to know about these locations?
397 bool active = false;
398 BPackageKit::BPackageRoster roster;
399 status = roster.IsPackageActive(
400 BPackageKit::B_PACKAGE_INSTALLATION_LOCATION_SYSTEM, info, &active);
401 if (status != B_OK) {
402 fprintf(stderr, "Could not check if package was active in system: %s\n",
403 strerror(status));
404 return;
405 }
406 if (!active) {
407 status = roster.IsPackageActive(
408 BPackageKit::B_PACKAGE_INSTALLATION_LOCATION_HOME, info, &active);
409 if (status != B_OK) {
410 fprintf(stderr,
411 "Could not check if package was active in home: %s\n",
412 strerror(status));
413 return;
414 }
415 }
416
417 if (active)
418 localInfo->SetState(ACTIVATED);
419
420 package->SetLocalInfo(localInfo);
421
422 BMessage settings;
423 _LoadSettings(settings);
424
425 MainWindow* window = new MainWindow(settings, package);
426 _ShowWindow(window);
427 }
428
429
430 void
_ShowWindow(MainWindow * window)431 App::_ShowWindow(MainWindow* window)
432 {
433 window->Show();
434 fWindowCount++;
435 }
436
437
438 bool
_LoadSettings(BMessage & settings)439 App::_LoadSettings(BMessage& settings)
440 {
441 if (!fSettingsRead) {
442 fSettingsRead = true;
443 if (load_settings(&fSettings, KEY_MAIN_SETTINGS, "HaikuDepot") != B_OK)
444 fSettings.MakeEmpty();
445 }
446 settings = fSettings;
447 return !fSettings.IsEmpty();
448 }
449
450
451 void
_StoreSettings(const BMessage & settings)452 App::_StoreSettings(const BMessage& settings)
453 {
454 // Take what is in settings and replace data under the same name in
455 // fSettings, leaving anything in fSettings that is not contained in
456 // settings.
457 int32 i = 0;
458
459 char* name;
460 type_code type;
461 int32 count;
462
463 while (settings.GetInfo(B_ANY_TYPE, i++, &name, &type, &count) == B_OK) {
464 fSettings.RemoveName(name);
465 for (int32 j = 0; j < count; j++) {
466 const void* data;
467 ssize_t size;
468 if (settings.FindData(name, type, j, &data, &size) != B_OK)
469 break;
470 fSettings.AddData(name, type, data, size);
471 }
472 }
473
474 save_settings(&fSettings, KEY_MAIN_SETTINGS, "HaikuDepot");
475 }
476
477
478 // #pragma mark -
479
480
481 static const char* kPackageDaemonSignature
482 = "application/x-vnd.haiku-package_daemon";
483
484 void
_CheckPackageDaemonRuns()485 App::_CheckPackageDaemonRuns()
486 {
487 while (!be_roster->IsRunning(kPackageDaemonSignature)) {
488 BAlert* alert = new BAlert(
489 B_TRANSLATE("Start package daemon"),
490 B_TRANSLATE("HaikuDepot needs the package daemon to function, "
491 "and it appears to be not running.\n"
492 "Would you like to start it now?"),
493 B_TRANSLATE("No, quit HaikuDepot"),
494 B_TRANSLATE("Start package daemon"), NULL, B_WIDTH_AS_USUAL,
495 B_WARNING_ALERT);
496 alert->SetShortcut(0, B_ESCAPE);
497
498 if (alert->Go() == 0)
499 HDFATAL("unable to start without the package daemon running");
500
501 if (!_LaunchPackageDaemon())
502 break;
503 }
504 }
505
506
507 bool
_LaunchPackageDaemon()508 App::_LaunchPackageDaemon()
509 {
510 status_t ret = be_roster->Launch(kPackageDaemonSignature);
511 if (ret != B_OK) {
512 BString errorMessage
513 = B_TRANSLATE("Starting the package daemon failed:\n\n%Error%");
514 errorMessage.ReplaceAll("%Error%", strerror(ret));
515
516 BAlert* alert = new BAlert(
517 B_TRANSLATE("Package daemon problem"), errorMessage,
518 B_TRANSLATE("Quit HaikuDepot"),
519 B_TRANSLATE("Try again"), NULL, B_WIDTH_AS_USUAL,
520 B_WARNING_ALERT);
521 alert->SetShortcut(0, B_ESCAPE);
522
523 if (alert->Go() == 0)
524 return false;
525 }
526 // TODO: Would be nice to send a message to the package daemon instead
527 // and get a reply once it is ready.
528 snooze(2000000);
529 return true;
530 }
531
532
533 /*static*/ bool
_CheckIsFirstRun()534 App::_CheckIsFirstRun()
535 {
536 BPath testFilePath;
537 bool exists = false;
538 status_t status = StorageUtils::LocalWorkingFilesPath("testfile.txt",
539 testFilePath, false);
540 if (status != B_OK) {
541 HDERROR("unable to establish the location of the test file");
542 }
543 else
544 status = StorageUtils::ExistsObject(testFilePath, &exists, NULL, NULL);
545 return !exists;
546 }
547
548
549 /*! \brief Checks to ensure that a working file is able to be written.
550 \return false if the startup should be stopped and the application should
551 quit.
552 */
553
554 bool
_CheckTestFile()555 App::_CheckTestFile()
556 {
557 BPath testFilePath;
558 BString pathDescription = "???";
559 status_t result = StorageUtils::LocalWorkingFilesPath("testfile.txt",
560 testFilePath, false);
561
562 if (result == B_OK) {
563 pathDescription = testFilePath.Path();
564 result = StorageUtils::CheckCanWriteTo(testFilePath);
565 }
566
567 if (result != B_OK) {
568 StorageUtils::SetWorkingFilesUnavailable();
569
570 BString msg = B_TRANSLATE("This application writes and reads some"
571 " working files on your computer in order to function. It appears"
572 " that there are problems writing a test file at [%TestFilePath%]."
573 " Check that there are no issues with your local disk or"
574 " permissions that might prevent this application from writing"
575 " files into that directory location. You may choose to acknowledge"
576 " this problem and continue, but some functionality may be"
577 " disabled.");
578 msg.ReplaceAll("%TestFilePath%", pathDescription);
579
580 BAlert* alert = new(std::nothrow) BAlert(
581 B_TRANSLATE("Problem with working files"),
582 msg,
583 B_TRANSLATE("Quit"), B_TRANSLATE("Continue"));
584
585 if (alert->Go() == 0)
586 return false;
587 }
588
589 return true;
590 }
591
592
593 /*! This method will check to see if the version of the application has changed.
594 If it has changed then it will delete all of the contents of the cache
595 directory. This will mean that when application logic changes, it need not
596 bother to migrate the cached files. Also any old cached files will be
597 cleared out that no longer serve any purpose.
598
599 Errors arising in this logic need not prevent the application from failing
600 to start as this is just a clean-up.
601 */
602
603 void
_ClearCacheOnVersionChange()604 App::_ClearCacheOnVersionChange()
605 {
606 BString version;
607
608 if (AppUtils::GetAppVersionString(version) != B_OK) {
609 HDERROR("clear cache; unable to get the application version");
610 return;
611 }
612
613 BPath lastVersionPath;
614 if (StorageUtils::LocalWorkingFilesPath(
615 "version.txt", lastVersionPath) != B_OK) {
616 HDERROR("clear cache; unable to get version file path");
617 return;
618 }
619
620 bool exists;
621 off_t size;
622
623 if (StorageUtils::ExistsObject(
624 lastVersionPath, &exists, NULL, &size) != B_OK) {
625 HDERROR("clear cache; unable to check version file exists");
626 return;
627 }
628
629 BString lastVersion;
630
631 if (exists && StorageUtils::AppendToString(lastVersionPath, lastVersion)
632 != B_OK) {
633 HDERROR("clear cache; unable to read the version from [%s]",
634 lastVersionPath.Path());
635 return;
636 }
637
638 if (lastVersion != version) {
639 HDINFO("last version [%s] and current version [%s] do not match"
640 " -> will flush cache", lastVersion.String(), version.String());
641 StorageUtils::RemoveWorkingDirectoryContents();
642 HDINFO("will write version [%s] to [%s]",
643 version.String(), lastVersionPath.Path());
644 StorageUtils::AppendToFile(version, lastVersionPath);
645 } else {
646 HDINFO("last version [%s] and current version [%s] match"
647 " -> cache retained", lastVersion.String(), version.String());
648 }
649 }
650