private async Task ValidateRunnerHash(string archiveFile, string packageHashValue) { var stopWatch = Stopwatch.StartNew(); // Validate Hash Matches if it is provided using (FileStream stream = File.OpenRead(archiveFile)) { if (!string.IsNullOrEmpty(packageHashValue)) { using (SHA256 sha256 = SHA256.Create()) { byte[] srcHashBytes = await sha256.ComputeHashAsync(stream); var hash = PrimitiveExtensions.ConvertToHexString(srcHashBytes); if (hash != packageHashValue) { // Hash did not match, we can't recover from this, just throw throw new Exception($"Computed runner hash {hash} did not match expected Runner Hash {packageHashValue} for {_targetPackage.Filename}"); } stopWatch.Stop(); Trace.Info($"Validated Runner Hash matches {_targetPackage.Filename} : {packageHashValue}"); _updateTrace.Add($"ValidateHashTime: {stopWatch.ElapsedMilliseconds}ms"); } } } }
/// <summary> /// _work /// \_update /// \bin /// \externals /// \run.sh /// \run.cmd /// \package.zip //temp download .zip/.tar.gz /// </summary> /// <param name="token"></param> /// <returns></returns> private async Task DownloadLatestRunner(CancellationToken token) { string latestRunnerDirectory = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Work), Constants.Path.UpdateDirectory); IOUtil.DeleteDirectory(latestRunnerDirectory, token); Directory.CreateDirectory(latestRunnerDirectory); int runnerSuffix = 1; string archiveFile = null; bool downloadSucceeded = false; try { // Download the runner, using multiple attempts in order to be resilient against any networking/CDN issues for (int attempt = 1; attempt <= Constants.RunnerDownloadRetryMaxAttempts; attempt++) { // Generate an available package name, and do our best effort to clean up stale local zip files while (true) { if (_targetPackage.Platform.StartsWith("win")) { archiveFile = Path.Combine(latestRunnerDirectory, $"runner{runnerSuffix}.zip"); } else { archiveFile = Path.Combine(latestRunnerDirectory, $"runner{runnerSuffix}.tar.gz"); } try { // delete .zip file if (!string.IsNullOrEmpty(archiveFile) && File.Exists(archiveFile)) { Trace.Verbose("Deleting latest runner package zip '{0}'", archiveFile); IOUtil.DeleteFile(archiveFile); } break; } catch (Exception ex) { // couldn't delete the file for whatever reason, so generate another name Trace.Warning("Failed to delete runner package zip '{0}'. Exception: {1}", archiveFile, ex); runnerSuffix++; } } // Allow a 15-minute package download timeout, which is good enough to update the runner from a 1 Mbit/s ADSL connection. if (!int.TryParse(Environment.GetEnvironmentVariable("GITHUB_ACTIONS_RUNNER_DOWNLOAD_TIMEOUT") ?? string.Empty, out int timeoutSeconds)) { timeoutSeconds = 15 * 60; } Trace.Info($"Attempt {attempt}: save latest runner into {archiveFile}."); using (var downloadTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds))) using (var downloadCts = CancellationTokenSource.CreateLinkedTokenSource(downloadTimeout.Token, token)) { try { Trace.Info($"Download runner: begin download"); //open zip stream in async mode using (HttpClient httpClient = new HttpClient(HostContext.CreateHttpClientHandler())) { if (!string.IsNullOrEmpty(_targetPackage.Token)) { Trace.Info($"Adding authorization token ({_targetPackage.Token.Length} chars)"); httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _targetPackage.Token); } Trace.Info($"Downloading {_targetPackage.DownloadUrl}"); using (FileStream fs = new FileStream(archiveFile, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true)) using (Stream result = await httpClient.GetStreamAsync(_targetPackage.DownloadUrl)) { //81920 is the default used by System.IO.Stream.CopyTo and is under the large object heap threshold (85k). await result.CopyToAsync(fs, 81920, downloadCts.Token); await fs.FlushAsync(downloadCts.Token); } } Trace.Info($"Download runner: finished download"); downloadSucceeded = true; break; } catch (OperationCanceledException) when(token.IsCancellationRequested) { Trace.Info($"Runner download has been canceled."); throw; } catch (Exception ex) { if (downloadCts.Token.IsCancellationRequested) { Trace.Warning($"Runner download has timed out after {timeoutSeconds} seconds"); } Trace.Warning($"Failed to get package '{archiveFile}' from '{_targetPackage.DownloadUrl}'. Exception {ex}"); } } } if (!downloadSucceeded) { throw new TaskCanceledException($"Runner package '{archiveFile}' failed after {Constants.RunnerDownloadRetryMaxAttempts} download attempts"); } // If we got this far, we know that we've successfully downloaded the runner package // Validate Hash Matches if it is provided using (FileStream stream = File.OpenRead(archiveFile)) { if (!String.IsNullOrEmpty(_targetPackage.HashValue)) { using (SHA256 sha256 = SHA256.Create()) { byte[] srcHashBytes = await sha256.ComputeHashAsync(stream); var hash = PrimitiveExtensions.ConvertToHexString(srcHashBytes); if (hash != _targetPackage.HashValue) { // Hash did not match, we can't recover from this, just throw throw new Exception($"Computed runner hash {hash} did not match expected Runner Hash {_targetPackage.HashValue} for {_targetPackage.Filename}"); } Trace.Info($"Validated Runner Hash matches {_targetPackage.Filename} : {_targetPackage.HashValue}"); } } } if (archiveFile.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) { ZipFile.ExtractToDirectory(archiveFile, latestRunnerDirectory); } else if (archiveFile.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase)) { string tar = WhichUtil.Which("tar", trace: Trace); if (string.IsNullOrEmpty(tar)) { throw new NotSupportedException($"tar -xzf"); } // tar -xzf using (var processInvoker = HostContext.CreateService <IProcessInvoker>()) { processInvoker.OutputDataReceived += new EventHandler <ProcessDataReceivedEventArgs>((sender, args) => { if (!string.IsNullOrEmpty(args.Data)) { Trace.Info(args.Data); } }); processInvoker.ErrorDataReceived += new EventHandler <ProcessDataReceivedEventArgs>((sender, args) => { if (!string.IsNullOrEmpty(args.Data)) { Trace.Error(args.Data); } }); int exitCode = await processInvoker.ExecuteAsync(latestRunnerDirectory, tar, $"-xzf \"{archiveFile}\"", null, token); if (exitCode != 0) { throw new NotSupportedException($"Can't use 'tar -xzf' extract archive file: {archiveFile}. return code: {exitCode}."); } } } else { throw new NotSupportedException($"{archiveFile}"); } Trace.Info($"Finished getting latest runner package at: {latestRunnerDirectory}."); } finally { try { // delete .zip file if (!string.IsNullOrEmpty(archiveFile) && File.Exists(archiveFile)) { Trace.Verbose("Deleting latest runner package zip: {0}", archiveFile); IOUtil.DeleteFile(archiveFile); } } catch (Exception ex) { //it is not critical if we fail to delete the .zip file Trace.Warning("Failed to delete runner package zip '{0}'. Exception: {1}", archiveFile, ex); } } // copy latest runner into runner root folder // copy bin from _work/_update -> bin.version under root string binVersionDir = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Root), $"{Constants.Path.BinDirectory}.{_targetPackage.Version}"); Directory.CreateDirectory(binVersionDir); Trace.Info($"Copy {Path.Combine(latestRunnerDirectory, Constants.Path.BinDirectory)} to {binVersionDir}."); IOUtil.CopyDirectory(Path.Combine(latestRunnerDirectory, Constants.Path.BinDirectory), binVersionDir, token); // copy externals from _work/_update -> externals.version under root string externalsVersionDir = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Root), $"{Constants.Path.ExternalsDirectory}.{_targetPackage.Version}"); Directory.CreateDirectory(externalsVersionDir); Trace.Info($"Copy {Path.Combine(latestRunnerDirectory, Constants.Path.ExternalsDirectory)} to {externalsVersionDir}."); IOUtil.CopyDirectory(Path.Combine(latestRunnerDirectory, Constants.Path.ExternalsDirectory), externalsVersionDir, token); // copy and replace all .sh/.cmd files Trace.Info($"Copy any remaining .sh/.cmd files into runner root."); foreach (FileInfo file in new DirectoryInfo(latestRunnerDirectory).GetFiles() ?? new FileInfo[0]) { string destination = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Root), file.Name); // Removing the file instead of just trying to overwrite it works around permissions issues on linux. // https://github.com/actions/runner/issues/981 Trace.Info($"Copy {file.FullName} to {destination}"); IOUtil.DeleteFile(destination); file.CopyTo(destination, true); } }