Skip to content

07 services reference

github-actions[bot] edited this page Oct 2, 2026 · 8 revisions

7. Services Reference

All classes live in BatchConvertToCHD/Services/. Namespaces are BatchConvertToCHD.Services unless noted.


7.1 AppHttpClient

internal static class AppHttpClient (AppHttpClient.cs:13)

Thread-safe singleton HttpClient used by BugReportService, StatsService, and UpdateService.

  • internal static HttpClient Client — double-checked locking; builds a SocketsHttpHandler with:
    • TLS 1.2 + TLS 1.3 only (EnabledSslProtocols),
    • PooledConnectionLifetime = 10 minutes,
    • default header Accept: application/json.
  • ServerCertificateValidationCallback (:50):
    • clean chain → accept;
    • name mismatch (RemoteCertificateNameMismatch) → warn ("may be caused by a proxy or firewall intercepting the connection") and accept;
    • any other error (expired, revoked, chain) → warn and reject.
  • internal static void Dispose() — disposes client+handler; the next Client access rebuilds them. Called from App_Exit.

7.2 ArchiveService

internal class ArchiveService : IDisposable (ArchiveService.cs:23)

Decompresses archives and compressed images for the conversion pipeline.

Construction

ArchiveService(string sevenZipExePath, bool isSevenZipAvailable) — the app passes the resolved 7-Zip executable path (7za.exe on Windows, 7zz on Linux/macOS; probed in the app directory first, then PATH) plus whether it exists.

API

Member Purpose
ExtractCsoAsync(...) (static) Decompresses a .cso/.ciso to an ISO via CSOSharp (CsoFile.Open → ExtractToIso). Returns (Success, FilePath, TempDir, ErrorMessage).
ExtractArchiveAsync(originalArchivePath, tempDirectoryRoot, onLog, token, totalSetSize = -1) Dispatches by extension and content: .zip → ExtractZipWith7ZaFallbackAsync; .7z → ExtractSevenZipArchiveAsync; .rar, or any file whose first bytes say Rar! (a .001 first volume) → ExtractRarArchive. totalSetSize overrides the disk-space estimate when the caller already measured a volume set. Returns (Success, List<string> FilePaths, TempDir, ErrorMessage).
ExtractArchiveWithFallback<TArchive>(...) (static) Shared SharpCompress extraction for single-stream archives (7z) with temp-copy fallback; TArchive : IArchive, IDisposable. RAR does not use it: a multi-volume RAR must be opened by path through its first volume.
IsMultiPartRarError(Exception) (static) True for SharpCompress multi-part RAR messages (missing volume, or a first volume that could not be found).
IsNetworkUnavailableError(Exception) (static) True when the exception chain indicates an unavailable network location: Win32 error codes read from HResult (53 bad netpath, 59 unexpected network error, 64 netname deleted, 67 bad net name, 1203/1222/1231) — locale-independent, so non-English Windows builds are recognized — with English message substrings and the French "réseau" as fallbacks.
RarVolumeSet.GetTotalBytes(path) (static, Utilities) Sizes the whole set a .rar input belongs to, so the disk-space check counts every volume.

