From 75b5121aa45cdbb046f9bdd32c2c8d7175522567 Mon Sep 17 00:00:00 2001 From: justcoding121 Date: Mon, 31 Aug 2026 08:25:23 -0500 Subject: [PATCH 1/9] fix(ci): redeploy website on beta/stable and after product releases download.data.ts bakes GitHub Release zip/MSI URLs at Pages build time. GITHUB_TOKEN release creates do not cascade Deploy website, so the download page stayed empty after v7.0.2-beta assets landed. - Always run Deploy website on push to develop/beta/stable - Allow deploy job on beta/stable refs - After release.yml publish-release, explicitly gh workflow run deploy-website --- .github/workflows/deploy-website.yml | 22 ++++++++++++++-------- .github/workflows/release.yml | 17 +++++++++++++++++ 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/.github/workflows/deploy-website.yml b/.github/workflows/deploy-website.yml index 22c510ba3..084523af7 100644 --- a/.github/workflows/deploy-website.yml +++ b/.github/workflows/deploy-website.yml @@ -1,16 +1,13 @@ name: Deploy website on: + # Always on product-channel and develop pushes so download.data.ts rebuilds when + # beta/stable merges land (those merges often do not touch website/**). push: branches: - develop - paths: - - 'website/**' - - 'wiki/images/**' - - 'docs/api/**' - - 'docs/styles/**' - - 'docs/fonts/**' - - '.github/workflows/deploy-website.yml' + - beta + - stable pull_request: branches: - develop @@ -21,6 +18,8 @@ on: - 'docs/styles/**' - 'docs/fonts/**' - '.github/workflows/deploy-website.yml' + # Human-published releases. Actions GITHUB_TOKEN `gh release create` does not + # cascade this event — release.yml also dispatches this workflow after assets. release: types: [published] workflow_dispatch: @@ -84,7 +83,14 @@ jobs: path: website/.vitepress/dist deploy: - if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/develop' || github.event_name == 'release' || github.event_name == 'workflow_dispatch') + if: > + github.event_name != 'pull_request' && ( + github.ref == 'refs/heads/develop' || + github.ref == 'refs/heads/beta' || + github.ref == 'refs/heads/stable' || + github.event_name == 'release' || + github.event_name == 'workflow_dispatch' + ) needs: build runs-on: ubuntu-latest permissions: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f8ce7b332..cd2eda428 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -330,6 +330,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: write + actions: write steps: - uses: actions/checkout@v6 with: @@ -444,3 +445,19 @@ jobs: fi rm -f "$NOTES_FILE" fi + + - name: Refresh website download links + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_CHANNEL: ${{ needs.resolve-version.outputs.channel }} + run: | + set -euo pipefail + # GITHUB_TOKEN `gh release create` does not fire `release: published` for + # other workflows. Rebuild Pages so download.data.ts sees new zip/MSI assets. + REF=develop + case "$RELEASE_CHANNEL" in + beta) REF=beta ;; + stable) REF=stable ;; + esac + echo "Dispatching deploy-website.yml --ref $REF (channel=$RELEASE_CHANNEL)" + gh workflow run deploy-website.yml --ref "$REF" From 6917e0d18c6cb62364bf73987aaab73ea7211114 Mon Sep 17 00:00:00 2001 From: justcoding121 Date: Mon, 31 Aug 2026 08:30:13 -0500 Subject: [PATCH 2/9] fix(ci): keep /download fresh after beta/stable releases - Always deploy Pages on push to develop/beta/stable - After release.yml publishes zip/MSI, dispatch Deploy website (GITHUB_TOKEN does not cascade release:published) - Mirror *.html as dir/index.html so GitHub Pages clean URL /download is not stuck on a stale object while /download.html updates - Fail the site build if GitHub releases API yields no download assets --- .github/workflows/deploy-website.yml | 23 ++++++++++++++++ website/download.data.ts | 39 ++++++++++++++++------------ 2 files changed, 45 insertions(+), 17 deletions(-) diff --git a/.github/workflows/deploy-website.yml b/.github/workflows/deploy-website.yml index 084523af7..6753dfb6e 100644 --- a/.github/workflows/deploy-website.yml +++ b/.github/workflows/deploy-website.yml @@ -76,6 +76,29 @@ jobs: echo "Warning: docs/api missing; API pages will not be published" fi + # GitHub Pages + VitePress cleanUrls: /download sometimes keeps a stale + # object while /download.html updates. Publish both shapes so the nav link works. + - name: Mirror cleanUrls HTML as index.html + shell: bash + run: | + set -euo pipefail + dist=website/.vitepress/dist + for f in "$dist"/*.html; do + base="$(basename "$f" .html)" + [[ "$base" == "index" || "$base" == "404" ]] && continue + mkdir -p "$dist/$base" + cp -f "$f" "$dist/$base/index.html" + done + # Nested docs pages + if [ -d "$dist/docs" ]; then + find "$dist/docs" -name '*.html' ! -name 'index.html' ! -name '404.html' | while read -r f; do + base="$(basename "$f" .html)" + dir="$(dirname "$f")" + mkdir -p "$dir/$base" + cp -f "$f" "$dir/$base/index.html" + done + fi + - name: Upload Pages artifact if: github.event_name != 'pull_request' uses: actions/upload-pages-artifact@v3 diff --git a/website/download.data.ts b/website/download.data.ts index ce96270b0..029509e92 100644 --- a/website/download.data.ts +++ b/website/download.data.ts @@ -47,15 +47,6 @@ type GhRelease = { assets: Array<{ name: string; browser_download_url: string }> } -function emptyLinks(): DownloadLinks { - return { - cli: {}, - inspector: {}, - releasesUrl: RELEASES, - latestTag: null, - } -} - function githubHeaders(): Record { const headers: Record = { Accept: 'application/vnd.github+json', @@ -94,15 +85,29 @@ function mapReleases(releases: GhRelease[]): DownloadLinks { export default defineLoader({ async load(): Promise { + const url = + 'https://api.github.com/repos/justcoding121/titanium-web-proxy/releases?per_page=40' try { - const res = await fetch( - 'https://api.github.com/repos/justcoding121/titanium-web-proxy/releases?per_page=40', - { headers: githubHeaders() }, - ) - if (!res.ok) return emptyLinks() - return mapReleases((await res.json()) as GhRelease[]) - } catch { - return emptyLinks() + const res = await fetch(url, { headers: githubHeaders() }) + if (!res.ok) { + throw new Error(`GitHub releases API ${res.status} ${res.statusText}`) + } + const links = mapReleases((await res.json()) as GhRelease[]) + const cliCount = Object.keys(links.cli).length + if (cliCount === 0) { + console.warn( + '[download.data] GitHub releases returned no CLI zip assets — download buttons will be empty', + ) + } else { + console.log( + `[download.data] resolved ${cliCount} CLI RIDs from tag ${links.cli['win-x64']?.tag ?? links.latestTag}`, + ) + } + return links + } catch (e) { + console.error('[download.data] failed to load GitHub releases:', e) + // Fail the Pages build rather than silently shipping empty download buttons. + throw e } }, }) From f134957282c638876672b5ac9f96e333d998aa1a Mon Sep 17 00:00:00 2001 From: justcoding121 Date: Mon, 31 Aug 2026 08:43:24 -0500 Subject: [PATCH 3/9] Make session cache and log folders discoverable; quiet peer H2 INTERNAL_ERROR RSTs. Show resolved spill/log paths with Open folder actions, and demote peer INTERNAL_ERROR resets so long-lived streams stop flooding Error logs. --- .../Services/DesktopShell.cs | 191 ++++++++++++++++++ .../Services/SessionBodyDiskCache.cs | 10 + .../Services/SessionStore.cs | 8 +- .../Views/LoggingSettingsWindow.axaml | 12 +- .../Views/LoggingSettingsWindow.axaml.cs | 11 + .../Views/SessionRetentionWindow.axaml | 19 +- .../Views/SessionRetentionWindow.axaml.cs | 12 ++ src/Titanium.Web.Proxy/Http2/Http2Helper.cs | 5 +- .../AutomationIdCoverageHeadlessTests.cs | 45 ++++- .../DesktopShellAndPathTests.cs | 150 ++++++++++++++ 10 files changed, 452 insertions(+), 11 deletions(-) create mode 100644 src/Titanium.Inspector/Services/DesktopShell.cs create mode 100644 tests/Titanium.Inspector.Tests/DesktopShellAndPathTests.cs diff --git a/src/Titanium.Inspector/Services/DesktopShell.cs b/src/Titanium.Inspector/Services/DesktopShell.cs new file mode 100644 index 000000000..6c236f0b1 --- /dev/null +++ b/src/Titanium.Inspector/Services/DesktopShell.cs @@ -0,0 +1,191 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Titanium.Inspector.Services; + +/// +/// Cross-platform open-in-file-manager helpers. Command builders are pure for unit tests; +/// / invoke the OS. +/// +public static class DesktopShell +{ + public readonly record struct ShellCommand(string FileName, string Arguments); + + public static bool TryBuildOpenDirectory( + string? directory, + OSPlatform platform, + out ShellCommand command, + out string? error) + { + command = default; + error = null; + if (string.IsNullOrWhiteSpace(directory)) + { + error = "Directory path is empty."; + return false; + } + + var full = Path.GetFullPath(directory.Trim()); + if (platform == OSPlatform.Windows) + { + command = new ShellCommand("explorer.exe", QuoteWindowsArg(full)); + return true; + } + + if (platform == OSPlatform.OSX) + { + command = new ShellCommand("open", QuoteUnixArg(full)); + return true; + } + + if (platform == OSPlatform.Linux) + { + command = new ShellCommand("xdg-open", QuoteUnixArg(full)); + return true; + } + + error = "Unsupported operating system."; + return false; + } + + public static bool TryBuildRevealFile( + string? filePath, + OSPlatform platform, + out ShellCommand command, + out string? error) + { + command = default; + error = null; + if (string.IsNullOrWhiteSpace(filePath)) + { + error = "File path is empty."; + return false; + } + + var full = Path.GetFullPath(filePath.Trim()); + var directory = Path.GetDirectoryName(full); + if (string.IsNullOrEmpty(directory)) + { + error = "File path has no directory."; + return false; + } + + if (platform == OSPlatform.Windows) + { + // explorer /select,"C:\path\file.log" + command = new ShellCommand("explorer.exe", "/select," + QuoteWindowsArg(full)); + return true; + } + + if (platform == OSPlatform.OSX) + { + command = new ShellCommand("open", "-R " + QuoteUnixArg(full)); + return true; + } + + if (platform == OSPlatform.Linux) + { + // No portable reveal; open the containing folder. + return TryBuildOpenDirectory(directory, platform, out command, out error); + } + + error = "Unsupported operating system."; + return false; + } + + public static bool TryOpenDirectory(string? directory, out string? error) => + TryOpenDirectory(directory, GetCurrentPlatform(), out error); + + public static bool TryOpenDirectory(string? directory, OSPlatform platform, out string? error) + { + if (!TryBuildOpenDirectory(directory, platform, out var command, out error)) + { + return false; + } + + try + { + Directory.CreateDirectory(Path.GetFullPath(directory!.Trim())); + Start(command); + return true; + } + catch (Exception ex) + { + error = ex.Message; + return false; + } + } + + public static bool TryRevealFileOrOpenDirectory(string? filePath, out string? error) => + TryRevealFileOrOpenDirectory(filePath, GetCurrentPlatform(), out error); + + public static bool TryRevealFileOrOpenDirectory(string? filePath, OSPlatform platform, out string? error) + { + if (string.IsNullOrWhiteSpace(filePath)) + { + error = "File path is empty."; + return false; + } + + var full = Path.GetFullPath(filePath.Trim()); + var directory = Path.GetDirectoryName(full); + if (string.IsNullOrEmpty(directory)) + { + error = "File path has no directory."; + return false; + } + + try + { + Directory.CreateDirectory(directory); + if (!TryBuildRevealFile(full, platform, out var command, out error)) + { + return false; + } + + Start(command); + return true; + } + catch (Exception ex) + { + error = ex.Message; + return false; + } + } + + public static OSPlatform GetCurrentPlatform() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return OSPlatform.Windows; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return OSPlatform.OSX; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return OSPlatform.Linux; + } + + return OSPlatform.Create("Unknown"); + } + + private static void Start(ShellCommand command) + { + Process.Start(new ProcessStartInfo + { + FileName = command.FileName, + Arguments = command.Arguments, + UseShellExecute = false, + }); + } + + private static string QuoteWindowsArg(string path) => + "\"" + path.Replace("\"", "\"\"", StringComparison.Ordinal) + "\""; + + private static string QuoteUnixArg(string path) => + "'" + path.Replace("'", "'\\''", StringComparison.Ordinal) + "'"; +} diff --git a/src/Titanium.Inspector/Services/SessionBodyDiskCache.cs b/src/Titanium.Inspector/Services/SessionBodyDiskCache.cs index e2a603e59..c39278341 100644 --- a/src/Titanium.Inspector/Services/SessionBodyDiskCache.cs +++ b/src/Titanium.Inspector/Services/SessionBodyDiskCache.cs @@ -30,6 +30,16 @@ public SessionBodyDiskCache(string directory, long maxBytes, TimeSpan maxAge) PruneOnStartup(); } + /// + /// Default spill directory under LocalApplicationData (Windows LocalAppData, + /// Linux ~/.local/share, macOS Application Support). + /// + public static string GetDefaultDirectory() => + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "TitaniumInspector", + "session-cache"); + public string DirectoryPath => _directory; public string PathFor(long sessionId) => Path.Combine(_directory, sessionId.ToString("D") + ".bin"); diff --git a/src/Titanium.Inspector/Services/SessionStore.cs b/src/Titanium.Inspector/Services/SessionStore.cs index 90a9f12d0..3da2c0f3c 100644 --- a/src/Titanium.Inspector/Services/SessionStore.cs +++ b/src/Titanium.Inspector/Services/SessionStore.cs @@ -26,10 +26,7 @@ public SessionStore(SessionStoreOptions? options = null, string? cacheDirectory _options = options ?? new SessionStoreOptions(); if (_options.SpillBodiesToDisk) { - var dir = cacheDirectory ?? Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "TitaniumInspector", - "session-cache"); + var dir = cacheDirectory ?? SessionBodyDiskCache.GetDefaultDirectory(); _disk = new SessionBodyDiskCache( dir, _options.DiskCacheMaxBytes, @@ -48,6 +45,9 @@ public SessionStore(SessionStoreOptions? options = null, string? cacheDirectory public ObservableCollection Sessions { get; } + /// Resolved body spill directory when disk spill is enabled; otherwise null. + public string? DiskCacheDirectoryPath => _disk?.DirectoryPath; + public SessionStoreOptions Options => _options; public int Count diff --git a/src/Titanium.Inspector/Views/LoggingSettingsWindow.axaml b/src/Titanium.Inspector/Views/LoggingSettingsWindow.axaml index 56e921db2..111c78614 100644 --- a/src/Titanium.Inspector/Views/LoggingSettingsWindow.axaml +++ b/src/Titanium.Inspector/Views/LoggingSettingsWindow.axaml @@ -3,7 +3,7 @@ x:Class="Titanium.Inspector.Views.LoggingSettingsWindow" AutomationProperties.AutomationId="LoggingSettingsWindow" Title="Logging" - Width="520" Height="380" + Width="520" Height="400" WindowStartupLocation="CenterOwner" CanResize="False"> @@ -13,6 +13,8 @@