Skip to content

Latest commit

 

History

History
195 lines (129 loc) · 15.3 KB

File metadata and controls

195 lines (129 loc) · 15.3 KB
title Services Reference
nav_order 8

7. Services Reference

All classes live in CHDStudio/Services/. Namespaces are CHDStudio.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
  • Files: maxcso.exe, psxpackager.exe

A Screenshot folder next to the executable is deliberately not cleaned: it is the fallback location for F8 screenshots.

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


7.7 ScreenshotService

internal static class ScreenshotService (ScreenshotService.cs:15)

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: the screenshots folder under %LocalAppData%\CHDStudio (created on demand), beside the logs folder. When that folder is not writable, the capture falls back to a screenshots folder next to the application executable.
  • 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, string releaseUrl, Version? currentVersion, ...) for testing.
  • Flow:
    1. GET AppConfig.PrimaryGitHubApiLatestReleaseUrl (https://api.github.com/repos/purelogiccode/CHDStudio/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, onHunkCompleted = null) (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. onHunkCompleted is forwarded to ChdEncodeOptions.HunkCompleted for progress logging.

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.
createld ChdEncoder.EncodeLaserDisc one raw A/V frame (auto) A/V frame avhu AVI (YUY2/VYUY/UYVY) input; one frame per hunk, matching chdman's createld default.

Integration

  • Called from ConvertToChdAsync's local TryChdSharpInProcessAsync 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; progress is logged through ChdSharpProgressLogger (CHDSHARP: Compressing, N% complete... (ratio=...)).
  • 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/createld and verifies each output with Chd.CheckFile — see Testing §11.6.