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