mirror of
https://github.com/SteamRE/DepotDownloader.git
synced 2026-10-20 01:46:17 +00:00
Use ansi escape code to indicate download progress in taskbar
This commit is contained in:
1 parent
ae72ac03c9
commit
ccbffd1c08
4 files changed
+212
-6
No files matched your search
@@ -0,0 +1,51 @@
|
|||||||
|
using System;
|
||||||
|
using Spectre.Console;
|
||||||
|
|
||||||
|
namespace DepotDownloader;
|
||||||
|
|
||||||
|
static class Ansi
|
||||||
|
{
|
||||||
|
// https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC
|
||||||
|
// https://learn.microsoft.com/en-us/windows/terminal/tutorials/progress-bar-sequences
|
||||||
|
public enum ProgressState
|
||||||
|
{
|
||||||
|
Hidden = 0,
|
||||||
|
Default = 1,
|
||||||
|
Error = 2,
|
||||||
|
Indeterminate = 3,
|
||||||
|
Warning = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
const char ESC = (char)0x1B;
|
||||||
|
const char BEL = (char)0x07;
|
||||||
|
|
||||||
|
private static bool useProgress;
|
||||||
|
|
||||||
|
public static void Init()
|
||||||
|
{
|
||||||
|
if (Console.IsInputRedirected || Console.IsOutputRedirected)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var (supportsAnsi, legacyConsole) = AnsiDetector.Detect(stdError: false, upgrade: true);
|
||||||
|
|
||||||
|
useProgress = supportsAnsi && !legacyConsole;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Progress(ulong downloaded, ulong total)
|
||||||
|
{
|
||||||
|
var progress = (byte)MathF.Round(downloaded / (float)total * 100.0f);
|
||||||
|
Progress(ProgressState.Default, progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Progress(ProgressState state, byte progress = 0)
|
||||||
|
{
|
||||||
|
if (!useProgress)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Write($"{ESC}]9;4;{(byte)state};{progress}{BEL}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
// Copied from https://github.com/spectreconsole/spectre.console/blob/d79e6adc5f8e637fb35c88f987023ffda6707243/src/Spectre.Console/Internal/Backends/Ansi/AnsiDetector.cs
|
||||||
|
// MIT License - Copyright(c) 2020 Patrik Svensson, Phil Scott, Nils Andresen
|
||||||
|
// which is partially based on https://github.com/keqingrong/supports-ansi/blob/master/index.js
|
||||||
|
// <auto-generated/>
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace Spectre.Console;
|
||||||
|
|
||||||
|
internal static class AnsiDetector
|
||||||
|
{
|
||||||
|
private static readonly Regex[] _regexes =
|
||||||
|
[
|
||||||
|
new("^xterm"), // xterm, PuTTY, Mintty
|
||||||
|
new("^rxvt"), // RXVT
|
||||||
|
new("^eterm"), // Eterm
|
||||||
|
new("^screen"), // GNU screen, tmux
|
||||||
|
new("tmux"), // tmux
|
||||||
|
new("^vt100"), // DEC VT series
|
||||||
|
new("^vt102"), // DEC VT series
|
||||||
|
new("^vt220"), // DEC VT series
|
||||||
|
new("^vt320"), // DEC VT series
|
||||||
|
new("ansi"), // ANSI
|
||||||
|
new("scoansi"), // SCO ANSI
|
||||||
|
new("cygwin"), // Cygwin, MinGW
|
||||||
|
new("linux"), // Linux console
|
||||||
|
new("konsole"), // Konsole
|
||||||
|
new("bvterm"), // Bitvise SSH Client
|
||||||
|
new("^st-256color"), // Suckless Simple Terminal, st
|
||||||
|
new("alacritty"), // Alacritty
|
||||||
|
];
|
||||||
|
|
||||||
|
public static (bool SupportsAnsi, bool LegacyConsole) Detect(bool stdError, bool upgrade)
|
||||||
|
{
|
||||||
|
// Running on Windows?
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
// Running under ConEmu?
|
||||||
|
var conEmu = Environment.GetEnvironmentVariable("ConEmuANSI");
|
||||||
|
if (!string.IsNullOrEmpty(conEmu) && conEmu.Equals("On", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return (true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
var supportsAnsi = Windows.SupportsAnsi(upgrade, stdError, out var legacyConsole);
|
||||||
|
return (supportsAnsi, legacyConsole);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DetectFromTerm();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (bool SupportsAnsi, bool LegacyConsole) DetectFromTerm()
|
||||||
|
{
|
||||||
|
// Check if the terminal is of type ANSI/VT100/xterm compatible.
|
||||||
|
var term = Environment.GetEnvironmentVariable("TERM");
|
||||||
|
if (!string.IsNullOrWhiteSpace(term))
|
||||||
|
{
|
||||||
|
if (_regexes.Any(regex => regex.IsMatch(term)))
|
||||||
|
{
|
||||||
|
return (true, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Windows
|
||||||
|
{
|
||||||
|
private const int STD_OUTPUT_HANDLE = -11;
|
||||||
|
private const int STD_ERROR_HANDLE = -12;
|
||||||
|
private const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004;
|
||||||
|
private const uint DISABLE_NEWLINE_AUTO_RETURN = 0x0008;
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
private static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
private static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static extern IntPtr GetStdHandle(int nStdHandle);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
public static extern uint GetLastError();
|
||||||
|
|
||||||
|
public static bool SupportsAnsi(bool upgrade, bool stdError, out bool isLegacy)
|
||||||
|
{
|
||||||
|
isLegacy = false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var @out = GetStdHandle(stdError ? STD_ERROR_HANDLE : STD_OUTPUT_HANDLE);
|
||||||
|
if (!GetConsoleMode(@out, out var mode))
|
||||||
|
{
|
||||||
|
// Could not get console mode, try TERM (set in cygwin, WSL-Shell).
|
||||||
|
var (ansiFromTerm, legacyFromTerm) = DetectFromTerm();
|
||||||
|
|
||||||
|
isLegacy = ansiFromTerm ? legacyFromTerm : isLegacy;
|
||||||
|
return ansiFromTerm;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0)
|
||||||
|
{
|
||||||
|
isLegacy = true;
|
||||||
|
|
||||||
|
if (!upgrade)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try enable ANSI support.
|
||||||
|
mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN;
|
||||||
|
if (!SetConsoleMode(@out, mode))
|
||||||
|
{
|
||||||
|
// Enabling failed.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLegacy = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// All we know here is that we don't support ANSI.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -610,6 +610,7 @@ namespace DepotDownloader
|
|||||||
|
|
||||||
private class GlobalDownloadCounter
|
private class GlobalDownloadCounter
|
||||||
{
|
{
|
||||||
|
public ulong completeDownloadSize;
|
||||||
public ulong totalBytesCompressed;
|
public ulong totalBytesCompressed;
|
||||||
public ulong totalBytesUncompressed;
|
public ulong totalBytesUncompressed;
|
||||||
}
|
}
|
||||||
@@ -624,6 +625,8 @@ namespace DepotDownloader
|
|||||||
|
|
||||||
private static async Task DownloadSteam3Async(List<DepotDownloadInfo> depots)
|
private static async Task DownloadSteam3Async(List<DepotDownloadInfo> depots)
|
||||||
{
|
{
|
||||||
|
Ansi.Progress(Ansi.ProgressState.Indeterminate);
|
||||||
|
|
||||||
var cts = new CancellationTokenSource();
|
var cts = new CancellationTokenSource();
|
||||||
cdnPool.ExhaustedToken = cts;
|
cdnPool.ExhaustedToken = cts;
|
||||||
|
|
||||||
@@ -634,7 +637,7 @@ namespace DepotDownloader
|
|||||||
// First, fetch all the manifests for each depot (including previous manifests) and perform the initial setup
|
// First, fetch all the manifests for each depot (including previous manifests) and perform the initial setup
|
||||||
foreach (var depot in depots)
|
foreach (var depot in depots)
|
||||||
{
|
{
|
||||||
var depotFileData = await ProcessDepotManifestAndFiles(cts, depot);
|
var depotFileData = await ProcessDepotManifestAndFiles(cts, depot, downloadCounter);
|
||||||
|
|
||||||
if (depotFileData != null)
|
if (depotFileData != null)
|
||||||
{
|
{
|
||||||
@@ -665,11 +668,13 @@ namespace DepotDownloader
|
|||||||
await DownloadSteam3AsyncDepotFiles(cts, downloadCounter, depotFileData, allFileNamesAllDepots);
|
await DownloadSteam3AsyncDepotFiles(cts, downloadCounter, depotFileData, allFileNamesAllDepots);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ansi.Progress(Ansi.ProgressState.Hidden);
|
||||||
|
|
||||||
Console.WriteLine("Total downloaded: {0} bytes ({1} bytes uncompressed) from {2} depots",
|
Console.WriteLine("Total downloaded: {0} bytes ({1} bytes uncompressed) from {2} depots",
|
||||||
downloadCounter.totalBytesCompressed, downloadCounter.totalBytesUncompressed, depots.Count);
|
downloadCounter.totalBytesCompressed, downloadCounter.totalBytesUncompressed, depots.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<DepotFilesData> ProcessDepotManifestAndFiles(CancellationTokenSource cts, DepotDownloadInfo depot)
|
private static async Task<DepotFilesData> ProcessDepotManifestAndFiles(CancellationTokenSource cts, DepotDownloadInfo depot, GlobalDownloadCounter downloadCounter)
|
||||||
{
|
{
|
||||||
var depotCounter = new DepotDownloadCounter();
|
var depotCounter = new DepotDownloadCounter();
|
||||||
|
|
||||||
@@ -751,7 +756,7 @@ namespace DepotDownloader
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Console.Write("Downloading depot manifest...");
|
Console.Write("Downloading depot manifest... ");
|
||||||
|
|
||||||
DepotManifest depotManifest = null;
|
DepotManifest depotManifest = null;
|
||||||
ulong manifestRequestCode = 0;
|
ulong manifestRequestCode = 0;
|
||||||
@@ -814,7 +819,7 @@ namespace DepotDownloader
|
|||||||
|
|
||||||
if (e.StatusCode == HttpStatusCode.Unauthorized || e.StatusCode == HttpStatusCode.Forbidden)
|
if (e.StatusCode == HttpStatusCode.Unauthorized || e.StatusCode == HttpStatusCode.Forbidden)
|
||||||
{
|
{
|
||||||
Console.WriteLine("Encountered 401 for depot manifest {0} {1}. Aborting.", depot.DepotId, depot.ManifestId);
|
Console.WriteLine("Encountered {2} for depot manifest {0} {1}. Aborting.", depot.DepotId, depot.ManifestId, (int)e.StatusCode);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -889,6 +894,7 @@ namespace DepotDownloader
|
|||||||
Directory.CreateDirectory(Path.GetDirectoryName(fileFinalPath));
|
Directory.CreateDirectory(Path.GetDirectoryName(fileFinalPath));
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(fileStagingPath));
|
Directory.CreateDirectory(Path.GetDirectoryName(fileStagingPath));
|
||||||
|
|
||||||
|
downloadCounter.completeDownloadSize += file.TotalSize;
|
||||||
depotCounter.completeDownloadSize += file.TotalSize;
|
depotCounter.completeDownloadSize += file.TotalSize;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -918,7 +924,7 @@ namespace DepotDownloader
|
|||||||
|
|
||||||
await Util.InvokeAsync(
|
await Util.InvokeAsync(
|
||||||
files.Select(file => new Func<Task>(async () =>
|
files.Select(file => new Func<Task>(async () =>
|
||||||
await Task.Run(() => DownloadSteam3AsyncDepotFile(cts, depotFilesData, file, networkChunkQueue)))),
|
await Task.Run(() => DownloadSteam3AsyncDepotFile(cts, downloadCounter, depotFilesData, file, networkChunkQueue)))),
|
||||||
maxDegreeOfParallelism: Config.MaxDownloads
|
maxDegreeOfParallelism: Config.MaxDownloads
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -966,6 +972,7 @@ namespace DepotDownloader
|
|||||||
|
|
||||||
private static void DownloadSteam3AsyncDepotFile(
|
private static void DownloadSteam3AsyncDepotFile(
|
||||||
CancellationTokenSource cts,
|
CancellationTokenSource cts,
|
||||||
|
GlobalDownloadCounter downloadCounter,
|
||||||
DepotFilesData depotFilesData,
|
DepotFilesData depotFilesData,
|
||||||
ProtoManifest.FileData file,
|
ProtoManifest.FileData file,
|
||||||
ConcurrentQueue<(FileStreamData, ProtoManifest.FileData, ProtoManifest.ChunkData)> networkChunkQueue)
|
ConcurrentQueue<(FileStreamData, ProtoManifest.FileData, ProtoManifest.ChunkData)> networkChunkQueue)
|
||||||
@@ -1128,6 +1135,11 @@ namespace DepotDownloader
|
|||||||
Console.WriteLine("{0,6:#00.00}% {1}", (depotDownloadCounter.sizeDownloaded / (float)depotDownloadCounter.completeDownloadSize) * 100.0f, fileFinalPath);
|
Console.WriteLine("{0,6:#00.00}% {1}", (depotDownloadCounter.sizeDownloaded / (float)depotDownloadCounter.completeDownloadSize) * 100.0f, fileFinalPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lock (downloadCounter)
|
||||||
|
{
|
||||||
|
downloadCounter.completeDownloadSize -= file.TotalSize;
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1136,6 +1148,11 @@ namespace DepotDownloader
|
|||||||
{
|
{
|
||||||
depotDownloadCounter.sizeDownloaded += sizeOnDisk;
|
depotDownloadCounter.sizeDownloaded += sizeOnDisk;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lock (downloadCounter)
|
||||||
|
{
|
||||||
|
downloadCounter.completeDownloadSize -= sizeOnDisk;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileIsExecutable = file.Flags.HasFlag(EDepotFileFlag.Executable);
|
var fileIsExecutable = file.Flags.HasFlag(EDepotFileFlag.Executable);
|
||||||
@@ -1217,7 +1234,7 @@ namespace DepotDownloader
|
|||||||
|
|
||||||
if (e.StatusCode == HttpStatusCode.Unauthorized || e.StatusCode == HttpStatusCode.Forbidden)
|
if (e.StatusCode == HttpStatusCode.Unauthorized || e.StatusCode == HttpStatusCode.Forbidden)
|
||||||
{
|
{
|
||||||
Console.WriteLine("Encountered 401 for chunk {0}. Aborting.", chunkID);
|
Console.WriteLine("Encountered {1} for chunk {0}. Aborting.", chunkID, (int)e.StatusCode);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1281,6 +1298,8 @@ namespace DepotDownloader
|
|||||||
{
|
{
|
||||||
downloadCounter.totalBytesCompressed += chunk.CompressedLength;
|
downloadCounter.totalBytesCompressed += chunk.CompressedLength;
|
||||||
downloadCounter.totalBytesUncompressed += chunk.UncompressedLength;
|
downloadCounter.totalBytesUncompressed += chunk.UncompressedLength;
|
||||||
|
|
||||||
|
Ansi.Progress(downloadCounter.totalBytesUncompressed, downloadCounter.completeDownloadSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remainingChunks == 0)
|
if (remainingChunks == 0)
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ namespace DepotDownloader
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ansi.Init();
|
||||||
|
|
||||||
DebugLog.Enabled = false;
|
DebugLog.Enabled = false;
|
||||||
|
|
||||||
AccountSettingsStore.LoadFromFile("account.config");
|
AccountSettingsStore.LoadFromFile("account.config");
|
||||||
|
|||||||
Reference in new issue
Block a user