Key behaviors

  • Pre-extraction disk check (CheckTempDiskSpace): estimates the uncompressed size (sum of ZIP entry lengths; for RAR the summed volume sizes; otherwise the archive file size) and requires estimated + max(est/10, 100 MB) free, else extraction is refused with a clear message.
  • Zip-slip protection: every entry destination must be under the normalized output directory, otherwise SecurityException ("Attempted to extract file outside of the target directory.").
  • 7-Zip fallback (7za.exe on Windows, 7zz on Linux/macOS): zip/7z failures (except cancellation) fall back to x "<archive>" -o"<output>" -y when the tool is available. Exit code 2 or "Is not archive"/"Cannot open" output → InvalidDataException "archive is invalid or corrupt".
  • RAR extraction (ExtractRarArchive): resolves the first volume of the set via RarVolumeSet.FindFirstVolume (a later .partNN.rar is redirected; a missing first volume raises a multi-part error) and opens RarArchive.OpenArchive(new FileInfo(firstVolume)), which lets SharpCompress follow the remaining volumes. A direct-read failure that is not archive damage copies every volume to a temp folder and retries there. Archive-damage exceptions (including SharpCompress's NullReferenceException/ArgumentOutOfRangeException/IndexOutOfRangeException decoder crashes) are rethrown for classification instead of retried.
  • Retries: ZIP open/entry writes retry 3 times on IOException/UnauthorizedAccessException with attempt * 1000 ms sleeps; CopyFileWithRetry copies the source archive to temp with the same 4-attempt schedule before the extraction fallback (missing-source errors are not retried), so a transient NAS/SMB hiccup that breaks direct streaming also gets a second chance on the temp copy; SharpCompress temp-copy fallback covers locked source files. A failed direct extraction is logged at Debug ("will fall back to temp-copy extraction") instead of Error.
  • Error categorization (converted to user-facing messages): unsupported ZIP compression method (Deflate64/LZMA/PPMd — re-compress advice), corrupt/incomplete archive (including SharpCompress RAR-decoder NullReferenceException/ArgumentOutOfRangeException/IndexOutOfRangeException), encrypted archive (CryptographicException), missing multi-part RAR volume, disk full (HResult -2147024784/-2147024783), locked file, network unavailable.
  • Post-extraction scan: collects files matching PrimaryTargetExtensionsSet (.cue/.iso/.img/.gdi/.toc/.raw/.ccd/.mds/.isz); if none and .bin files exist, a (Track N) set becomes a multi-FILE cue (TrackBinCueBuilder), otherwise a MODE2/2352 auto-cue is generated for the largest bin (BinCueGenerator); if nothing supported → "No supported primary files found in archive."
  • Companion filtering: InputFileFilter.RemoveCompanionDataFilesAsync drops raw images that a descriptor in the archive already covers, so a cue/bin or CloneCD set inside an archive converts once through its descriptor rather than once per file with both attempts aimed at the same output name.
  • .ecm is not a primary target, so an archive containing only .ecm files still reports nothing supported. Loose .ecm files convert normally.
  • Cancellation: observed throughout; OperationCanceledException is rethrown.

7.3 BugReportApiSink

internal class BugReportApiSink : ILogEventSink (BugReportApiSink.cs:13)

Serilog sink that forwards Warning and above log events to BugReportService.SendBugReportAsync.

  • Ignores LogEventLevel.Information and below.
  • Drops messages matching BugReportService.IsExcludedFromBugReport.
  • Flood control: a static interlocked flag allows only one in-flight send; the flag is released in a ContinueWith(ExecuteSynchronously) continuation. Fire-and-forget.

7.4 BugReportService

internal class BugReportService (BugReportService.cs:14)

Client for the PureLogicCode BugReport API. Full details and the exclusion list in Bug Reporting System.

  • SendBugReportAsync(message, ex?, token) — POSTs { message (formatted report), applicationName, version, userInfo (Environment.UserName), environment, stackTrace } with header X-API-KEY; returns whether the server accepted it. Excluded messages return false without any HTTP call; send failures are logged at Debug and swallowed.
  • IsExcludedFromBugReport(message) — case-insensitive substring match against 38 known-noise patterns.
  • BuildFormattedReport — builds the === Environment Details === / === Error Details === / === Exception Details === sections (inner-exception chain, max depth 5).

7.5 FileWatcherService

internal sealed class FileWatcherService : IDisposable (FileWatcherService.cs:12)

Explains why a file disappeared mid-batch.

  • StartWatching(folderPath) — FileSystemWatcher with IncludeSubdirectories=true, NotifyFilter = FileName | DirectoryName, 64 KB internal buffer; handles Deleted/Renamed/Created/Error; tolerates bad paths and disconnected drives.
  • StopWatching() / Dispose().
  • GetContextForMissingFile(filePath) — returns a human-readable diagnostic: not watched, folder inaccessible, deleted at time, renamed from/to, created-then-gone, or never observed. History is capped at 1,000 events (FIFO); buffer overflow clears history to avoid stale diagnostics.
  • Supporting types: FileEventRecord { Timestamp, EventType, RelatedName }, FileWatchEventType { Deleted, RenamedFrom, RenamedTo, Created }.

The watcher is started when the user picks the conversion input folder and is consumed in ProcessSingleFileForConversionAsync when a selected file is gone.


7.6 LegacyCleanupService

internal static class LegacyCleanupService (LegacyCleanupService.cs:10)

Runs once at startup on a background thread and deletes leftovers from older versions next to the executable:

  • Folders: logs, Resources, Screenshot
  • Files: maxcso.exe, psxpackager.exe

All failures are silently ignored (files may be in use).


7.7 ScreenshotService

internal sealed class ScreenshotService (ScreenshotService.cs:14)

Captures the focused application window and saves it as a PNG. F8 is handled by the window itself (MainWindow_KeyDown in MainWindow.axaml.cs), so the hotkey only fires while the app window is focused; the capture is rendered with Avalonia's RenderTargetBitmap (sized from the window bounds and RenderScaling), so it works the same on every platform:

  • Location: %LocalAppData%\BatchConvertToCHD\screenshots (created on demand; the platform equivalent elsewhere).
  • Filename: screenshot_yyyy-MM-dd_HH-mm-ss-fff.png.
  • TakeScreenshot(Window) (static) returns the saved path, or null on failure (the error is logged).
  • Triggered by the F8 hotkey (see User Guide).

7.8 StatsService

internal class StatsService (StatsService.cs:8)

Records anonymous usage statistics once per launch.

  • RecordUsageAsync() — POSTs { applicationId, version } with Authorization: Bearer {apiKey} to https://www.purelogiccode.com/ApplicationStats/stats.
  • HTTP 429 → Logger.Debug("Usage statistics rate-limited (HTTP 429) - this is expected behavior") — treated as expected, no retry, no bug report.
  • Other non-success statuses → Logger.Information (below the bug-report threshold).
  • Network errors → Logger.Debug with the exception, silently swallowed.

7.9 UpdateService

internal class UpdateService (UpdateService.cs:13)

Checks GitHub for new releases at startup.

  • CheckForNewVersionAsync(onLog, onStatusUpdate, onBugReport) — wrapper; core overload takes (HttpClient, Version? currentVersion, ...) for testing.
  • Flow:
    1. GET the configured release URL (AppConfig.GitHubApiLatestReleaseUrls: https://api.github.com/repos/purelogiccode/BatchConvertToCHD/releases/latest) with a User-Agent. Rate limits (403/429) skip the check entirely because they are per-IP; other failures fall through to error handling.
    2. 403/429 → "GitHub API rate limit exceeded. Skipping update check." — no bug report.
    3. 5xx → "Update check skipped: GitHub server error." — no bug report.
    4. Deserialize GitHubRelease (tag_name, html_url, name, body, prerelease, draft).
    5. Skip draft/prerelease/empty tags.
    6. Compare versions (TryNormalizeVersions — 4-part versions with -1 parts normalized to 0; ParseVersionFromTag strips prefixes like v/release/version and leading non-digits).
    7. Newer version → Dispatcher message box ("A new version ... Would you like to go to the download page?"); on Yes opens html_url. If the browser fails, the URL is copied to the clipboard (with its own bug-report path on failure) and a "Browser Launch Failed" dialog shows the URL.
    8. Network/SSL errors → logged, no bug report. HTTP errors with a status code and generic exceptions → logged and reported via onBugReport.
  • TryNormalizeVersions / ParseVersionFromTag are internal static and heavily unit-tested.

7.10 ChdSharpEncoderService

internal static class ChdSharpEncoderService (ChdSharpEncoderService.cs:11)

In-process CHD encoder backed by the CHDSharp library (CHDSharpLib 1.4.3). It replaced the bundled CHDSharp.exe command-line tool, so every platform has an encoder without shipping a native executable: on Windows it is the automatic fallback behind chdman; on Linux/macOS it is the only encoder and is always used.

API

Member Purpose
Encode(command, inputPath, outputPath, rawUnits2352, taskCount, token) (static) Encodes inputPath to outputPath using chdman's commands and defaults. Writes to the caller's staging path; throws ArgumentException for an unsupported command, OperationCanceledException on cancellation, and encoding exceptions on failure. Output is byte-identical to chdman 0.289.

taskCount (chdman's -np) is clamped to 1–64 before it reaches ChdEncodeOptions.TaskCount.

Command / default mapping

Command CHDSharp call Hunk Unit Codecs Metadata / extras
createcd ChdEncoder.EncodeCd 19584 2448 cdlz,cdzl,cdfl CUE/GDI/TOC/ISO parsed by the library; a 2352-byte source gets its 96-byte subcode portion zero-filled by the reader, so -us 2352 is not needed.
createdvd ChdEncoder.EncodeRaw 4096 2048 lzma,zlib,huff,flac MetadataWriter.BuildDvdMetadata() — the DVD tag that makes it a DVD.
createhd ChdEncoder.EncodeRaw 4096 512 lzma,zlib,huff,flac CHS geometry guessed from the image size (MetadataWriter.GuessChs), GDDD metadata, logical length = geometry product (sub-geometry inputs round up past the file length).
createraw ChdEncoder.EncodeRaw largest multiple of the unit ≤ 4096 2352 when rawUnits2352, else 512 lzma,zlib,huff,flac -us 2352 equivalent for raw CD tracks.

Integration

  • Called from ConvertToChdAsync's local TryChdSharpInProcessAsync (MainWindow.axaml.cs:5995) on a background thread (Task.Run), after the input has been prepared (ASCII copy or cue work directory). The staged output is moved into place only after Encode returns.
  • The log line is CHDSHARP: <command> <file> and mirrors the chdman invocation it replaces.
  • Because the encoder is always present, CheckDependenciesAndNotifyUser never refuses conversion; a missing chdman on Windows is only a notice.
  • Tests: ChdSharpEncoderServiceTests round-trips createcd/createdvd/createhd/createraw and verifies each output with Chd.CheckFile — see Testing §11.6.

Clone this wiki locally