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