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 int32 typeInt; 346 347 if (message->FindString(KEY_ALERT_TEXT, &alertText) != B_OK) 348 alertText = "?"; 349 350 if (message->FindString(KEY_ALERT_TITLE, &alertTitle) != B_OK) 351 alertTitle = B_TRANSLATE("Error"); 352 353 if (message->FindInt32(KEY_ALERT_TYPE, &typeInt) != B_OK) 354 typeInt = B_INFO_ALERT; 355 356 BAlert* alert = new BAlert(alertTitle, alertText, B_TRANSLATE("OK"), 357 NULL, NULL, B_WIDTH_AS_USUAL, static_cast<alert_type>(typeInt)); 358 359 alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE); 360 alert->Go(); 361 } 362 363 364 // #pragma mark - private 365 366 367 void 368 App::_Open(const BEntry& entry) 369 { 370 BPath path; 371 if (!entry.Exists() || entry.GetPath(&path) != B_OK) { 372 fprintf(stderr, "Package file not found: %s\n", path.Path()); 373 return; 374 } 375 376 // Try to parse package file via Package Kit 377 BPackageKit::BPackageInfo info; 378 status_t status = info.ReadFromPackageFile(path.Path()); 379 if (status != B_OK) { 380 fprintf(stderr, "Failed to parse package file: %s\n", 381 strerror(status)); 382 return; 383 } 384 385 // Transfer information into PackageInfo 386 PackageInfoRef package(new(std::nothrow) PackageInfo(info), true); 387 if (!package.IsSet()) { 388 fprintf(stderr, "Could not allocate PackageInfo\n"); 389 return; 390 } 391 392 package->SetLocalFilePath(path.Path()); 393 394 // Set if the package is active 395 // 396 // TODO(leavengood): It is very awkward having to check these two locations 397 // here, and in many other places in HaikuDepot. Why do clients of the 398 // package kit have to know about these locations? 399 bool active = false; 400 BPackageKit::BPackageRoster roster; 401 status = roster.IsPackageActive( 402 BPackageKit::B_PACKAGE_INSTALLATION_LOCATION_SYSTEM, info, &active); 403 if (status != B_OK) { 404 fprintf(stderr, "Could not check if package was active in system: %s\n", 405 strerror(status)); 406 return; 407 } 408 if (!active) { 409 status = roster.IsPackageActive( 410 BPackageKit::B_PACKAGE_INSTALLATION_LOCATION_HOME, info, &active); 411 if (status != B_OK) { 412 fprintf(stderr, 413 "Could not check if package was active in home: %s\n", 414 strerror(status)); 415 return; 416 } 417 } 418 419 if (active) { 420 package->SetState(ACTIVATED); 421 } 422 423 BMessage settings; 424 _LoadSettings(settings); 425 426 MainWindow* window = new MainWindow(settings, package); 427 _ShowWindow(window); 428 } 429 430 431 void 432 App::_ShowWindow(MainWindow* window) 433 { 434 window->Show(); 435 fWindowCount++; 436 } 437 438 439 bool 440 App::_LoadSettings(BMessage& settings) 441 { 442 if (!fSettingsRead) { 443 fSettingsRead = true; 444 if (load_settings(&fSettings, KEY_MAIN_SETTINGS, "HaikuDepot") != B_OK) 445 fSettings.MakeEmpty(); 446 } 447 settings = fSettings; 448 return !fSettings.IsEmpty(); 449 } 450 451 452 void 453 App::_StoreSettings(const BMessage& settings) 454 { 455 // Take what is in settings and replace data under the same name in 456 // fSettings, leaving anything in fSettings that is not contained in 457 // settings. 458 int32 i = 0; 459 460 char* name; 461 type_code type; 462 int32 count; 463 464 while (settings.GetInfo(B_ANY_TYPE, i++, &name, &type, &count) == B_OK) { 465 fSettings.RemoveName(name); 466 for (int32 j = 0; j < count; j++) { 467 const void* data; 468 ssize_t size; 469 if (settings.FindData(name, type, j, &data, &size) != B_OK) 470 break; 471 fSettings.AddData(name, type, data, size); 472 } 473 } 474 475 save_settings(&fSettings, KEY_MAIN_SETTINGS, "HaikuDepot"); 476 } 477 478 479 // #pragma mark - 480 481 482 static const char* kPackageDaemonSignature 483 = "application/x-vnd.haiku-package_daemon"; 484 485 void 486 App::_CheckPackageDaemonRuns() 487 { 488 while (!be_roster->IsRunning(kPackageDaemonSignature)) { 489 BAlert* alert = new BAlert( 490 B_TRANSLATE("Start package daemon"), 491 B_TRANSLATE("HaikuDepot needs the package daemon to function, " 492 "and it appears to be not running.\n" 493 "Would you like to start it now?"), 494 B_TRANSLATE("No, quit HaikuDepot"), 495 B_TRANSLATE("Start package daemon"), NULL, B_WIDTH_AS_USUAL, 496 B_WARNING_ALERT); 497 alert->SetShortcut(0, B_ESCAPE); 498 499 if (alert->Go() == 0) 500 HDFATAL("unable to start without the package daemon running"); 501 502 if (!_LaunchPackageDaemon()) 503 break; 504 } 505 } 506 507 508 bool 509 App::_LaunchPackageDaemon() 510 { 511 status_t ret = be_roster->Launch(kPackageDaemonSignature); 512 if (ret != B_OK) { 513 BString errorMessage 514 = B_TRANSLATE("Starting the package daemon failed:\n\n%Error%"); 515 errorMessage.ReplaceAll("%Error%", strerror(ret)); 516 517 BAlert* alert = new BAlert( 518 B_TRANSLATE("Package daemon problem"), errorMessage, 519 B_TRANSLATE("Quit HaikuDepot"), 520 B_TRANSLATE("Try again"), NULL, B_WIDTH_AS_USUAL, 521 B_WARNING_ALERT); 522 alert->SetShortcut(0, B_ESCAPE); 523 524 if (alert->Go() == 0) 525 return false; 526 } 527 // TODO: Would be nice to send a message to the package daemon instead 528 // and get a reply once it is ready. 529 snooze(2000000); 530 return true; 531 } 532 533 534 /*static*/ bool 535 App::_CheckIsFirstRun() 536 { 537 BPath testFilePath; 538 bool exists = false; 539 status_t status = StorageUtils::LocalWorkingFilesPath("testfile.txt", 540 testFilePath, false); 541 if (status != B_OK) { 542 HDERROR("unable to establish the location of the test file"); 543 } 544 else 545 status = StorageUtils::ExistsObject(testFilePath, &exists, NULL, NULL); 546 return !exists; 547 } 548 549 550 /*! \brief Checks to ensure that a working file is able to be written. 551 \return false if the startup should be stopped and the application should 552 quit. 553 */ 554 555 bool 556 App::_CheckTestFile() 557 { 558 BPath testFilePath; 559 BString pathDescription = "???"; 560 status_t result = StorageUtils::LocalWorkingFilesPath("testfile.txt", 561 testFilePath, false); 562 563 if (result == B_OK) { 564 pathDescription = testFilePath.Path(); 565 result = StorageUtils::CheckCanWriteTo(testFilePath); 566 } 567 568 if (result != B_OK) { 569 StorageUtils::SetWorkingFilesUnavailable(); 570 571 BString msg = B_TRANSLATE("This application writes and reads some" 572 " working files on your computer in order to function. It appears" 573 " that there are problems writing a test file at [%TestFilePath%]." 574 " Check that there are no issues with your local disk or" 575 " permissions that might prevent this application from writing" 576 " files into that directory location. You may choose to acknowledge" 577 " this problem and continue, but some functionality may be" 578 " disabled."); 579 msg.ReplaceAll("%TestFilePath%", pathDescription); 580 581 BAlert* alert = new(std::nothrow) BAlert( 582 B_TRANSLATE("Problem with working files"), 583 msg, 584 B_TRANSLATE("Quit"), B_TRANSLATE("Continue")); 585 586 if (alert->Go() == 0) 587 return false; 588 } 589 590 return true; 591 } 592 593 594 /*! This method will check to see if the version of the application has changed. 595 If it has changed then it will delete all of the contents of the cache 596 directory. This will mean that when application logic changes, it need not 597 bother to migrate the cached files. Also any old cached files will be 598 cleared out that no longer serve any purpose. 599 600 Errors arising in this logic need not prevent the application from failing 601 to start as this is just a clean-up. 602 */ 603 604 void 605 App::_ClearCacheOnVersionChange() 606 { 607 BString version; 608 609 if (AppUtils::GetAppVersionString(version) != B_OK) { 610 HDERROR("clear cache; unable to get the application version"); 611 return; 612 } 613 614 BPath lastVersionPath; 615 if (StorageUtils::LocalWorkingFilesPath( 616 "version.txt", lastVersionPath) != B_OK) { 617 HDERROR("clear cache; unable to get version file path"); 618 return; 619 } 620 621 bool exists; 622 off_t size; 623 624 if (StorageUtils::ExistsObject( 625 lastVersionPath, &exists, NULL, &size) != B_OK) { 626 HDERROR("clear cache; unable to check version file exists"); 627 return; 628 } 629 630 BString lastVersion; 631 632 if (exists && StorageUtils::AppendToString(lastVersionPath, lastVersion) 633 != B_OK) { 634 HDERROR("clear cache; unable to read the version from [%s]", 635 lastVersionPath.Path()); 636 return; 637 } 638 639 if (lastVersion != version) { 640 HDINFO("last version [%s] and current version [%s] do not match" 641 " -> will flush cache", lastVersion.String(), version.String()); 642 StorageUtils::RemoveWorkingDirectoryContents(); 643 HDINFO("will write version [%s] to [%s]", 644 version.String(), lastVersionPath.Path()); 645 StorageUtils::AppendToFile(version, lastVersionPath); 646 } else { 647 HDINFO("last version [%s] and current version [%s] match" 648 " -> cache retained", lastVersion.String(), version.String()); 649 } 650 } 651