diff --git a/AGENTS.md b/AGENTS.md index 278924c..2e1a827 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -87,6 +87,15 @@ cmake --build build-msvc SDL2 is optional but is the preferred cross-platform live display backend. If it is missing from vcpkg, install it with `vcpkg install sdl2:x64-windows`. +Windows packaging lives under `packaging/windows/`: + +- `packaging/windows/build-installer.ps1` stages the Release build and runs + `ISCC.exe`. +- `packaging/windows/hasciicam.iss` defines the installer. +- The installer is x64-only, machine-wide, and ships the virtual-camera helper + as an optional default-on component. +- `hasciicam_vcamctl.exe` owns virtual-camera install and uninstall actions. + The root `GNUmakefile` is a legacy Linux-oriented path. It assumes Unix linker flags such as SDL, X11, ncurses, and `libm`. Do not treat it as the cross-platform source of truth. diff --git a/README.md b/README.md index 186c38c..d27cef1 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,20 @@ cmake --preset windows-vcpkg-ninja ``` Other presets are `linux-ninja`, `macos-ninja`, and `wasm-emscripten`. +## Windows Installer + +HasciiCam ships a Windows installer built with Inno Setup 7. The packaging +entry point is `packaging/windows/build-installer.ps1`; it stages the CMake +install tree, copies the repository docs and licenses that CMake does not +install, and then invokes `ISCC.exe`. + +The installer is x64-only and machine-wide. The virtual-camera component is +checked by default, requires administrator rights, and targets Windows 11 +build 22000 or later. The component can be left out by choosing `Application +only` on the Components page. + +More detail lives in `docs/windows-installer.md`. + ## On-Screen GUI (SDL Live Mode) HasciiCam can show an optional on-screen control panel in live SDL mode. diff --git a/docs/smoke-tests.md b/docs/smoke-tests.md index 4246775..5d69c67 100644 --- a/docs/smoke-tests.md +++ b/docs/smoke-tests.md @@ -107,6 +107,22 @@ Checks: - live SDL output stays responsive while the loopback node is attached - a consumer application can open `/dev/video10` after the producer starts +## Windows (installer) + +Build the installer from a Release Windows build tree: + +```powershell +powershell -ExecutionPolicy Bypass -File packaging/windows/build-installer.ps1 -BuildDir build +``` + +Checks: + +- an installer appears in `releases/` +- the installer opens with `Full` selected by default +- the `Application only` type omits the virtual-camera component +- a Full install reports `hasciicam_vcamctl status` +- uninstall removes the PATH entry and the virtual-camera registration + ## Linux (size negotiation) ```sh diff --git a/docs/windows-installer.md b/docs/windows-installer.md new file mode 100644 index 0000000..2470b22 --- /dev/null +++ b/docs/windows-installer.md @@ -0,0 +1,44 @@ +# HasciiCam Windows Installer + +The Windows installer is built from `packaging/windows/hasciicam.iss` with the +helper script `packaging/windows/build-installer.ps1`. + +## Build + +```powershell +powershell -ExecutionPolicy Bypass -File packaging/windows/build-installer.ps1 -BuildDir build +``` + +The script: + +1. Runs `cmake --install` into a staging directory. +2. Copies the repository docs and license tree that CMake does not install. +3. Invokes `ISCC.exe` from `C:\Program Files\Inno Setup 7` unless overridden. + +## Installer shape + +- x64 only +- machine-wide +- `Full` is the default install type +- `Application only` omits the virtual-camera component +- the virtual-camera component is checked by default + +The virtual-camera component requires administrator rights and Windows 11 +build 22000 or later. It is installed and registered through +`hasciicam_vcamctl.exe`, not `regsvr32`. + +## Smoke test + +After building the installer, run a manual install in Windows Sandbox or a +disposable VM and verify: + +```powershell +.\releases\hasciicam--windows-x64-setup.exe +``` + +Then check: + +- `hasciicam --version` +- `hasciicam -h` +- `hasciicam_vcamctl status` +- uninstall removes the PATH entry and the virtual-camera registration diff --git a/packaging/windows/build-installer.ps1 b/packaging/windows/build-installer.ps1 new file mode 100644 index 0000000..c8c6cc1 --- /dev/null +++ b/packaging/windows/build-installer.ps1 @@ -0,0 +1,166 @@ +param( + [Parameter(Mandatory = $false)] + [string]$BuildDir = (Join-Path $PSScriptRoot '..\..\build'), + + [Parameter(Mandatory = $false)] + [string]$StageDir = (Join-Path $PSScriptRoot '..\..\build\installer-stage'), + + [Parameter(Mandatory = $false)] + [string]$OutputDir = (Join-Path $PSScriptRoot '..\..\releases'), + + [Parameter(Mandatory = $false)] + [string]$IsccPath = 'C:\Program Files\Inno Setup 7\ISCC.exe', + + [Parameter(Mandatory = $false)] + [string]$Version +) + +$ErrorActionPreference = 'Stop' + +function Get-RepoRoot { + return (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path +} + +function Get-CMakeProjectVersion { + param([string]$CachePath) + + if (-not (Test-Path $CachePath)) { + throw "Missing CMake cache: $CachePath" + } + $match = Select-String -Path $CachePath -Pattern '^CMAKE_PROJECT_VERSION:STATIC=(.+)$' | Select-Object -First 1 + if (-not $match) { + throw "Unable to determine project version from $CachePath" + } + return $match.Matches[0].Groups[1].Value.Trim() +} + +function Ensure-EmptyDirectory { + param([string]$Path) + + if (Test-Path $Path) { + Remove-Item -LiteralPath $Path -Recurse -Force + } + New-Item -ItemType Directory -Path $Path -Force | Out-Null +} + +function Copy-Tree { + param( + [string]$Source, + [string]$Destination + ) + + if (-not (Test-Path $Source)) { + throw "Missing source path: $Source" + } + New-Item -ItemType Directory -Path $Destination -Force | Out-Null + Copy-Item -Path (Join-Path $Source '*') -Destination $Destination -Recurse -Force +} + +function Copy-FileIfPresent { + param( + [string]$Source, + [string]$DestinationDirectory + ) + + if (Test-Path $Source) { + New-Item -ItemType Directory -Path $DestinationDirectory -Force | Out-Null + Copy-Item -LiteralPath $Source -Destination $DestinationDirectory -Force + } +} + +function Invoke-Checked { + param( + [string]$FilePath, + [string[]]$Arguments, + [string]$WorkingDirectory + ) + + Push-Location $WorkingDirectory + try { + & $FilePath @Arguments + if ($LASTEXITCODE -ne 0) { + throw "Command failed ($LASTEXITCODE): $FilePath $($Arguments -join ' ')" + } + } finally { + Pop-Location + } +} + +$RepoRoot = Get-RepoRoot +if (-not (Test-Path $BuildDir)) { + throw "Build directory not found: $BuildDir" +} + +if (-not [System.IO.Path]::IsPathRooted($BuildDir)) { + $BuildDir = Join-Path $RepoRoot $BuildDir +} +if (-not [System.IO.Path]::IsPathRooted($StageDir)) { + $StageDir = Join-Path $RepoRoot $StageDir +} +if (-not [System.IO.Path]::IsPathRooted($OutputDir)) { + $OutputDir = Join-Path $RepoRoot $OutputDir +} + +$BuildDir = [System.IO.Path]::GetFullPath($BuildDir) +$StageDir = [System.IO.Path]::GetFullPath($StageDir) +$OutputDir = [System.IO.Path]::GetFullPath($OutputDir) + +if (-not $Version) { + $Version = Get-CMakeProjectVersion -CachePath (Join-Path $BuildDir 'CMakeCache.txt') +} + +if (-not (Test-Path $IsccPath)) { + throw "Inno Setup compiler not found: $IsccPath" +} + +Ensure-EmptyDirectory -Path $StageDir +New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null + +Invoke-Checked -FilePath 'cmake' -Arguments @('--install', $BuildDir, '--prefix', $StageDir, '--config', 'Release') -WorkingDirectory $RepoRoot + +Copy-FileIfPresent -Source (Join-Path $RepoRoot 'README.md') -DestinationDirectory $StageDir +Copy-FileIfPresent -Source (Join-Path $RepoRoot 'COPYING') -DestinationDirectory $StageDir +Copy-FileIfPresent -Source (Join-Path $RepoRoot 'docs\windows-installer.md') -DestinationDirectory (Join-Path $StageDir 'docs') +Copy-Tree -Source (Join-Path $RepoRoot 'LICENSES') -Destination (Join-Path $StageDir 'licenses') +Copy-FileIfPresent -Source (Join-Path $StageDir 'share\man\man1\hasciicam.1') -DestinationDirectory (Join-Path $StageDir 'docs') + +Get-ChildItem -Path $BuildDir -Filter '*.dll' -File | ForEach-Object { + if ($_.Name -ne 'hasciicam_virtual_camera_source.dll') { + Copy-Item -LiteralPath $_.FullName -Destination (Join-Path $StageDir 'bin') -Force + } +} + +$requiredStageFiles = @( + (Join-Path $StageDir 'bin\hasciicam.exe'), + (Join-Path $StageDir 'bin\hasciicam_vcamctl.exe'), + (Join-Path $StageDir 'bin\hasciicam_virtual_camera_source.dll'), + (Join-Path $StageDir 'README.md'), + (Join-Path $StageDir 'COPYING'), + (Join-Path $StageDir 'docs\hasciicam.1'), + (Join-Path $StageDir 'docs\windows-installer.md'), + (Join-Path $StageDir 'licenses') +) + +foreach ($requiredPath in $requiredStageFiles) { + if (-not (Test-Path $requiredPath)) { + throw "Required staging path missing: $requiredPath" + } +} + +$HasSDL2Dll = Test-Path (Join-Path $StageDir 'bin\SDL2.dll') + +$issPath = Join-Path $PSScriptRoot 'hasciicam.iss' +$defines = @( + "/DMyAppVersion=$Version", + "/DMyBuildHome=$StageDir", + "/DMyOutputDir=$OutputDir" +) +if ($HasSDL2Dll) { + $defines += '/DHasSDL2Dll=1' +} else { + $defines += '/DHasSDL2Dll=0' +} + +Invoke-Checked -FilePath $IsccPath -Arguments ($defines + @($issPath)) -WorkingDirectory $RepoRoot + +Write-Host "Installer built in $OutputDir" diff --git a/packaging/windows/hasciicam.iss b/packaging/windows/hasciicam.iss new file mode 100644 index 0000000..356ccb8 --- /dev/null +++ b/packaging/windows/hasciicam.iss @@ -0,0 +1,304 @@ +#define MyAppName "HasciiCam" +#define MyAppPublisher "Dyne.org foundation" +#define MyAppURL "https://dyne.org/" + +#ifndef MyAppVersion +#error MyAppVersion is required +#endif + +#ifndef MyBuildHome +#error MyBuildHome is required +#endif + +#ifndef MyOutputDir +#error MyOutputDir is required +#endif + +#ifndef HasSDL2Dll +#define HasSDL2Dll 0 +#endif + +[Setup] +AppId={{7C1D6B7A-0F0A-4A18-A7E5-6F2B8D2F6F3D}} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +AppVerName={#MyAppName} {#MyAppVersion} +AppPublisher={#MyAppPublisher} +AppPublisherURL={#MyAppURL} +AppSupportURL={#MyAppURL} +AppUpdatesURL={#MyAppURL} +DefaultDirName={autopf}\{#MyAppName} +DefaultGroupName={#MyAppName} +UninstallDisplayIcon={app}\hasciicam.exe +ArchitecturesAllowed=x64compatible +ArchitecturesInstallIn64BitMode=x64compatible +PrivilegesRequired=admin +DisableWelcomePage=yes +DisableProgramGroupPage=yes +DisableReadyPage=no +DisableFinishedPage=no +DisableDirPage=auto +UsePreviousAppDir=yes +UsePreviousTasks=yes +WizardStyle=modern +LicenseFile={#MyBuildHome}\COPYING +InfoBeforeFile={#MyBuildHome}\docs\windows-installer.md +OutputBaseFilename=hasciicam-{#MyAppVersion}-windows-x64-setup +OutputDir={#MyOutputDir} +SolidCompression=yes + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Types] +Name: "full"; Description: "Full" +Name: "application"; Description: "Application only" + +[Components] +Name: "main"; Description: "HasciiCam program, documentation, and shared runtime files"; Types: full application; Flags: fixed +Name: "virtualcamera"; Description: "Install and register the Windows virtual camera (Windows 11 build 22000+, administrator rights)"; Types: full + +[Tasks] +Name: "addpath"; Description: "Add HasciiCam to PATH" + +[Files] +Source: "{#MyBuildHome}\bin\hasciicam.exe"; DestDir: "{app}"; Components: main; Flags: ignoreversion +Source: "{#MyBuildHome}\bin\hasciicam_vcamctl.exe"; DestDir: "{app}"; Components: virtualcamera; Flags: ignoreversion +Source: "{#MyBuildHome}\bin\hasciicam_virtual_camera_source.dll"; DestDir: "{app}"; Components: virtualcamera; Flags: ignoreversion +#if HasSDL2Dll +Source: "{#MyBuildHome}\bin\SDL2.dll"; DestDir: "{app}"; Components: main; Flags: ignoreversion +#endif +Source: "{#MyBuildHome}\README.md"; DestDir: "{app}"; Components: main; Flags: ignoreversion +Source: "{#MyBuildHome}\docs\hasciicam.1"; DestDir: "{app}\docs"; Components: main; Flags: ignoreversion +Source: "{#MyBuildHome}\docs\windows-installer.md"; DestDir: "{app}\docs"; Components: main; Flags: ignoreversion +Source: "{#MyBuildHome}\COPYING"; DestDir: "{app}\licenses"; Components: main; Flags: ignoreversion +Source: "{#MyBuildHome}\licenses\*"; DestDir: "{app}\licenses"; Components: main; Flags: ignoreversion recursesubdirs createallsubdirs + +[Run] +Filename: "{app}\hasciicam.exe"; Parameters: "-h"; Description: "Show HasciiCam help"; Flags: postinstall nowait skipifsilent unchecked + +[Code] +const + EnvironmentKey = 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment'; + MyHWND_BROADCAST = $FFFF; + MyWM_SETTINGCHANGE = $001A; + MySMTO_ABORTIFHUNG = $0002; + MySW_HIDE = 0; + +function SendMessageTimeout(hWnd: Integer; + Msg: Integer; + wParam: Integer; + lParam: string; + fuFlags: Integer; + uTimeout: Integer; + var lpdwResult: Integer): Integer; + external 'SendMessageTimeoutW@user32.dll stdcall'; + +function RunHelper(const Params: string; var ResultCode: Integer): Boolean; +begin + Result := Exec(ExpandConstant('{app}\hasciicam_vcamctl.exe'), + Params, + ExpandConstant('{app}'), + MySW_HIDE, + ewWaitUntilTerminated, + ResultCode); +end; + +function NormalizePathEntry(const Entry: string): string; +var + Value: string; +begin + Value := Trim(Entry); + if (Length(Value) >= 2) and (Value[1] = '"') and (Value[Length(Value)] = '"') then + begin + Delete(Value, Length(Value), 1); + Delete(Value, 1, 1); + Value := Trim(Value); + end; + while (Length(Value) > 3) and ((Value[Length(Value)] = '\') or (Value[Length(Value)] = '/')) do + Delete(Value, Length(Value), 1); + Result := Lowercase(Value); +end; + +function PathContainsEntry(const PathValue, Entry: string): Boolean; +var + Remaining: string; + Item: string; + Delim: Integer; + Wanted: string; +begin + Result := False; + Wanted := NormalizePathEntry(Entry); + Remaining := PathValue; + while Remaining <> '' do + begin + Delim := Pos(';', Remaining); + if Delim = 0 then + begin + Item := Remaining; + Remaining := ''; + end + else + begin + Item := Copy(Remaining, 1, Delim - 1); + Delete(Remaining, 1, Delim); + end; + if CompareText(NormalizePathEntry(Item), Wanted) = 0 then + begin + Result := True; + Exit; + end; + end; +end; + +function RebuildPathWithoutEntry(const PathValue, Entry: string; var NewPath: string): Boolean; +var + Remaining: string; + Item: string; + Delim: Integer; + Wanted: string; + Normalized: string; +begin + Result := False; + NewPath := ''; + Wanted := NormalizePathEntry(Entry); + Remaining := PathValue; + while Remaining <> '' do + begin + Delim := Pos(';', Remaining); + if Delim = 0 then + begin + Item := Remaining; + Remaining := ''; + end + else + begin + Item := Copy(Remaining, 1, Delim - 1); + Delete(Remaining, 1, Delim); + end; + Normalized := NormalizePathEntry(Item); + if (Normalized <> '') and (CompareText(Normalized, Wanted) <> 0) then + begin + if NewPath <> '' then + NewPath := NewPath + ';'; + NewPath := NewPath + Trim(Item); + end; + end; + Result := True; +end; + +procedure BroadcastEnvironmentChange; +var + ResultCode: Integer; +begin + SendMessageTimeout(MyHWND_BROADCAST, + MyWM_SETTINGCHANGE, + 0, + 'Environment', + MySMTO_ABORTIFHUNG, + 5000, + ResultCode); +end; + +function UpdateMachinePath(AddEntry: Boolean): Boolean; +var + CurrentPath: string; + UpdatedPath: string; + AppPath: string; +begin + Result := False; + AppPath := ExpandConstant('{app}'); + if not RegQueryStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', CurrentPath) then + CurrentPath := ''; + if AddEntry then + begin + if PathContainsEntry(CurrentPath, AppPath) then + begin + Result := True; + Exit; + end; + if CurrentPath = '' then + UpdatedPath := AppPath + else + UpdatedPath := CurrentPath + ';' + AppPath; + end + else + begin + if not RebuildPathWithoutEntry(CurrentPath, AppPath, UpdatedPath) then + Exit; + if CompareText(CurrentPath, UpdatedPath) = 0 then + begin + Result := True; + Exit; + end; + end; + if not RegWriteExpandStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', UpdatedPath) then + Exit; + BroadcastEnvironmentChange; + Result := True; +end; + +function RemoveInstalledVirtualCamera: Boolean; +var + ResultCode: Integer; +begin + Result := True; + if not FileExists(ExpandConstant('{app}\hasciicam_vcamctl.exe')) then + Exit; + if not RunHelper('remove --root "{app}"', ResultCode) then + begin + Result := False; + Exit; + end; + if ResultCode <> 0 then + Result := False; +end; + +function InstallVirtualCamera: Boolean; +var + ResultCode: Integer; +begin + Result := True; + if not WizardIsComponentSelected('virtualcamera') then + Exit; + if not RunHelper('install --source "{app}\hasciicam_virtual_camera_source.dll" --root "{app}"', + ResultCode) then + begin + Result := False; + Exit; + end; + if ResultCode <> 0 then + Result := False; +end; + +function PrepareToInstall(var NeedsRestart: Boolean): String; +begin + Result := ''; + if not RemoveInstalledVirtualCamera then + Result := 'Unable to remove the existing HasciiCam virtual camera before upgrading. Stop Windows Frame Server if it is holding the DLL and retry.'; +end; + +procedure CurStepChanged(CurStep: TSetupStep); +begin + if CurStep = ssPostInstall then + begin + if not InstallVirtualCamera then + RaiseException('Unable to register the HasciiCam virtual camera. Stop Windows Frame Server if it is holding the DLL and retry.'); + if WizardIsTaskSelected('addpath') then + begin + if not UpdateMachinePath(True) then + MsgBox('HasciiCam was installed, but PATH could not be updated automatically.', mbInformation, MB_OK); + end; + end; +end; + +procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); +begin + if CurUninstallStep = usUninstall then + begin + if not RemoveInstalledVirtualCamera then + RaiseException('Unable to unregister the HasciiCam virtual camera. Stop Windows Frame Server if it is holding the DLL and retry.'); + if not UpdateMachinePath(False) then + MsgBox('HasciiCam was removed, but PATH could not be updated automatically.', mbInformation, MB_OK); + end; +end; diff --git a/src/virtual_camera/windows/install/hasciicam_virtual_camera_install.c b/src/virtual_camera/windows/install/hasciicam_virtual_camera_install.c index a8478ff..099b164 100644 --- a/src/virtual_camera/windows/install/hasciicam_virtual_camera_install.c +++ b/src/virtual_camera/windows/install/hasciicam_virtual_camera_install.c @@ -182,19 +182,21 @@ int hasciicam_virtual_camera_install_copy_dll(const wchar_t *source_path, set_error(err, err_size, "unable to create install directory"); return 0; } - if (!CopyFileW(source_path, dest_path, FALSE)) { - DWORD error = GetLastError(); - if (error == ERROR_SHARING_VIOLATION || error == ERROR_ACCESS_DENIED) { - snprintf(err, - err_size, - "unable to replace source DLL because Windows Frame Server still has it loaded " - "(error=%lu); stop the FrameServer service and retry", - (unsigned long)error); - } else { - snprintf(err, err_size, "unable to copy DLL into install root (error=%lu)", - (unsigned long)error); + if (_wcsicmp(source_path, dest_path) != 0) { + if (!CopyFileW(source_path, dest_path, FALSE)) { + DWORD error = GetLastError(); + if (error == ERROR_SHARING_VIOLATION || error == ERROR_ACCESS_DENIED) { + snprintf(err, + err_size, + "unable to replace source DLL because Windows Frame Server still has it loaded " + "(error=%lu); stop the FrameServer service and retry", + (unsigned long)error); + } else { + snprintf(err, err_size, "unable to copy DLL into install root (error=%lu)", + (unsigned long)error); + } + return 0; } - return 0; } if (!join_path(log_path, sizeof(log_path) / sizeof(log_path[0]), diff --git a/tests/test_virtual_camera_install.c b/tests/test_virtual_camera_install.c index b7f484e..738c754 100644 --- a/tests/test_virtual_camera_install.c +++ b/tests/test_virtual_camera_install.c @@ -77,8 +77,10 @@ int main(void) { wchar_t module_dir[MAX_PATH]; wchar_t source_path[MAX_PATH]; wchar_t temp_root[MAX_PATH]; + wchar_t same_root[MAX_PATH]; wchar_t dest_dir[MAX_PATH]; wchar_t dest_path[MAX_PATH]; + wchar_t same_source_path[MAX_PATH]; wchar_t removable_path[MAX_PATH]; wchar_t *slash; DWORD length; @@ -138,6 +140,44 @@ int main(void) { if (!copied) fprintf(stderr, "copy helper error: %s\n", err); expect_true(copied, "copy helper should set permissions on a writable temp root"); + expect_true(swprintf(same_root, + sizeof(same_root) / sizeof(same_root[0]), + L"%lsHasciiCamSame_%lu", + temp_root, + (unsigned long)GetCurrentProcessId()) >= 0, + "same-path temp root formatting should succeed"); + if (swprintf(same_root, + sizeof(same_root) / sizeof(same_root[0]), + L"%lsHasciiCamSame_%lu", + temp_root, + (unsigned long)GetCurrentProcessId()) >= 0) { + expect_true(CreateDirectoryW(same_root, NULL) || GetLastError() == ERROR_ALREADY_EXISTS, + "same-path temp root should be creatable"); + expect_true(swprintf(same_source_path, + sizeof(same_source_path) / sizeof(same_source_path[0]), + L"%ls\\same-source.dll", + same_root) >= 0, + "same-path source DLL path formatting should succeed"); + if (swprintf(same_source_path, + sizeof(same_source_path) / sizeof(same_source_path[0]), + L"%ls\\same-source.dll", + same_root) >= 0) { + int copied_same_source = CopyFileW(dll_path, same_source_path, FALSE); + int same_path_install = 0; + if (!copied_same_source) + fprintf(stderr, "same-path source copy error: %lu\n", (unsigned long)GetLastError()); + expect_true(copied_same_source, + "same-path helper test should create its own source copy"); + same_path_install = hasciicam_virtual_camera_install_copy_dll(same_source_path, + same_source_path, + err, + sizeof(err)); + if (!same_path_install) + fprintf(stderr, "same-path helper error: %s\n", err); + expect_true(same_path_install, + "copy helper should accept an already-installed source DLL path"); + } + } expect_true(swprintf(removable_path, sizeof(removable_path) / sizeof(removable_path[0]), L"%ls\\removable.dll",