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 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 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 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 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 101 App::IsFirstRun() 102 { 103 return fIsFirstRun; 104 } 105 106 107 void 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 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 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 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 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 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 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 431 App::_ShowWindow(MainWindow* window) 432 { 433 window->Show(); 434 fWindowCount++; 435 } 436 437 438 bool 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 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 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 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 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 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 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