uint32 FBuildPatchInstaller::Run()
{
    // Make sure this function can never be parallelized
    static FCriticalSection SingletonFunctionLockCS;
    FScopeLock SingletonFunctionLock(&SingletonFunctionLockCS);
    FBuildPatchInstallError::Reset();

    SetRunning(true);
    SetInited(true);
    SetDownloadSpeed(-1);
    UpdateDownloadProgressInfo(true);

    // Register the current manifest with the installation info, to make sure we pull from it
    if (CurrentBuildManifest.IsValid())
    {
        InstallationInfo.RegisterAppInstallation(CurrentBuildManifest.ToSharedRef(), InstallDirectory);
    }

    // Keep track of files that failed verify
    TArray<FString> CorruptFiles;

    // Init prereqs progress value
    const bool bInstallPrereqs = !CurrentBuildManifest.IsValid() && !NewBuildManifest->GetPrereqPath().IsEmpty();

    // Get the start time
    double StartTime = FPlatformTime::Seconds();
    double CleanUpTime = 0;

    // Keep retrying the install while it is not canceled, or caused by download error
    bool bProcessSuccess = false;
    bool bCanRetry = true;
    int32 InstallRetries = 5;
    while (!bProcessSuccess && bCanRetry)
    {
        // Run the install
        bool bInstallSuccess = RunInstallation(CorruptFiles);
        BuildProgress.SetStateProgress(EBuildPatchProgress::PrerequisitesInstall, bInstallPrereqs ? 0.0f : 1.0f);
        if (bInstallSuccess)
        {
            BuildProgress.SetStateProgress(EBuildPatchProgress::Downloading, 1.0f);
            BuildProgress.SetStateProgress(EBuildPatchProgress::Installing, 1.0f);
        }

        // Backup local changes then move generated files
        bInstallSuccess = bInstallSuccess && RunBackupAndMove();

        // Run Verification
        CorruptFiles.Empty();
        BuildProgress.SetStateProgress(EBuildPatchProgress::Initializing, 1.0f);
        bProcessSuccess = bInstallSuccess && RunVerification(CorruptFiles);

        // Clean staging if INSTALL success
        if (bInstallSuccess)
        {
            GLog->Logf(TEXT("BuildPatchServices: Deleting staging area"));
            CleanUpTime = FPlatformTime::Seconds();
            IFileManager::Get().DeleteDirectory(*StagingDirectory, false, true);
            CleanUpTime = FPlatformTime::Seconds() - CleanUpTime;
        }
        BuildProgress.SetStateProgress(EBuildPatchProgress::CleanUp, 1.0f);

        // Set if we can retry
        --InstallRetries;
        bCanRetry = InstallRetries > 0 && !FBuildPatchInstallError::IsInstallationCancelled() && !FBuildPatchInstallError::IsNoRetryError();

        // If successful or we will retry, remove the moved files marker
        if (bProcessSuccess || bCanRetry)
        {
            GLog->Logf(TEXT("BuildPatchServices: Reset MM"));
            IFileManager::Get().Delete(*PreviousMoveMarker, false, true);
        }
    }

    if (bProcessSuccess)
    {
        // Run the prerequisites installer if this is our first install and the manifest has prerequisites info
        if (bInstallPrereqs)
        {
            // @TODO: We also want to trigger prereq install if this is an update and the prereq installer differs in the update
            bProcessSuccess &= RunPrereqInstaller();
        }
    }

    // Set final stat values and log out results
    {
        FScopeLock Lock(&ThreadLock);
        bSuccess = bProcessSuccess;
        BuildStats.ProcessSuccess = bProcessSuccess;
        BuildStats.ProcessExecuteTime = (FPlatformTime::Seconds() - StartTime) - BuildStats.ProcessPausedTime;
        BuildStats.FailureReason = FBuildPatchInstallError::GetErrorString();
        BuildStats.FailureReasonText = FBuildPatchInstallError::GetErrorText();
        BuildStats.CleanUpTime = CleanUpTime;

        // Log stats
        GLog->Logf(TEXT("BuildPatchServices: Build Stat: AppName: %s"), *BuildStats.AppName);
        GLog->Logf(TEXT("BuildPatchServices: Build Stat: AppInstalledVersion: %s"), *BuildStats.AppInstalledVersion);
        GLog->Logf(TEXT("BuildPatchServices: Build Stat: AppPatchVersion: %s"), *BuildStats.AppPatchVersion);
        GLog->Logf(TEXT("BuildPatchServices: Build Stat: CloudDirectory: %s"), *BuildStats.CloudDirectory);
        GLog->Logf(TEXT("BuildPatchServices: Build Stat: NumFilesInBuild: %u"), BuildStats.NumFilesInBuild);
        GLog->Logf(TEXT("BuildPatchServices: Build Stat: NumFilesOutdated: %u"), BuildStats.NumFilesOutdated);
        GLog->Logf(TEXT("BuildPatchServices: Build Stat: NumFilesToRemove: %u"), BuildStats.NumFilesToRemove);
        GLog->Logf(TEXT("BuildPatchServices: Build Stat: NumChunksRequired: %u"), BuildStats.NumChunksRequired);
        GLog->Logf(TEXT("BuildPatchServices: Build Stat: ChunksQueuedForDownload: %u"), BuildStats.ChunksQueuedForDownload);
        GLog->Logf(TEXT("BuildPatchServices: Build Stat: ChunksLocallyAvailable: %u"), BuildStats.ChunksLocallyAvailable);
        GLog->Logf(TEXT("BuildPatchServices: Build Stat: NumChunksDownloaded: %u"), BuildStats.NumChunksDownloaded);
        GLog->Logf(TEXT("BuildPatchServices: Build Stat: NumChunksRecycled: %u"), BuildStats.NumChunksRecycled);
        GLog->Logf(TEXT("BuildPatchServices: Build Stat: NumChunksCacheBooted: %u"), BuildStats.NumChunksCacheBooted);
        GLog->Logf(TEXT("BuildPatchServices: Build Stat: NumDriveCacheChunkLoads: %u"), BuildStats.NumDriveCacheChunkLoads);
        GLog->Logf(TEXT("BuildPatchServices: Build Stat: NumRecycleFailures: %u"), BuildStats.NumRecycleFailures);
        GLog->Logf(TEXT("BuildPatchServices: Build Stat: NumDriveCacheLoadFailures: %u"), BuildStats.NumDriveCacheLoadFailures);
        GLog->Logf(TEXT("BuildPatchServices: Build Stat: TotalDownloadedData: %lld"), BuildStats.TotalDownloadedData);
        GLog->Logf(TEXT("BuildPatchServices: Build Stat: AverageDownloadSpeed: %.3f MB/sec"), BuildStats.AverageDownloadSpeed / 1024.0 / 1024.0);
        GLog->Logf(TEXT("BuildPatchServices: Build Stat: TheoreticalDownloadTime: %s"), *FPlatformTime::PrettyTime(BuildStats.TheoreticalDownloadTime));
        GLog->Logf(TEXT("BuildPatchServices: Build Stat: VerifyTime: %s"), *FPlatformTime::PrettyTime(BuildStats.VerifyTime));
        GLog->Logf(TEXT("BuildPatchServices: Build Stat: CleanUpTime: %s"), *FPlatformTime::PrettyTime(BuildStats.CleanUpTime));
        GLog->Logf(TEXT("BuildPatchServices: Build Stat: ProcessExecuteTime: %s"), *FPlatformTime::PrettyTime(BuildStats.ProcessExecuteTime));
        GLog->Logf(TEXT("BuildPatchServices: Build Stat: ProcessPausedTime: %.1f sec"), BuildStats.ProcessPausedTime);
        GLog->Logf(TEXT("BuildPatchServices: Build Stat: ProcessSuccess: %s"), BuildStats.ProcessSuccess ? TEXT("TRUE") : TEXT("FALSE"));
        GLog->Logf(TEXT("BuildPatchServices: Build Stat: FailureReason: %s"), *BuildStats.FailureReason);
        GLog->Logf(TEXT("BuildPatchServices: Build Stat: FailureReasonText: %s"), *BuildStats.FailureReasonText.BuildSourceString());
    }

    // Mark that we are done
    SetRunning(false);

    return bSuccess ? 0 : 1;
}
bool FBuildPatchInstaller::RunInstallation(TArray<FString>& CorruptFiles)
{
    GLog->Logf(TEXT("BuildPatchServices: Starting Installation"));
    // Save the staging directories
    FPaths::NormalizeDirectoryName(DataStagingDir);
    FPaths::NormalizeDirectoryName(InstallStagingDir);

    // Make sure staging directories exist
    IFileManager::Get().MakeDirectory(*DataStagingDir, true);
    IFileManager::Get().MakeDirectory(*InstallStagingDir, true);

    // Reset any error from a previous install
    FBuildPatchInstallError::Reset();
    FBuildPatchAnalytics::ResetCounters();
    BuildProgress.Reset();
    BuildProgress.SetStateProgress(EBuildPatchProgress::Initializing, 0.01f);
    BuildProgress.SetStateProgress(EBuildPatchProgress::CleanUp, 0.0f);

    // Remove any inventory
    FBuildPatchFileConstructor::PurgeFileDataInventory();

    // Check if we should skip out of this process
    bool bPreviousStagingCompleted = FPaths::FileExists(PreviousMoveMarker);
    if (bPreviousStagingCompleted)
    {
        GLog->Logf(TEXT("BuildPatchServices: Detected previous staging completed"));
        // Set weights for verify only
        BuildProgress.SetStateWeight(EBuildPatchProgress::Downloading, 0.0f);
        BuildProgress.SetStateWeight(EBuildPatchProgress::Installing, 0.0f);
        BuildProgress.SetStateWeight(EBuildPatchProgress::MovingToInstall, 0.0f);
        BuildProgress.SetStateWeight(EBuildPatchProgress::BuildVerification, 1.0f);
        // Mark all installation steps complete
        BuildProgress.SetStateProgress(EBuildPatchProgress::Initializing, 1.0f);
        BuildProgress.SetStateProgress(EBuildPatchProgress::Resuming, 1.0f);
        BuildProgress.SetStateProgress(EBuildPatchProgress::Downloading, 1.0f);
        BuildProgress.SetStateProgress(EBuildPatchProgress::Installing, 1.0f);
        BuildProgress.SetStateProgress(EBuildPatchProgress::MovingToInstall, 1.0f);
        return true;
    }

    // Get the list of files needing construction
    TArray< FString > FilesToConstruct;
    if (CorruptFiles.Num() > 0)
    {
        FilesToConstruct.Append(CorruptFiles);
    }
    else
    {
        FBuildPatchAppManifest::GetOutdatedFiles(CurrentBuildManifest, NewBuildManifest, InstallDirectory, FilesToConstruct);
    }
    GLog->Logf(TEXT("BuildPatchServices: Requiring %d files"), FilesToConstruct.Num());

    // Create the downloader
    FBuildPatchDownloader::Create(DataStagingDir, NewBuildManifest, &BuildProgress);

    // Create chunk cache
    if (bIsChunkData)
    {
        FBuildPatchChunkCache::Init(NewBuildManifest, CurrentBuildManifest, DataStagingDir, InstallDirectory, &BuildProgress, FilesToConstruct, InstallationInfo);
    }

    // Hold the file constructor thread
    FBuildPatchFileConstructor* FileConstructor = NULL;

    // Store some totals
    const uint32 NumFilesInBuild = NewBuildManifest->GetNumFiles();

    // Stats for build
    const uint32 NumFilesToConstruct = bIsFileData ? NumFilesInBuild : FBuildPatchChunkCache::Get().GetStatNumFilesToConstruct();
    const uint32 NumRequiredChunks = bIsFileData ? NumFilesInBuild : FBuildPatchChunkCache::Get().GetStatNumRequiredChunks();
    const uint32 NumChunksToDownload = bIsFileData ? NumFilesInBuild : FBuildPatchChunkCache::Get().GetStatNumChunksToDownload();
    const uint32 NumChunksToConstruct = bIsFileData ? 0 : FBuildPatchChunkCache::Get().GetStatNumChunksToRecycle();
    TotalInitialDownloadSize = bIsFileData ? NewBuildManifest->GetFileSize(FilesToConstruct) : FBuildPatchChunkCache::Get().GetStatTotalChunkDownloadSize();

    // Save stats
    {
        FScopeLock Lock(&ThreadLock);
        BuildStats.AppName = NewBuildManifest->GetAppName();
        BuildStats.AppPatchVersion = NewBuildManifest->GetVersionString();
        BuildStats.AppInstalledVersion = CurrentBuildManifest.IsValid() ? CurrentBuildManifest->GetVersionString() : TEXT("NONE");
        BuildStats.CloudDirectory = FBuildPatchServicesModule::GetCloudDirectory();
        BuildStats.NumFilesInBuild = NumFilesInBuild;
        BuildStats.NumFilesOutdated = NumFilesToConstruct;
        BuildStats.NumChunksRequired = NumRequiredChunks;
        BuildStats.ChunksQueuedForDownload = NumChunksToDownload;
        BuildStats.ChunksLocallyAvailable = NumChunksToConstruct;
    }

    // Save initial counts as float for use with progress updates
    InitialNumChunkDownloads = NumChunksToDownload;
    InitialNumChunkConstructions = NumChunksToConstruct;

    // Setup some weightings for the progress tracking
    const float NumRequiredChunksFloat = NumRequiredChunks;
    BuildProgress.SetStateWeight(EBuildPatchProgress::Downloading, NumRequiredChunksFloat > 0.0f ? InitialNumChunkDownloads / NumRequiredChunksFloat : 0.0f);
    BuildProgress.SetStateWeight(EBuildPatchProgress::Installing, NumRequiredChunksFloat > 0.0f ? 0.1f + (InitialNumChunkConstructions / NumRequiredChunksFloat) : 0.0f);
    BuildProgress.SetStateWeight(EBuildPatchProgress::MovingToInstall, NumFilesToConstruct > 0 ? 0.05f : 0.0f);
    // A verify weight of 1 / 9 will make it 10% of the total progress
    BuildProgress.SetStateWeight(EBuildPatchProgress::BuildVerification, 1.1f / 9.0f);

    // If this is a repair operation, start off with install and download complete
    if (bIsRepairing)
    {
        GLog->Logf(TEXT("BuildPatchServices: Performing a repair operation"));
        BuildProgress.SetStateProgress(EBuildPatchProgress::Downloading, 1.0f);
        BuildProgress.SetStateProgress(EBuildPatchProgress::Installing, 1.0f);
        BuildProgress.SetStateProgress(EBuildPatchProgress::MovingToInstall, 1.0f);
    }

    // Start the file constructor
    GLog->Logf(TEXT("BuildPatchServices: Starting file contruction worker"));
    FileConstructor = new FBuildPatchFileConstructor(CurrentBuildManifest, NewBuildManifest, InstallDirectory, InstallStagingDir, FilesToConstruct, &BuildProgress);

    // Initializing is now complete if we are constructing files
    BuildProgress.SetStateProgress(EBuildPatchProgress::Initializing, NumFilesToConstruct > 0 ? 1.0f : 0.0f);

    // If this is file data, queue the download list
    if (bIsFileData)
    {
        TArray< FGuid > RequiredFileData;
        NewBuildManifest->GetChunksRequiredForFiles(FilesToConstruct, RequiredFileData);
        FBuildPatchDownloader::Get().AddChunksToDownload(RequiredFileData);
    }

    // Wait for the file constructor to complete
    while (FileConstructor->IsComplete() == false)
    {
        UpdateDownloadProgressInfo();
        FPlatformProcess::Sleep(0.1f);
    }
    FileConstructor->Wait();
    delete FileConstructor;
    FileConstructor = NULL;
    GLog->Logf(TEXT("BuildPatchServices: File construction complete"));

    // Wait for downloader to complete
    FBuildPatchDownloader::Get().NotifyNoMoreChunksToAdd();
    while (FBuildPatchDownloader::Get().IsComplete() == false)
    {
        UpdateDownloadProgressInfo();
        FPlatformProcess::Sleep(0.0f);
    }
    TArray< FBuildPatchDownloadRecord > AllChunkDownloads = FBuildPatchDownloader::Get().GetDownloadRecordings();
    SetDownloadSpeed(-1);

    // Calculate the average download speed from the recordings
    // NB: Because we are threading several downloads at once this is not simply averaging every download. We have to know about
    //     how much data is being received simultaneously too. We also need to ignore download pauses.
    int64 TotalDownloadedBytes = 0;
    double TotalTimeDownloading = 0;
    double RecoredEndTime = 0;
    if (AllChunkDownloads.Num() > 0)
    {
        // Sort by start time
        AllChunkDownloads.Sort();
        // Start with first record
        TotalTimeDownloading = AllChunkDownloads[0].EndTime - AllChunkDownloads[0].StartTime;
        TotalDownloadedBytes = AllChunkDownloads[0].DownloadSize;
        RecoredEndTime = AllChunkDownloads[0].EndTime;
        // For every other record..
        for (int32 RecordIdx = 1; RecordIdx < AllChunkDownloads.Num(); ++RecordIdx)
        {
            // Do we have some time to count
            if (RecoredEndTime < AllChunkDownloads[RecordIdx].EndTime)
            {
                // Was there a break in downloading
                if (AllChunkDownloads[RecordIdx].StartTime > RecoredEndTime)
                {
                    TotalTimeDownloading += AllChunkDownloads[RecordIdx].EndTime - AllChunkDownloads[RecordIdx].StartTime;
                }
                // Otherwise don't count time overlap
                else
                {
                    TotalTimeDownloading += AllChunkDownloads[RecordIdx].EndTime - RecoredEndTime;
                }
                RecoredEndTime = AllChunkDownloads[RecordIdx].EndTime;
            }
            // Count all bytes
            TotalDownloadedBytes += AllChunkDownloads[RecordIdx].DownloadSize;
        }
    }

    // Set final stats
    {
        FScopeLock Lock(&ThreadLock);
        BuildStats.TotalDownloadedData = TotalDownloadedBytes;
        BuildStats.NumChunksDownloaded = AllChunkDownloads.Num();
        const double TotalDownloadedBytesDouble = TotalDownloadedBytes;
        BuildStats.AverageDownloadSpeed = TotalTimeDownloading > 0 ? TotalDownloadedBytesDouble / TotalTimeDownloading : 0;
        BuildStats.TheoreticalDownloadTime = TotalTimeDownloading;
        BuildStats.NumChunksRecycled = bIsFileData ? 0 : FBuildPatchChunkCache::Get().GetCounterChunksRecycled();
        BuildStats.NumChunksCacheBooted = bIsFileData ? 0 : FBuildPatchChunkCache::Get().GetCounterChunksCacheBooted();
        BuildStats.NumDriveCacheChunkLoads = bIsFileData ? 0 : FBuildPatchChunkCache::Get().GetCounterDriveCacheChunkLoads();
        BuildStats.NumRecycleFailures = bIsFileData ? 0 : FBuildPatchChunkCache::Get().GetCounterRecycleFailures();
        BuildStats.NumDriveCacheLoadFailures = bIsFileData ? 0 : FBuildPatchChunkCache::Get().GetCounterDriveCacheLoadFailures();
    }

    // Perform static cleanup
    if (bIsChunkData)
    {
        FBuildPatchChunkCache::Shutdown();
    }
    FBuildPatchDownloader::Shutdown();
    FBuildPatchFileConstructor::PurgeFileDataInventory();

    GLog->Logf(TEXT("BuildPatchServices: Staged install complete"));

    return !FBuildPatchInstallError::HasFatalError();
}
void FBuildPatchInstaller::UpdateDownloadProgressInfo( bool bReset )
{
    // Static variables for persistent values
    static double LastTime = FPlatformTime::Seconds();
    static double NowTime = 0;
    static double DeltaTime = 0;
    static double LastReadingTime = 0;
    static double LastDataReadings[NUM_DOWNLOAD_READINGS] = { 0 };
    static double LastTimeReadings[NUM_DOWNLOAD_READINGS] = { 0 };
    static uint32 ReadingIdx = 0;
    static bool bProgressIsDownload = true;
    static double AverageDownloadSpeed = 0;

    // Reset internals?
    if( bReset )
    {
        LastTime = FPlatformTime::Seconds();
        LastReadingTime = LastTime;
        NowTime = 0;
        DeltaTime = 0;
        for (int32 i = 0; i < NUM_DOWNLOAD_READINGS; ++i)
        {
            LastDataReadings[i] = 0;
            LastTimeReadings[i] = 0;
        }
        ReadingIdx = 0;
        bProgressIsDownload = true;
        AverageDownloadSpeed = 0;
        return;
    }

    // Return if not downloading yet
    if( !NewBuildManifest->IsFileDataManifest() && !FBuildPatchChunkCache::Get().HaveDownloadsStarted() )
    {
        return;
    }

    // Calculate percentage complete based on number of chunks
    const int64 DownloadNumBytesLeft = FBuildPatchDownloader::Get().GetNumBytesLeft();
    const float DownloadSizeFloat = TotalInitialDownloadSize;
    const float DownloadBytesLeftFloat = DownloadNumBytesLeft;
    const float DownloadProgress = 1.0f - (TotalInitialDownloadSize > 0 ? DownloadBytesLeftFloat / DownloadSizeFloat : 0.0f);
    BuildProgress.SetStateProgress( EBuildPatchProgress::Downloading, DownloadProgress );

    // Calculate the average download speed
    NowTime = FPlatformTime::Seconds();
    DeltaTime += NowTime - LastTime;
    if( DeltaTime > TIME_PER_READING )
    {
        const double BytesDownloaded = FBuildPatchDownloader::Get().GetByteDownloadCountReset();
        const double TimeSinceLastReading = NowTime - LastReadingTime;
        LastReadingTime = NowTime;
        LastDataReadings[ReadingIdx] = BytesDownloaded;
        LastTimeReadings[ReadingIdx] = TimeSinceLastReading;
        ReadingIdx = (ReadingIdx + 1) % NUM_DOWNLOAD_READINGS;
        DeltaTime = 0;
        double TotalData = 0;
        double TotalTime = 0;
        for (uint32 i = 0; i < NUM_DOWNLOAD_READINGS; ++i)
        {
            TotalData += LastDataReadings[i];
            TotalTime += LastTimeReadings[i];
        }
        AverageDownloadSpeed = TotalData / TotalTime;
    }

    // Set download values
    SetDownloadSpeed( DownloadProgress < 1.0f ? AverageDownloadSpeed : -1.0f );
    SetDownloadBytesLeft( DownloadNumBytesLeft );

    // Set last time
    LastTime = NowTime;
}
Example #4
0
void c_main(char *blockBase, u32 blockSize)
{
	int numRead = 0;
	char commandline[128];
	blobStatus status;
	int i;
	int retval = 0;
	
	/* We really want to be able to communicate, so initialise the
	 * serial port at 9k6 (which works good for terminals)
	 */
	SerialInit(baud9k6);
	TimerInit();
	
	/* initialise status */
	status.kernelSize = 0;
	status.kernelType = fromFlash;
	status.ramdiskSize = 0;
	status.ramdiskType = fromFlash;
	status.blockSize = blockSize;
	status.downloadSpeed = baud115k2;
	
	/* Load kernel and ramdisk from flash to RAM */
	Reload("kernel", &status);
	Reload("ramdisk", &status);

	/* Print the required GPL string */
	SerialOutputString("\r" PACKAGE " version " VERSION  "\r"
			   "Copyright (C) 1999 2000 "
			   "Jan-Derk Bakker and Erik Mouw\r"
			   "Copyright (C) 2000 "
			   "Johan Pouwelse.\r");
	SerialOutputString(PACKAGE " comes with ABSOLUTELY NO WARRANTY; "
			   "read the GNU GPL for details.\r");
	SerialOutputString("This is free software, and you are welcome "
			   "to redistribute it\r");
	SerialOutputString("under certain conditions; "
			   "read the GNU GPL for details.\r\r");

	/* and some information */
#ifdef BLOB_DEBUG
	SerialOutputString("Running from ");
	if(RunningFromInternal())
		SerialOutputString("internal flash\r");
	else
		SerialOutputString("external flash\r");

	SerialOutputString("blockBase = 0x");
	SerialOutputHex((int) blockBase);
	SerialOutputString(", blockSize = 0x");
	SerialOutputHex(blockSize);
	SerialOutputByte('\r');
#endif
	/* wait 10 seconds before starting autoboot */
	SerialOutputString("Autoboot in progress, press any key to stop ");
	for(i = 0; i < 10; i++) {
		SerialOutputByte('.');

 		retval = SerialInputBlock(commandline, 1, 1); 

		if(retval > 0)
			break;
	}

	/* no key was pressed, so proceed booting the kernel */
	if(retval == 0) {
		commandline[0] = '\0';
		BootKernel(commandline);
	}

	SerialOutputString("\rAutoboot aborted\r");
	SerialOutputString("Type \"help\" to get a list of commands\r");

	/* the command loop. endless, of course */
	for(;;) {
		DisplayPrompt(NULL);

		/* wait an hour to get a command */
		numRead = GetCommand(commandline, 128, 3600);

		if(numRead > 0) {
			if(MyStrNCmp(commandline, "boot", 4) == 0) {
				BootKernel(commandline + 4);
			} else if(MyStrNCmp(commandline, "clock", 5) == 0) {
				SetClock(commandline + 5);
			} else if(MyStrNCmp(commandline, "download ", 9) == 0) {
				Download(commandline + 9, &status);
			} else if(MyStrNCmp(commandline, "flash ", 6) == 0) {
				Flash(commandline + 6, &status);
			} else if(MyStrNCmp(commandline, "help", 4) == 0) {
				PrintHelp();
			} else if(MyStrNCmp(commandline, "reload ", 7) == 0) {
				Reload(commandline + 7, &status);
			} else if(MyStrNCmp(commandline, "reset", 5) == 0) {
				ResetTerminal();
			} else if(MyStrNCmp(commandline, "speed ", 6) == 0) {
				SetDownloadSpeed(commandline + 6, &status);
			}
			else if(MyStrNCmp(commandline, "status", 6) == 0) {
				PrintStatus(&status);
			} else {
				SerialOutputString("*** Unknown command: ");
				SerialOutputString(commandline);
				SerialOutputByte('\r');
			}
		}
	}
} /* c_main */