| title | Services Reference |
|---|---|
| nav_order | 8 |
All classes live in CHDStudio/Services/. Namespaces are CHDStudio.Services unless noted.
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 aSocketsHttpHandlerwith:- TLS 1.2 + TLS 1.3 only (
EnabledSslProtocols), PooledConnectionLifetime = 10 minutes,- default header
Accept: application/json.
- TLS 1.2 + TLS 1.3 only (
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 nextClientaccess rebuilds them. Called fromApp_Exit.
internal class ArchiveService : IDisposable (ArchiveService.cs:23)
Decompresses archives and compressed images for the conversion pipeline.
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.
| 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. |
- 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 requiresestimated + 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.exeon Windows,7zzon Linux/macOS): zip/7z failures (except cancellation) fall back tox "<archive>" -o"<output>" -ywhen 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 viaRarVolumeSet.FindFirstVolume(a later.partNN.raris redirected; a missing first volume raises a multi-part error) and opensRarArchive.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'sNullReferenceException/ArgumentOutOfRangeException/IndexOutOfRangeExceptiondecoder crashes) are rethrown for classification instead of retried. - Retries: ZIP open/entry writes retry 3 times on
IOException/UnauthorizedAccessExceptionwithattempt * 1000 mssleeps;CopyFileWithRetrycopies 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 atDebug("will fall back to temp-copy extraction") instead ofError. - 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.binfiles 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.RemoveCompanionDataFilesAsyncdrops 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. It also drops a.ccdthat a.cueof the same base name already describes. .ecmis not a primary target, so an archive containing only.ecmfiles still reports nothing supported. Loose.ecmfiles convert normally.- Cancellation: observed throughout;
OperationCanceledExceptionis rethrown.
internal class BugReportApiSink : ILogEventSink (BugReportApiSink.cs:13)
Serilog sink that forwards Warning and above log events to BugReportService.SendBugReportAsync.
- Ignores
LogEventLevel.Informationand 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.
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 headerX-API-KEY; returns whether the server accepted it. Excluded messages returnfalsewithout 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).
internal sealed class FileWatcherService : IDisposable (FileWatcherService.cs:12)
Explains why a file disappeared mid-batch.
StartWatching(folderPath)—FileSystemWatcherwithIncludeSubdirectories=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.
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).
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
screenshotsfolder under%LocalAppData%\CHDStudio(created on demand), beside thelogsfolder. When that folder is not writable, the capture falls back to ascreenshotsfolder next to the application executable. - Filename:
screenshot_yyyy-MM-dd_HH-mm-ss-fff.png. TakeScreenshot(Window)(static) returns the saved path, ornullon failure (the error is logged).- Triggered by the F8 hotkey (see User Guide).
internal class StatsService (StatsService.cs:8)
Records anonymous usage statistics once per launch.
RecordUsageAsync()— POSTs{ applicationId, version }withAuthorization: Bearer {apiKey}tohttps://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.Debugwith the exception, silently swallowed.
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:
- 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. - 403/429 → "GitHub API rate limit exceeded. Skipping update check." — no bug report.
- 5xx → "Update check skipped: GitHub server error." — no bug report.
- Deserialize
GitHubRelease(tag_name,html_url,name,body,prerelease,draft). - Skip draft/prerelease/empty tags.
- Compare versions (
TryNormalizeVersions— 4-part versions with-1parts normalized to 0;ParseVersionFromTagstrips prefixes likev/release/versionand leading non-digits). - 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. - Network/SSL errors → logged, no bug report. HTTP errors with a status code and generic exceptions → logged and reported via
onBugReport.
- GET
TryNormalizeVersions/ParseVersionFromTagareinternal staticand heavily unit-tested.
internal static class ChdSharpEncoderService (ChdSharpEncoderService.cs:11)
In-process CHD encoder backed by the CHDSharp library (CHDSharpLib 1.6.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.
| Member | Purpose |
|---|---|
Encode(command, inputPath, outputPath, rawUnits2352, taskCount, token, onHunkCompleted = null, conservativeCodecs = false, codecTags = null, parentPath = null) (static) |
Encodes inputPath to outputPath using chdman's commands and defaults. codecTags overrides the command's default codec list; parentPath writes a delta child. 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 when using the default codec list. onHunkCompleted is forwarded to ChdEncodeOptions.HunkCompleted for progress logging. |
taskCount (chdman's -np) is clamped to 1–64 before it reaches ChdEncodeOptions.TaskCount.
| 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. |
The codec column is the Default preset. The Convert tab's Compression selector overrides it through codecTags; parentPath adds -op/ChdEncodeOptions.ParentPath for delta output. The conservative retry always uses the compatible set.
- Called from
ConvertToChdAsync's localTryChdSharpInProcessAsyncon 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 afterEncodereturns. - The log line is
CHDSHARP: <command> <file>and mirrors the chdman invocation it replaces; progress is logged throughChdSharpProgressLogger(CHDSHARP: Compressing, N% complete... (ratio=...)). - Because the encoder is always present,
CheckDependenciesAndNotifyUsernever refuses conversion; a missingchdmanon Windows is only a notice. - Tests:
ChdSharpEncoderServiceTestsround-tripscreatecd/createdvd/createhd/createraw/createldand verifies each output withChd.CheckFile - Codec presets and delta output:
codecTagsandparentPathparameters, used by the Convert tab's compression selector and delta-CHD option.
internal static class ChdRecompressService (ChdRecompressService.cs)
Recompresses an existing CHD with CHDSharp's ChdEncoder.Copy. It preserves metadata, optionally upgrades legacy CD/GD tags, and can resolve a differential child source against its parent and write a standalone output.
| Member | Purpose |
|---|---|
Recompress(sourcePath, outputPath, preset, parentPath, upgradeMetadata, taskCount, token, onHunkCompleted = null) (static) |
Opens the source (with parentPath when it is a child), chooses codec tags for the selected ChdCompressionPreset, and calls ChdEncoder.Copy. Throws InvalidDataException when the source cannot be opened. Uncompressed laserdisc output falls back to the compatible AVHuff codec set; CD output supports none. |
The command used for preset selection is derived from the source flags (createcd, createdvd, createhd, createraw, or createld when the source carries AVAV metadata). Auto only detects a platform for CD/DVD sources, and auto-preset codecs whose media type does not match the command are rejected in favour of the defaults.
internal static class ParentChdResolver (ParentChdResolver.cs)
Resolves the parent of a differential child CHD for verification, extraction, checksumming, Image Info and recompression.
| Member | Purpose |
|---|---|
Resolve(childPath, explicitParentPath = null, autoDetect = true) (static) |
Reads the child header. If no parent is required it returns IsRequired = false. An explicit parent is validated and returned (with a warning when its hash differs). Otherwise sibling .chd files are matched against the child's ParentSha1/ParentMd5. Returns a user-facing Error when a required parent cannot be found. |
PrepareBatch(files, explicitParentPath, autoDetect, cancellationToken) (static) |
Resolves every file once and returns a BatchPlan with the files ordered children-first and the precomputed Resolution for each path. Callers run it on a background task and use plan.ResolutionFor(file) in the batch loop. |
OrderChildrenFirst(files, explicitParentPath, autoDetect) (static) |
Convenience wrapper over PrepareBatch that returns only the ordered paths. |
Only the fixed 76/80/120/108/124-byte CHD header is read to obtain the parent hashes; the compressed hunk map is deliberately not parsed. PrepareBatch shares one header cache across the batch, so a folder with many children reads each sibling header once instead of re-reading it for every child.
internal static class ChdCompressionPresets (Utilities/ChdCompressionPresets.cs)
Maps a ChdCompressionPreset to codec tags and chdman -c arguments. Defaults preserve the existing chdman-compatible behaviour; Smallest uses zstd/cdzs, Compatible uses zlib/cdzl, Auto uses PlatformDetector.AutoCodecs, and Uncompressed is supported for raw/DVD/HDD and CD media. Laserdisc output requires AVHuff.
ArchiveProgressLogger (Utilities/ArchiveProgressLogger.cs) implements IProgress<ArchiveProgress> and logs archive extraction at ten-percent steps. ArchiveService.ExtractArchiveAsync reports bytes and entry counts from ExtractZipWithOpenRead; ZIP entry copies check the cancellation token and report progress while writing, so a long archive no longer looks frozen between the start and end log lines. — see Testing §11.6.