diff --git a/PrinterInstaller/PrinterInstaller.iss b/PrinterInstaller/PrinterInstaller.iss index 106f855..e2a25c9 100644 --- a/PrinterInstaller/PrinterInstaller.iss +++ b/PrinterInstaller/PrinterInstaller.iss @@ -1,10 +1,11 @@ ; Universal Printer Installer for Network Printers ; Installs HP/Xerox network printers from ShopDB via TCP/IP +; Pure Inno Setup Pascal Script - no PowerShell dependency [Setup] AppId={{7B9C3E41-5F2A-4D8B-9E1C-8A6D4F3E2B1A}} AppName=WJDT GE Aerospace Printer Installer -AppVersion=1.0 +AppVersion=2.0 AppPublisher=WJDT AppPublisherURL=http://tsgwp00524.logon.ds.ge.com AppSupportURL=http://tsgwp00524.logon.ds.ge.com @@ -43,203 +44,453 @@ Source: "drivers\xerox_x64\*"; DestDir: "{tmp}\xerox_drivers_x64"; Flags: ignore ; Xerox Universal Print Driver x32 Source: "drivers\xerox_x32\*"; DestDir: "{tmp}\xerox_drivers_x32"; Flags: ignoreversion recursesubdirs; Check: not Is64BitInstallMode +; HP DesignJet T1700dr PS3 x64 +Source: "drivers\hp_designjet_x64\*"; DestDir: "{tmp}\hp_designjet_x64"; Flags: ignoreversion recursesubdirs; Check: Is64BitInstallMode + +; HP DesignJet T1700dr PS3 x32 +Source: "drivers\hp_designjet_x32\*"; DestDir: "{tmp}\hp_designjet_x32"; Flags: ignoreversion recursesubdirs; Check: not Is64BitInstallMode + [Code] var PrinterSelectionPage: TInputOptionWizardPage; PrinterDataArray: array of record PrinterName: String; FQDN: String; - IPAddress: String; Vendor: String; Model: String; IsInstalled: Boolean; end; + InstallLog: TStringList; -// Query database for printers via web API +// ─── Logging ──────────────────────────────────────────────────────────────── + +procedure AddLog(const Msg: String); +begin + if InstallLog <> nil then + InstallLog.Add(Msg); +end; + +// ─── JSON helpers ──────────────────────────────────────────────────────────── + +// Extract a single field value from a flat JSON object string. +// Returns the raw string value (unquoted), or the number/bool literal, or ''. +function JsonGetString(const JsonObj, Key: String): String; +var + P, Q: Integer; +begin + Result := ''; + P := Pos('"' + Key + '"', JsonObj); + if P = 0 then Exit; + P := P + Length('"' + Key + '"'); + + // Skip whitespace and colon + while (P <= Length(JsonObj)) and + ((JsonObj[P] = ' ') or (JsonObj[P] = ':') or (JsonObj[P] = #9)) do + P := P + 1; + if P > Length(JsonObj) then Exit; + + if JsonObj[P] = '"' then + begin + // Quoted string value + P := P + 1; + Q := P; + while (Q <= Length(JsonObj)) and (JsonObj[Q] <> '"') do + Q := Q + 1; + Result := Copy(JsonObj, P, Q - P); + end + else if Copy(JsonObj, P, 4) = 'null' then + Result := '' + else if Copy(JsonObj, P, 4) = 'true' then + Result := 'true' + else if Copy(JsonObj, P, 5) = 'false' then + Result := 'false' + else + begin + // Numeric value - read until delimiter + Q := P; + while (Q <= Length(JsonObj)) and + (JsonObj[Q] <> ',') and (JsonObj[Q] <> '}') and + (JsonObj[Q] <> ' ') and (JsonObj[Q] <> #13) and (JsonObj[Q] <> #10) do + Q := Q + 1; + Result := Trim(Copy(JsonObj, P, Q - P)); + end; +end; + +// ─── HTTP ──────────────────────────────────────────────────────────────────── + +// Perform an HTTP GET using the built-in Windows WinHttp COM object. +// Sends Windows credentials automatically (NTLM/Kerberos for intranet sites). +function HttpGet(const Url: String; var Response: String): Boolean; +var + WinHttp: Variant; +begin + Result := False; + Response := ''; + try + WinHttp := CreateOleObject('WinHttp.WinHttpRequest.5.1'); + WinHttp.Open('GET', Url, False); + WinHttp.SetAutoLogonPolicy(0); // Always send Windows credentials + WinHttp.SetRequestHeader('Accept', 'application/json'); + WinHttp.Send(''); + if WinHttp.Status = 200 then + begin + Response := WinHttp.ResponseText; + Result := True; + end; + except + Result := False; + end; +end; + +// ─── Printer query ─────────────────────────────────────────────────────────── + +// Query ShopDB API and populate PrinterDataArray with active HP/Xerox printers. function QueryPrinters(): Boolean; var - ResultCode: Integer; - TempScriptPath, TempOutputPath: String; - PSScript: String; - Output: AnsiString; - Lines: TStringList; - I: Integer; - Fields: TStringList; + Response: String; + I, Depth, ObjStart, Count: Integer; + ObjStr, Name, Vendor, Model, Fqdn, Ip, Active, Address: String; begin Result := False; + SetArrayLength(PrinterDataArray, 0); - // Build PowerShell script to query database API - PSScript := - 'try {' + #13#10 + - ' $url = "https://tsgwp00525.rd.ds.ge.com/shopdb/api_printers.asp"' + #13#10 + - ' # Allow TLS 1.2 for HTTPS connections' + #13#10 + - ' [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12' + #13#10 + - ' $response = Invoke-WebRequest -Uri $url -Method GET -UseBasicParsing -UseDefaultCredentials -ErrorAction Stop' + #13#10 + - ' $printers = $response.Content | ConvertFrom-Json' + #13#10 + - ' foreach ($printer in $printers) {' + #13#10 + - ' if (($printer.isactive -eq 1 -or $printer.isactive -eq $true) -and ($printer.vendor -eq "HP" -or $printer.vendor -eq "Xerox" -or $printer.vendor -eq "HID")) {' + #13#10 + - ' $name = $printer.printerwindowsname' + #13#10 + - ' $vendor = $printer.vendor' + #13#10 + - ' $model = $printer.modelnumber' + #13#10 + - ' # Use FQDN or IP if available, otherwise default to USB for card printers' + #13#10 + - ' if ($printer.fqdn -and $printer.fqdn -ne "" -and $printer.fqdn -ne "USB") {' + #13#10 + - ' $address = $printer.fqdn' + #13#10 + - ' } elseif ($printer.ipaddress -and $printer.ipaddress -ne "") {' + #13#10 + - ' $address = $printer.ipaddress' + #13#10 + - ' } else {' + #13#10 + - ' $address = "USB"' + #13#10 + - ' }' + #13#10 + - ' if ($address -and $address -ne "") {' + #13#10 + - ' Write-Output "$name|$address|$vendor|$model"' + #13#10 + - ' }' + #13#10 + - ' }' + #13#10 + - ' }' + #13#10 + - '} catch {' + #13#10 + - ' Write-Error "Failed to query printers: $_"' + #13#10 + - ' exit 1' + #13#10 + - '}'; + if not HttpGet('https://tsgwp00525.wjs.geaerospace.net/shopdb/api_printers.asp', Response) then + Exit; + if Response = '' then Exit; - TempScriptPath := ExpandConstant('{tmp}\query_printers.ps1'); - TempOutputPath := ExpandConstant('{tmp}\printers_output.txt'); - - SaveStringToFile(TempScriptPath, PSScript, False); - - if not Exec('powershell.exe', - '-NoProfile -ExecutionPolicy Bypass -Command "& ''' + TempScriptPath + ''' | Out-File -FilePath ''' + TempOutputPath + ''' -Encoding ASCII"', - '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then + // Count top-level JSON objects to size the array + Count := 0; + Depth := 0; + for I := 1 to Length(Response) do begin - DeleteFile(TempScriptPath); - Exit; + if Response[I] = '{' then + begin + if Depth = 0 then Count := Count + 1; + Depth := Depth + 1; + end + else if Response[I] = '}' then + Depth := Depth - 1; end; + if Count = 0 then Exit; + SetArrayLength(PrinterDataArray, Count); - DeleteFile(TempScriptPath); - - if not FileExists(TempOutputPath) then - Exit; - - if not LoadStringFromFile(TempOutputPath, Output) then + // Parse each JSON object and extract printer fields + Count := 0; + Depth := 0; + ObjStart := 0; + for I := 1 to Length(Response) do begin - DeleteFile(TempOutputPath); - Exit; - end; - - DeleteFile(TempOutputPath); - - Lines := TStringList.Create; - try - Lines.Text := Output; - - // Count valid lines - I := 0; - while I < Lines.Count do + if Response[I] = '{' then begin - if (Trim(Lines[I]) = '') or (Pos('|', Lines[I]) = 0) then - Lines.Delete(I) - else - I := I + 1; - end; - - if Lines.Count = 0 then - Exit; - - SetArrayLength(PrinterDataArray, Lines.Count); - - for I := 0 to Lines.Count - 1 do + if Depth = 0 then ObjStart := I; + Depth := Depth + 1; + end + else if Response[I] = '}' then begin - Fields := TStringList.Create; - try - Fields.Delimiter := '|'; - Fields.StrictDelimiter := True; - Fields.DelimitedText := Trim(Lines[I]); + Depth := Depth - 1; + if Depth = 0 then + begin + ObjStr := Copy(Response, ObjStart, I - ObjStart + 1); + Active := JsonGetString(ObjStr, 'isactive'); + Vendor := JsonGetString(ObjStr, 'vendor'); - if Fields.Count >= 4 then + if ((Active = '1') or (Active = 'true')) and + ((Vendor = 'HP') or (Vendor = 'Xerox')) then begin - PrinterDataArray[I].PrinterName := Trim(Fields[0]); - PrinterDataArray[I].FQDN := Trim(Fields[1]); - PrinterDataArray[I].IPAddress := Trim(Fields[1]); - PrinterDataArray[I].Vendor := Trim(Fields[2]); - PrinterDataArray[I].Model := Trim(Fields[3]); - Result := True; + Name := JsonGetString(ObjStr, 'printerwindowsname'); + Model := JsonGetString(ObjStr, 'modelnumber'); + Fqdn := JsonGetString(ObjStr, 'fqdn'); + Ip := JsonGetString(ObjStr, 'ipaddress'); + + // Prefer FQDN over IP address + if (Fqdn <> '') and (Fqdn <> 'USB') then + Address := Fqdn + else if Ip <> '' then + Address := Ip + else + Address := ''; + + if (Name <> '') and (Address <> '') then + begin + PrinterDataArray[Count].PrinterName := Name; + PrinterDataArray[Count].FQDN := Address; + PrinterDataArray[Count].Vendor := Vendor; + PrinterDataArray[Count].Model := Model; + PrinterDataArray[Count].IsInstalled := False; + Count := Count + 1; + Result := True; + end; end; - finally - Fields.Free; end; end; - finally - Lines.Free; end; + + SetArrayLength(PrinterDataArray, Count); end; -// Check which printers are already installed -function CheckInstalledPrinters(): Boolean; +// ─── Registry-based state checks ───────────────────────────────────────────── + +// True if the named printer exists in the Windows print subsystem registry. +function IsPrinterInstalled(const PrinterName: String): Boolean; +begin + Result := RegKeyExists(HKEY_LOCAL_MACHINE, + 'SYSTEM\CurrentControlSet\Control\Print\Printers\' + PrinterName); +end; + +// True if the named print driver is registered in the Windows print environment. +function IsDriverInstalled(const DriverName: String; IsX64: Boolean): Boolean; +var + RegPath: String; +begin + if IsX64 then + RegPath := 'SYSTEM\CurrentControlSet\Control\Print\Environments\Windows x64\Drivers\Version-3\' + else + RegPath := 'SYSTEM\CurrentControlSet\Control\Print\Environments\Windows NT x86\Drivers\Version-3\'; + Result := RegKeyExists(HKEY_LOCAL_MACHINE, RegPath + DriverName); +end; + +// True if a Standard TCP/IP printer port with the given name exists. +function IsPortInstalled(const PortName: String): Boolean; +begin + Result := RegKeyExists(HKEY_LOCAL_MACHINE, + 'SYSTEM\CurrentControlSet\Control\Print\Monitors\Standard TCP/IP Port\Ports\' + PortName); +end; + +// Update IsInstalled flag for every entry in PrinterDataArray. +procedure CheckInstalledPrinters(); +var + I: Integer; +begin + for I := 0 to GetArrayLength(PrinterDataArray) - 1 do + PrinterDataArray[I].IsInstalled := IsPrinterInstalled(PrinterDataArray[I].PrinterName); +end; + +// ─── Driver staging ────────────────────────────────────────────────────────── + +// Stage a driver package into the Windows Driver Store using pnputil. +// Returns True on success (exit code 0, 259 = no device, or 3010 = reboot needed). +function StageDriver(const InfPath: String): Boolean; var ResultCode: Integer; - TempScriptPath, TempOutputPath: String; - PSScript: String; - Output: AnsiString; - Lines: TStringList; - I, J: Integer; begin Result := False; + if not FileExists(InfPath) then Exit; - // Build PowerShell script to get installed printers - PSScript := - 'try {' + #13#10 + - ' Get-Printer | Select-Object -ExpandProperty Name | ForEach-Object { Write-Output $_ }' + #13#10 + - '} catch { exit 1 }'; - - TempScriptPath := ExpandConstant('{tmp}\check_installed.ps1'); - TempOutputPath := ExpandConstant('{tmp}\installed_output.txt'); - - SaveStringToFile(TempScriptPath, PSScript, False); - - if not Exec('powershell.exe', - '-NoProfile -ExecutionPolicy Bypass -Command "& ''' + TempScriptPath + ''' | Out-File -FilePath ''' + TempOutputPath + ''' -Encoding ASCII"', - '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then + // Windows 10+ syntax + if Exec(ExpandConstant('{sys}\pnputil.exe'), + '/add-driver "' + InfPath + '" /install', + '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then begin - DeleteFile(TempScriptPath); - Exit; - end; - - DeleteFile(TempScriptPath); - - if not FileExists(TempOutputPath) then - Exit; - - if not LoadStringFromFile(TempOutputPath, Output) then - begin - DeleteFile(TempOutputPath); - Exit; - end; - - DeleteFile(TempOutputPath); - - Lines := TStringList.Create; - try - Lines.Text := Output; - - // Check each printer in database against installed printers - for I := 0 to GetArrayLength(PrinterDataArray) - 1 do + if (ResultCode = 0) or (ResultCode = 259) or (ResultCode = 3010) then begin - PrinterDataArray[I].IsInstalled := False; - - for J := 0 to Lines.Count - 1 do - begin - if Trim(Lines[J]) = PrinterDataArray[I].PrinterName then - begin - PrinterDataArray[I].IsInstalled := True; - Break; - end; - end; + Result := True; + Exit; end; + end; + // Fallback: Windows 7 legacy syntax + if Exec(ExpandConstant('{sys}\pnputil.exe'), + '-i -a "' + InfPath + '"', + '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then + Result := (ResultCode = 0) or (ResultCode = 259) or (ResultCode = 3010); +end; + +// ─── Spooler management ────────────────────────────────────────────────────── + +// Stop and restart the Windows Print Spooler so it picks up newly staged +// drivers and newly created port registry entries. +procedure RestartSpooler(); +var + ResultCode: Integer; +begin + Exec(ExpandConstant('{sys}\net.exe'), 'stop spooler', + '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + Sleep(1500); + Exec(ExpandConstant('{sys}\net.exe'), 'start spooler', + '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + Sleep(2000); +end; + +// ─── Driver registration ───────────────────────────────────────────────────── + +// Register a staged driver with the print subsystem via printui.dll. +// Call after pnputil has staged the driver and the spooler has been restarted. +procedure InstallDriverViaUI(const DriverName, InfPath: String); +var + ResultCode: Integer; +begin + Exec(ExpandConstant('{sys}\rundll32.exe'), + 'printui.dll,PrintUIEntry /ia /m "' + DriverName + '" /f "' + InfPath + '"', + '', SW_HIDE, ewWaitUntilTerminated, ResultCode); +end; + +// ─── Port management ───────────────────────────────────────────────────────── + +// Create (or overwrite) a Standard TCP/IP printer port in the registry. +// Always writes all values so stale/corrupt entries from previous installs are +// replaced with a known-good format. The print spooler must be restarted after +// this call for the changes to take effect. +function CreateTCPIPPort(const PortAddress: String): Boolean; +var + RegBase: String; +begin + RegBase := 'SYSTEM\CurrentControlSet\Control\Print\Monitors\Standard TCP/IP Port\Ports\IP_' + PortAddress; + Result := False; + if not RegWriteStringValue(HKEY_LOCAL_MACHINE, RegBase, 'HostName', PortAddress) then Exit; + if not RegWriteStringValue(HKEY_LOCAL_MACHINE, RegBase, 'IPAddress', PortAddress) then Exit; + if not RegWriteDWordValue (HKEY_LOCAL_MACHINE, RegBase, 'PortNumber', 9100) then Exit; + if not RegWriteDWordValue (HKEY_LOCAL_MACHINE, RegBase, 'Protocol', 1) then Exit; + if not RegWriteDWordValue (HKEY_LOCAL_MACHINE, RegBase, 'Version', 1) then Exit; + if not RegWriteStringValue(HKEY_LOCAL_MACHINE, RegBase, 'MonitorName', 'Standard TCP/IP Port') then Exit; + if not RegWriteStringValue(HKEY_LOCAL_MACHINE, RegBase, 'Description', 'Generic Network Card') then Exit; + if not RegWriteDWordValue (HKEY_LOCAL_MACHINE, RegBase, 'SNMP', 0) then Exit; + if not RegWriteDWordValue (HKEY_LOCAL_MACHINE, RegBase, 'Queue', 0) then Exit; + Result := True; +end; + +// ─── Port and printer management via WMI ───────────────────────────────────── + +// Create (or re-register) a Standard TCP/IP printer port. +// Always attempts WMI registration with the live spooler — a port may exist in +// the registry but be absent from the spooler's live list (stale/orphaned entry), +// in which case WMI creation will succeed and bring it back to life. +// WMI also writes correct registry values, so CreateTCPIPPort is only used as +// a fallback when WMI fails entirely. +function CreatePortViaWMI(const PortAddress: String): Boolean; +var + WMILocator, WMIService, PortClass, NewPort: Variant; + PortName: String; +begin + PortName := 'IP_' + PortAddress; + Result := False; + + // Always try WMI — even if registry key exists, the port may not be in the + // spooler's live port list and WMI registration will fix that. + // WMI also writes its own registry values which the spooler trusts on restart. + try + WMILocator := CreateOleObject('WbemScripting.SWbemLocator'); + WMIService := WMILocator.ConnectServer('.', 'root\cimv2'); + WMIService.Security_.Privileges.AddAsString('SeLoadDriverPrivilege', True); + PortClass := WMIService.Get('Win32_TCPIPPrinterPort'); + NewPort := PortClass.SpawnInstance_(); + NewPort.Name := PortName; + NewPort.HostAddress := PortAddress; + NewPort.PortNumber := 9100; + NewPort.Protocol := 1; + NewPort.Put_(); + Sleep(500); + AddLog(' Port registered with spooler via WMI: ' + PortName); Result := True; - finally - Lines.Free; + except + // May fail if port is already live in the spooler — fall back to registry write. + AddLog(' WMI port note: ' + GetExceptionMessage); + Result := CreateTCPIPPort(PortAddress); + if Result then + AddLog(' Port written to registry (fallback): ' + PortName) + else + AddLog(' WARNING: Failed to write port registry values: ' + PortName); end; end; -// Initialize wizard pages +// Add a locally-managed network printer. +// First attempts WMI Win32_Printer; falls back to printui.dll /if /q. +// InfPath is only used by the printui fallback. +function AddNetworkPrinter(const PrinterName, DriverName, PortName, InfPath: String): Boolean; +var + WMILocator, WMIService, PrinterClass, NewPrinter: Variant; + ResultCode: Integer; +begin + Result := False; + if IsPrinterInstalled(PrinterName) then begin Result := True; Exit; end; + + AddLog(' PrinterName: ' + PrinterName); + AddLog(' DriverName: ' + DriverName); + AddLog(' PortName: ' + PortName); + AddLog(' InfPath: ' + InfPath); + if IsPortInstalled(PortName) then AddLog(' Port: present') else AddLog(' Port: MISSING'); + if IsDriverInstalled(DriverName, Is64BitInstallMode) then AddLog(' Driver: present') else AddLog(' Driver: MISSING'); + if InfPath <> '' then + begin + if FileExists(InfPath) then AddLog(' INF: found') else AddLog(' INF: NOT FOUND'); + end; + + // Attempt 1: WMI Win32_Printer (DeviceID is the writable key, not Name) + AddLog(' Attempting WMI...'); + try + WMILocator := CreateOleObject('WbemScripting.SWbemLocator'); + AddLog(' WMI: locator created'); + WMIService := WMILocator.ConnectServer('.', 'root\cimv2'); + WMIService.Security_.Privileges.AddAsString('SeLoadDriverPrivilege', True); + AddLog(' WMI: connected to root\cimv2'); + PrinterClass := WMIService.Get('Win32_Printer'); + AddLog(' WMI: got Win32_Printer class'); + NewPrinter := PrinterClass.SpawnInstance_(); + AddLog(' WMI: spawned instance'); + NewPrinter.DeviceID := PrinterName; + NewPrinter.DriverName := DriverName; + NewPrinter.PortName := PortName; + NewPrinter.PrintProcessor := 'winprint'; + AddLog(' WMI: properties set, calling Put_()'); + NewPrinter.Put_(); + AddLog(' WMI: Put_() returned'); + Sleep(1000); + Result := IsPrinterInstalled(PrinterName); + if Result then + begin + AddLog(' Printer created via WMI'); + Exit; + end + else + AddLog(' WMI Put_() returned without error but printer not in registry'); + except + AddLog(' WMI exception: ' + GetExceptionMessage); + end; + + // Attempt 2: printui.dll /if (silent install from staged INF) + if InfPath <> '' then + begin + AddLog(' Attempting printui /if...'); + AddLog(' CMD: rundll32 printui.dll,PrintUIEntry /if /b "' + PrinterName + + '" /f "' + InfPath + '" /r "' + PortName + '" /m "' + DriverName + '" /q'); + Exec(ExpandConstant('{sys}\rundll32.exe'), + 'printui.dll,PrintUIEntry /if /b "' + PrinterName + + '" /f "' + InfPath + '" /r "' + PortName + + '" /m "' + DriverName + '" /q', + '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + AddLog(' printui exit code: ' + IntToStr(ResultCode)); + Sleep(2000); + Result := IsPrinterInstalled(PrinterName); + if Result then + AddLog(' Printer created via printui') + else + AddLog(' ERROR: printer not in registry after printui'); + end + else + AddLog(' ERROR: no InfPath available for printui fallback'); +end; + +// Remove a printer via WMI Win32_Printer.Delete_(). +function RemoveNetworkPrinter(const PrinterName: String): Boolean; +var + WMILocator, WMIService, PrinterInst: Variant; +begin + if not IsPrinterInstalled(PrinterName) then begin Result := True; Exit; end; + Result := False; + try + WMILocator := CreateOleObject('WbemScripting.SWbemLocator'); + WMIService := WMILocator.ConnectServer('.', 'root\cimv2'); + PrinterInst := WMIService.Get('Win32_Printer.DeviceID="' + PrinterName + '"'); + PrinterInst.Delete_(); + Sleep(1000); + Result := not IsPrinterInstalled(PrinterName); + except + AddLog(' ERROR: WMI exception removing printer'); + Result := False; + end; +end; + +// ─── Wizard initialisation ─────────────────────────────────────────────────── + procedure InitializeWizard(); var I: Integer; @@ -247,13 +498,12 @@ var AutoSelectPrinter: String; FoundMatch: Boolean; begin - // Query printers from database if not QueryPrinters() then begin MsgBox('Failed to query printers from ShopDB database.' + #13#10#13#10 + 'Please ensure:' + #13#10 + '- You have network connectivity' + #13#10 + - '- The ShopDB server (tsgwp00525.rd.ds.ge.com) is accessible' + #13#10 + + '- The ShopDB server (tsgwp00525.wjs.geaerospace.net) is accessible' + #13#10 + '- You are connected to the GE network' + #13#10 + '- The API endpoint is running', mbError, MB_OK); @@ -264,33 +514,26 @@ begin if GetArrayLength(PrinterDataArray) = 0 then begin MsgBox('No printers found in the database.' + #13#10#13#10 + - 'This installer supports:' + #13#10 + - '- HP and Xerox network printers (TCP/IP)' + #13#10 + - '- HID card printers (USB)', + 'This installer supports HP and Xerox network printers (TCP/IP).' + #13#10#13#10 + + 'Verify the ShopDB server is accessible and has active printers.', mbInformation, MB_OK); WizardForm.Close; Exit; end; - // Check which printers are already installed CheckInstalledPrinters(); - // Check for command-line parameter to auto-select printer(s) - // Usage: PrinterInstaller.exe /PRINTER="PrinterName" - // Or multiple: /PRINTER="Printer1,Printer2,Printer3" + // Optional command-line pre-selection: /PRINTER="Name" or /PRINTER="A,B,C" AutoSelectPrinter := ExpandConstant('{param:PRINTER|}'); FoundMatch := False; - // Create printer selection page PrinterSelectionPage := CreateInputOptionPage(wpWelcome, 'Manage Network Printers', 'Select printers to install or uncheck to remove', 'Checked printers will be installed. Unchecked printers that are currently installed will be removed. ' + 'Already-installed printers are pre-checked.', - False, - False); + False, False); - // Add printers to selection page for I := 0 to GetArrayLength(PrinterDataArray) - 1 do begin if PrinterDataArray[I].PrinterName <> '' then @@ -306,59 +549,42 @@ begin PrinterDataArray[I].FQDN + ')'; PrinterSelectionPage.Add(DisplayText); - // Check if this printer should be auto-selected via command line if AutoSelectPrinter <> '' then begin - // First try exact match against each comma-separated printer name if Pos(',', AutoSelectPrinter) > 0 then begin - // Multiple printers separated by comma - check for exact matches if Pos(',' + PrinterDataArray[I].PrinterName + ',', ',' + AutoSelectPrinter + ',') > 0 then begin PrinterSelectionPage.Values[I] := True; FoundMatch := True; end - else if PrinterDataArray[I].IsInstalled then - PrinterSelectionPage.Values[I] := True else - PrinterSelectionPage.Values[I] := False; + PrinterSelectionPage.Values[I] := PrinterDataArray[I].IsInstalled; end else begin - // Single printer - use partial match (original behavior) if Pos(AutoSelectPrinter, PrinterDataArray[I].PrinterName) > 0 then begin PrinterSelectionPage.Values[I] := True; FoundMatch := True; end - else if PrinterDataArray[I].IsInstalled then - PrinterSelectionPage.Values[I] := True else - PrinterSelectionPage.Values[I] := False; + PrinterSelectionPage.Values[I] := PrinterDataArray[I].IsInstalled; end; end - // No auto-select parameter - pre-check if already installed - else if PrinterDataArray[I].IsInstalled then - begin - PrinterSelectionPage.Values[I] := True; - end else - begin - PrinterSelectionPage.Values[I] := False; - end; + PrinterSelectionPage.Values[I] := PrinterDataArray[I].IsInstalled; end; end; - // If command-line parameter was provided but no match found, show warning if (AutoSelectPrinter <> '') and (not FoundMatch) then - begin MsgBox('Printer "' + AutoSelectPrinter + '" not found in the database.' + #13#10#13#10 + 'Please select a printer from the list manually.', mbInformation, MB_OK); - end; end; -// Validate selection before continuing +// ─── Page validation ───────────────────────────────────────────────────────── + function NextButtonClick(CurPageID: Integer): Boolean; var I: Integer; @@ -369,11 +595,8 @@ begin if CurPageID = PrinterSelectionPage.ID then begin HasChange := False; - - // Check if any printer state has changed for I := 0 to GetArrayLength(PrinterDataArray) - 1 do begin - // If checkbox state differs from installation state, we have a change if PrinterSelectionPage.Values[I] <> PrinterDataArray[I].IsInstalled then begin HasChange := True; @@ -390,405 +613,591 @@ begin end; end; -// Install printers during installation phase (after files are extracted) +// ─── Default printer selection ─────────────────────────────────────────────── + +// Show a dialog listing newly installed printers and return the one the user +// picks as default, or '' if they click Skip. +function PromptDefaultPrinter(const Names: TArrayOfString): String; +var + Form: TSetupForm; + Lbl: TNewStaticText; + List: TNewListBox; + BtnSet, BtnSkip: TNewButton; + I: Integer; +begin + Result := ''; + if GetArrayLength(Names) = 0 then Exit; + + // CreateCustomForm(ClientWidth, ClientHeight, KeepSizeX, KeepSizeY) + Form := CreateCustomForm(400, 210, False, False); + try + Form.Caption := 'Set Default Printer'; + Form.Position := poScreenCenter; + + Lbl := TNewStaticText.Create(Form); + Lbl.Parent := Form; + Lbl.Left := 12; + Lbl.Top := 12; + Lbl.Width := Form.ClientWidth - 24; + Lbl.Caption := 'Select a printer to set as the system default for all users, or click Skip.'; + Lbl.WordWrap := True; + Lbl.AutoSize := True; + + List := TNewListBox.Create(Form); + List.Parent := Form; + List.Left := 12; + List.Top := Lbl.Top + Lbl.Height + 8; + List.Width := Form.ClientWidth - 24; + List.Height := 110; + for I := 0 to GetArrayLength(Names) - 1 do + List.Items.Add(Names[I]); + List.ItemIndex := 0; + + BtnSet := TNewButton.Create(Form); + BtnSet.Parent := Form; + BtnSet.Caption := 'Set Default'; + BtnSet.Width := 90; + BtnSet.Height := 25; + BtnSet.Left := Form.ClientWidth - 195; + BtnSet.Top := Form.ClientHeight - 37; + BtnSet.ModalResult := mrOK; + BtnSet.Default := True; + + BtnSkip := TNewButton.Create(Form); + BtnSkip.Parent := Form; + BtnSkip.Caption := 'Skip'; + BtnSkip.Width := 75; + BtnSkip.Height := 25; + BtnSkip.Left := Form.ClientWidth - 95; + BtnSkip.Top := Form.ClientHeight - 37; + BtnSkip.ModalResult := mrCancel; + BtnSkip.Cancel := True; + + if Form.ShowModal() = mrOK then + if List.ItemIndex >= 0 then + Result := Names[List.ItemIndex]; + finally + Form.Free; + end; +end; + +// Set the Windows default printer for the current user and propagate it to all +// other loaded user hives and .DEFAULT. +// "Let Windows manage my default printer" (LegacyDefaultPrinterMode=0) causes +// printui /y to hang, so we disable it (set to 1) for every profile first. +procedure SetDefaultPrinterAllUsers(const PrinterName: String); +var + ResultCode: Integer; + DeviceValue: String; + SIDNames: TArrayOfString; + I: Integer; + WinKey: String; +begin + WinKey := 'Software\Microsoft\Windows NT\CurrentVersion\Windows'; + + // Disable "Let Windows manage my default printer" for the current user before + // calling printui — otherwise the call hangs when that setting is active. + RegWriteDWordValue(HKEY_CURRENT_USER, WinKey, 'LegacyDefaultPrinterMode', 1); + + // Set default for the current user session + Exec(ExpandConstant('{sys}\rundll32.exe'), + 'printui.dll,PrintUIEntry /y /n "' + PrinterName + '"', + '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + AddLog('Set default printer: ' + PrinterName); + + // Read back the Device string Windows just wrote (format: "Name,winspool,Ne00:") + if not RegQueryStringValue(HKEY_CURRENT_USER, WinKey, 'Device', DeviceValue) then Exit; + + // Propagate both values to .DEFAULT (new user profiles will inherit them) + RegWriteDWordValue(HKEY_USERS, '.DEFAULT\' + WinKey, 'LegacyDefaultPrinterMode', 1); + RegWriteStringValue(HKEY_USERS, '.DEFAULT\' + WinKey, 'Device', DeviceValue); + + // Propagate to every other currently-loaded user hive (SIDs, not _Classes keys) + if RegGetSubkeyNames(HKEY_USERS, '', SIDNames) then + for I := 0 to GetArrayLength(SIDNames) - 1 do + if (Pos('S-1-5-', SIDNames[I]) = 1) and (Pos('_Classes', SIDNames[I]) = 0) then + begin + RegWriteDWordValue(HKEY_USERS, + SIDNames[I] + '\' + WinKey, 'LegacyDefaultPrinterMode', 1); + RegWriteStringValue(HKEY_USERS, + SIDNames[I] + '\' + WinKey, 'Device', DeviceValue); + end; +end; + +// ─── DesignJet model detection ─────────────────────────────────────────────── + +function IsDesignJetModel(const Model: String): Boolean; +var L: String; +begin + L := Lowercase(Model); + Result := (Pos('designjet', L) > 0) or (Pos('t1700', L) > 0); +end; + +// ─── Legacy printer cleanup ────────────────────────────────────────────────── + +// Returns True if the string references the old print server but not the new one. +function IsLegacyServerRef(const S: String): Boolean; +var L: String; +begin + L := Lowercase(Trim(S)); + Result := (Pos('tsgwp00525', L) > 0) and (Pos('.wjs.', L) = 0); +end; + +// Check a printer's registry key for any sign of the old server across the +// values most likely to carry the server name: Port, ShareName, HostName, +// Description, the printer name itself. +function IsPrinterLegacy(const PrinterName: String): Boolean; +var + RegBase, Val: String; +begin + Result := IsLegacyServerRef(PrinterName); + if Result then Exit; + + RegBase := 'SYSTEM\CurrentControlSet\Control\Print\Printers\' + PrinterName; + + Val := ''; RegQueryStringValue(HKEY_LOCAL_MACHINE, RegBase, 'Port', Val); if IsLegacyServerRef(Val) then begin Result := True; Exit; end; + Val := ''; RegQueryStringValue(HKEY_LOCAL_MACHINE, RegBase, 'ShareName', Val); if IsLegacyServerRef(Val) then begin Result := True; Exit; end; + Val := ''; RegQueryStringValue(HKEY_LOCAL_MACHINE, RegBase, 'HostName', Val); if IsLegacyServerRef(Val) then begin Result := True; Exit; end; + Val := ''; RegQueryStringValue(HKEY_LOCAL_MACHINE, RegBase, 'Description', Val); if IsLegacyServerRef(Val) then begin Result := True; Exit; end; +end; + +// Remove any printers whose registry data references the old print servers +// (tsgwp00525 / tsgwp00525.rd.ds.ge.com / tsgwp00525.logon.ds.ge.com), +// excluding the current server (tsgwp00525.wjs.geaerospace.net). +// Deletes registry keys directly with the spooler stopped to avoid WMI hanging +// while trying to contact the unreachable old server. +// Returns the number of printers removed. +function CleanupLegacyPrinters(): Integer; +var + PrinterNames, LegacyFound_ConnKeys: TArrayOfString; + I, J: Integer; + PrinterName, PortValue: String; + LegacyFound: Boolean; + ResultCode: Integer; +begin + Result := 0; + AddLog('Checking for legacy printers from old servers...'); + + if not RegGetSubkeyNames(HKEY_LOCAL_MACHINE, + 'SYSTEM\CurrentControlSet\Control\Print\Printers', PrinterNames) then + begin + AddLog(' Could not enumerate installed printers'); + Exit; + end; + + // First pass: check whether any legacy printers exist before stopping spooler + LegacyFound := False; + for I := 0 to GetArrayLength(PrinterNames) - 1 do + if IsPrinterLegacy(PrinterNames[I]) then + begin + LegacyFound := True; + Break; + end; + + if not LegacyFound then + begin + AddLog(' No legacy printers found'); + Exit; + end; + + // Stop the spooler so registry keys are not locked — avoids any network contact + // with the old (unreachable) print server that would cause WMI to hang. + AddLog(' Legacy printers found — stopping spooler for registry cleanup...'); + Exec(ExpandConstant('{sys}\net.exe'), 'stop spooler /y', + '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + Sleep(2000); + + // Second pass: delete registry keys for each legacy printer + for I := 0 to GetArrayLength(PrinterNames) - 1 do + begin + PrinterName := PrinterNames[I]; + if IsPrinterLegacy(PrinterName) then + begin + PortValue := ''; + RegQueryStringValue(HKEY_LOCAL_MACHINE, + 'SYSTEM\CurrentControlSet\Control\Print\Printers\' + PrinterName, + 'Port', PortValue); + AddLog(' Removing: ' + PrinterName + ' (port: ' + PortValue + ')'); + Exec(ExpandConstant('{sys}\reg.exe'), + 'delete "HKLM\SYSTEM\CurrentControlSet\Control\Print\Printers\' + + PrinterName + '" /f', + '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + if ResultCode = 0 then + begin + AddLog(' Removed: ' + PrinterName); + Result := Result + 1; + end + else + AddLog(' WARNING: reg delete returned ' + IntToStr(ResultCode) + + ' for ' + PrinterName); + end; + end; + + // Also remove HKCU\Printers\Connections entries — network-connected printers + // store a connection record here that causes Windows to re-add the printer + // from the old server when the spooler restarts, even after the HKLM key + // is gone. Connection key names use commas instead of backslashes: + // \\tsgwp00525\share → ,,tsgwp00525,share + AddLog(' Checking for legacy printer connections in user hives...'); + if RegGetSubkeyNames(HKEY_USERS, '.DEFAULT\Printers\Connections', PrinterNames) then + for I := 0 to GetArrayLength(PrinterNames) - 1 do + if IsLegacyServerRef(PrinterNames[I]) then + begin + AddLog(' Removing .DEFAULT connection: ' + PrinterNames[I]); + Exec(ExpandConstant('{sys}\reg.exe'), + 'delete "HKU\.DEFAULT\Printers\Connections\' + PrinterNames[I] + '" /f', + '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + end; + + // Reuse PrinterNames var to enumerate SIDs + if RegGetSubkeyNames(HKEY_USERS, '', PrinterNames) then + for I := 0 to GetArrayLength(PrinterNames) - 1 do + if (Pos('S-1-5-', PrinterNames[I]) = 1) and (Pos('_Classes', PrinterNames[I]) = 0) then + begin + if RegGetSubkeyNames(HKEY_USERS, + PrinterNames[I] + '\Printers\Connections', LegacyFound_ConnKeys) then + for J := 0 to GetArrayLength(LegacyFound_ConnKeys) - 1 do + if IsLegacyServerRef(LegacyFound_ConnKeys[J]) then + begin + AddLog(' Removing connection for ' + PrinterNames[I] + ': ' + LegacyFound_ConnKeys[J]); + Exec(ExpandConstant('{sys}\reg.exe'), + 'delete "HKU\' + PrinterNames[I] + '\Printers\Connections\' + + LegacyFound_ConnKeys[J] + '" /f', + '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + end; + end; + + // Restart spooler — the main flow will restart it again if needed + AddLog(' Restarting spooler after cleanup...'); + Exec(ExpandConstant('{sys}\net.exe'), 'start spooler', + '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + Sleep(2000); +end; + +// ─── Main installation ─────────────────────────────────────────────────────── + procedure CurStepChanged(CurStep: TSetupStep); var I: Integer; - PSScript: String; - TempScriptPath: String; - ResultCode: Integer; - TotalCount: Integer; + HasChanges, HasHP, HasXerox, HasDesignJet, NeedSpoolerRestart: Boolean; IsX64: Boolean; - HPDriverPath, XeroxDriverPath: String; + HPInfPath, XeroxInfPath, HPDriverDir, DesignJetInfPath: String; + InstalledCount, RemovedCount, FailCount: Integer; + DriverName, PortName, PortAddress, InfPath: String; + LogDir, LogPath, SummaryMsg: String; + InstalledNames: TArrayOfString; + DefaultPrinter: String; + AllCount, CleanedCount: Integer; begin - // Run after files are extracted but before finishing - if CurStep = ssPostInstall then + if CurStep <> ssPostInstall then Exit; + + // Check whether anything actually needs to change + HasChanges := False; + for I := 0 to GetArrayLength(PrinterDataArray) - 1 do begin - // Check if there are any changes to process (installs or removals) - TotalCount := 0; - for I := 0 to GetArrayLength(PrinterDataArray) - 1 do + if PrinterSelectionPage.Values[I] <> PrinterDataArray[I].IsInstalled then begin - // Count changes: either installing (not installed but checked) or removing (installed but unchecked) - if (not PrinterDataArray[I].IsInstalled and PrinterSelectionPage.Values[I]) or - (PrinterDataArray[I].IsInstalled and not PrinterSelectionPage.Values[I]) then - TotalCount := TotalCount + 1; + HasChanges := True; + Break; end; + end; + if not HasChanges then Exit; - if TotalCount = 0 then - Exit; + InstallLog := TStringList.Create; + try + AddLog('WJDT Printer Installer starting...'); + + LogDir := ExpandConstant('{userdocs}\wjdt\logs'); + ForceDirectories(LogDir); + LogPath := LogDir + '\printer_install.log'; + + // ── Step 0: Remove legacy printers from old print servers ──────────────── + CleanedCount := CleanupLegacyPrinters(); - // Determine architecture and driver paths IsX64 := Is64BitInstallMode; if IsX64 then begin - HPDriverPath := ExpandConstant('{tmp}\hp_drivers_x64'); - XeroxDriverPath := ExpandConstant('{tmp}\xerox_drivers_x64\UNIV_5.1055.3.0_PCL6_x64_Driver.inf\x3UNIVX.inf'); + HPDriverDir := ExpandConstant('{tmp}\hp_drivers_x64'); + HPInfPath := HPDriverDir + '\hpcu355u.inf'; + XeroxInfPath := ExpandConstant('{tmp}\xerox_drivers_x64\UNIV_5.1076.3.0_PCL6_x64_Driver.inf\x3UNIVX.inf'); + DesignJetInfPath := ExpandConstant('{tmp}\hp_designjet_x64\hpi94dev.inf'); end else begin - HPDriverPath := ExpandConstant('{tmp}\hp_drivers_x32'); - XeroxDriverPath := ExpandConstant('{tmp}\xerox_drivers_x32'); + HPDriverDir := ExpandConstant('{tmp}\hp_drivers_x32'); + HPInfPath := HPDriverDir + '\hpcu355c.inf'; + XeroxInfPath := ExpandConstant('{tmp}\xerox_drivers_x32\UNIV_5.1076.3.0_PCL6_x86_Driver.inf\x3UNIVX.inf'); + DesignJetInfPath := ExpandConstant('{tmp}\hp_designjet_x32\hpi94ded.inf'); end; - // Build PowerShell installation script - PSScript := - '$ErrorActionPreference = "Continue"' + #13#10 + - '# Installer architecture mode (matches file copy behavior)' + #13#10; - - if IsX64 then - PSScript := PSScript + '$Is64BitInstallMode = $true' + #13#10 - else - PSScript := PSScript + '$Is64BitInstallMode = $false' + #13#10; - - PSScript := PSScript + - 'Write-Host ""' + #13#10 + - 'Write-Host "========================================" -ForegroundColor Cyan' + #13#10 + - 'Write-Host " WJDT Printer Installer" -ForegroundColor Cyan' + #13#10 + - 'Write-Host "========================================" -ForegroundColor Cyan' + #13#10 + - 'Write-Host ""' + #13#10 + - '' + #13#10 + - '# Function to install printer driver' + #13#10 + - 'function Install-PrinterDriver {' + #13#10 + - ' param(' + #13#10 + - ' [string]$Vendor,' + #13#10 + - ' [string]$DriverPath' + #13#10 + - ' )' + #13#10 + - '' + #13#10 + - ' try {' + #13#10 + - ' if ($Vendor -eq "HP") {' + #13#10 + - ' $driverName = "HP Universal Printing PCL 6"' + #13#10 + - ' # Use different INF file for x32 vs x64' + #13#10 + - ' if ($DriverPath -like "*x64*") {' + #13#10 + - ' $infFile = Join-Path $DriverPath "hpcu345u.inf"' + #13#10 + - ' } else {' + #13#10 + - ' $infFile = Join-Path $DriverPath "hpcu345c.inf"' + #13#10 + - ' }' + #13#10 + - ' } elseif ($Vendor -eq "Xerox") {' + #13#10 + - ' $driverName = "Xerox Global Print Driver PCL6"' + #13#10 + - ' # Check if DriverPath is already an INF file (x64) or directory (x32)' + #13#10 + - ' if ($DriverPath -like "*.inf") {' + #13#10 + - ' $infFile = $DriverPath' + #13#10 + - ' } else {' + #13#10 + - ' $infFile = Join-Path $DriverPath "x3UNIVX.inf"' + #13#10 + - ' }' + #13#10 + - ' } else {' + #13#10 + - ' return $false' + #13#10 + - ' }' + #13#10 + - '' + #13#10 + - ' # Check if driver already installed' + #13#10 + - ' $existingDriver = Get-PrinterDriver -Name $driverName -ErrorAction SilentlyContinue' + #13#10 + - ' if ($existingDriver) {' + #13#10 + - ' Write-Host " $driverName already installed" -ForegroundColor Gray' + #13#10 + - ' return $true' + #13#10 + - ' }' + #13#10 + - '' + #13#10 + - ' Write-Host " Installing $driverName driver..." -ForegroundColor Yellow' + #13#10 + - '' + #13#10 + - ' # Locate pnputil.exe - check all possible locations' + #13#10 + - ' $pnputil = $null' + #13#10 + - ' $pnputilLocations = @(' + #13#10 + - ' (Join-Path $env:SystemRoot "System32\pnputil.exe"),' + #13#10 + - ' (Join-Path $env:SystemRoot "SysNative\pnputil.exe"),' + #13#10 + - ' (Join-Path $env:SystemRoot "Syswow64\pnputil.exe"),' + #13#10 + - ' "C:\Windows\System32\pnputil.exe",' + #13#10 + - ' "C:\Windows\SysNative\pnputil.exe",' + #13#10 + - ' "C:\Windows\Syswow64\pnputil.exe"' + #13#10 + - ' )' + #13#10 + - '' + #13#10 + - ' foreach ($location in $pnputilLocations) {' + #13#10 + - ' if (Test-Path $location) {' + #13#10 + - ' $pnputil = $location' + #13#10 + - ' Write-Host " Found pnputil at: $pnputil" -ForegroundColor Gray' + #13#10 + - ' break' + #13#10 + - ' }' + #13#10 + - ' }' + #13#10 + - '' + #13#10 + - ' # Try PATH as last resort' + #13#10 + - ' if (-not $pnputil) {' + #13#10 + - ' $pnputil = (Get-Command pnputil.exe -ErrorAction SilentlyContinue).Path' + #13#10 + - ' if ($pnputil) {' + #13#10 + - ' Write-Host " Found pnputil in PATH: $pnputil" -ForegroundColor Gray' + #13#10 + - ' }' + #13#10 + - ' }' + #13#10 + - '' + #13#10 + - ' # If still not found, try without full path' + #13#10 + - ' if (-not $pnputil) {' + #13#10 + - ' $pnputil = "pnputil.exe"' + #13#10 + - ' Write-Host " Using pnputil.exe from system PATH (no full path)" -ForegroundColor Gray' + #13#10 + - ' }' + #13#10 + - '' + #13#10 + - ' # Use pnputil to add driver package' + #13#10 + - ' try {' + #13#10 + - ' # Debug: Verify file exists' + #13#10 + - ' Write-Host " INF file path: $infFile" -ForegroundColor Gray' + #13#10 + - ' if (Test-Path $infFile) {' + #13#10 + - ' Write-Host " INF file exists: YES" -ForegroundColor Green' + #13#10 + - ' } else {' + #13#10 + - ' Write-Host " INF file exists: NO - FILE NOT FOUND!" -ForegroundColor Red' + #13#10 + - ' Write-Host " ERROR: Cannot proceed without INF file" -ForegroundColor Red' + #13#10 + - ' return $false' + #13#10 + - ' }' + #13#10 + - ' Write-Host " Running: & $pnputil /add-driver $infFile /install" -ForegroundColor Gray' + #13#10 + - ' $pnpResult = & $pnputil /add-driver "$infFile" /install 2>&1' + #13#10 + - ' $pnpExitCode = $LASTEXITCODE' + #13#10 + - ' Write-Host " pnputil exit code: $pnpExitCode" -ForegroundColor Gray' + #13#10 + - ' if ($pnpResult) {' + #13#10 + - ' Write-Host " pnputil output: $pnpResult" -ForegroundColor Gray' + #13#10 + - ' }' + #13#10 + - '' + #13#10 + - ' # Exit code 0 = success, 259 = driver already installed' + #13#10 + - ' if ($pnpExitCode -ne 0 -and $pnpExitCode -ne 259) {' + #13#10 + - ' Write-Host " Warning: pnputil returned code $pnpExitCode" -ForegroundColor Yellow' + #13#10 + - ' }' + #13#10 + - ' } catch {' + #13#10 + - ' Write-Host " Warning: pnputil execution failed: $_" -ForegroundColor Yellow' + #13#10 + - ' Write-Host " Attempting to continue with Add-PrinterDriver..." -ForegroundColor Yellow' + #13#10 + - ' }' + #13#10 + - '' + #13#10 + - ' Start-Sleep -Seconds 5' + #13#10 + - '' + #13#10 + - ' # Try to add printer driver using PowerShell cmdlet' + #13#10 + - ' try {' + #13#10 + - ' Add-PrinterDriver -Name $driverName -ErrorAction Stop' + #13#10 + - ' Write-Host " $driverName installed successfully" -ForegroundColor Green' + #13#10 + - ' return $true' + #13#10 + - ' } catch {' + #13#10 + - ' # If Add-PrinterDriver fails, try legacy rundll32 method' + #13#10 + - ' Write-Host " Add-PrinterDriver failed, trying legacy method..." -ForegroundColor Yellow' + #13#10 + - ' try {' + #13#10 + - ' $null = rundll32 printui.dll,PrintUIEntry /ia /m "$driverName" /f "$infFile"' + #13#10 + - ' Start-Sleep -Seconds 3' + #13#10 + - ' # Verify driver was installed' + #13#10 + - ' $verifyDriver = Get-PrinterDriver -Name $driverName -ErrorAction SilentlyContinue' + #13#10 + - ' if ($verifyDriver) {' + #13#10 + - ' Write-Host " $driverName installed via legacy method" -ForegroundColor Green' + #13#10 + - ' return $true' + #13#10 + - ' } else {' + #13#10 + - ' Write-Host " Warning: Driver installation could not be verified" -ForegroundColor Yellow' + #13#10 + - ' return $true # Continue anyway' + #13#10 + - ' }' + #13#10 + - ' } catch {' + #13#10 + - ' Write-Host " Warning: Legacy installation also failed" -ForegroundColor Yellow' + #13#10 + - ' return $true # Continue anyway, printer creation might still work' + #13#10 + - ' }' + #13#10 + - ' }' + #13#10 + - ' } catch {' + #13#10 + - ' Write-Host " Error installing driver: $_" -ForegroundColor Red' + #13#10 + - ' return $false' + #13#10 + - ' }' + #13#10 + - '}' + #13#10 + - '' + #13#10 + - '# Function to remove printer and its port' + #13#10 + - 'function Remove-NetworkPrinter {' + #13#10 + - ' param(' + #13#10 + - ' [string]$PrinterName,' + #13#10 + - ' [string]$PortAddress' + #13#10 + - ' )' + #13#10 + - '' + #13#10 + - ' try {' + #13#10 + - ' Write-Host ""' + #13#10 + - ' Write-Host "Removing: $PrinterName" -ForegroundColor Magenta' + #13#10 + - '' + #13#10 + - ' # Check if printer exists' + #13#10 + - ' $existingPrinter = Get-Printer -Name $PrinterName -ErrorAction SilentlyContinue' + #13#10 + - ' if (-not $existingPrinter) {' + #13#10 + - ' Write-Host " SKIPPED: Printer not found" -ForegroundColor Yellow' + #13#10 + - ' return $true' + #13#10 + - ' }' + #13#10 + - '' + #13#10 + - ' # Remove printer' + #13#10 + - ' Write-Host " Removing printer..." -ForegroundColor Yellow' + #13#10 + - ' Remove-Printer -Name $PrinterName -ErrorAction Stop' + #13#10 + - ' Write-Host " Printer removed" -ForegroundColor Green' + #13#10 + - '' + #13#10 + - ' # Remove port if exists and not used by other printers' + #13#10 + - ' $portName = "IP_$PortAddress"' + #13#10 + - ' $existingPort = Get-PrinterPort -Name $portName -ErrorAction SilentlyContinue' + #13#10 + - ' if ($existingPort) {' + #13#10 + - ' $printersUsingPort = Get-Printer | Where-Object { $_.PortName -eq $portName }' + #13#10 + - ' if ($printersUsingPort.Count -eq 0) {' + #13#10 + - ' Write-Host " Removing unused port..." -ForegroundColor Yellow' + #13#10 + - ' Remove-PrinterPort -Name $portName -ErrorAction Stop' + #13#10 + - ' Write-Host " Port removed: $portName" -ForegroundColor Green' + #13#10 + - ' } else {' + #13#10 + - ' Write-Host " Port still in use by other printers" -ForegroundColor Gray' + #13#10 + - ' }' + #13#10 + - ' }' + #13#10 + - '' + #13#10 + - ' Write-Host " SUCCESS: $PrinterName removed" -ForegroundColor Green' + #13#10 + - ' return $true' + #13#10 + - ' } catch {' + #13#10 + - ' Write-Host " ERROR: $_" -ForegroundColor Red' + #13#10 + - ' return $false' + #13#10 + - ' }' + #13#10 + - '}' + #13#10 + - '' + #13#10 + - '# Function to install individual printer' + #13#10 + - 'function Install-NetworkPrinter {' + #13#10 + - ' param(' + #13#10 + - ' [string]$PrinterName,' + #13#10 + - ' [string]$PortAddress,' + #13#10 + - ' [string]$Vendor,' + #13#10 + - ' [string]$Model' + #13#10 + - ' )' + #13#10 + - '' + #13#10 + - ' try {' + #13#10 + - ' Write-Host ""' + #13#10 + - ' Write-Host "Installing: $PrinterName" -ForegroundColor Cyan' + #13#10 + - ' Write-Host " Address: $PortAddress" -ForegroundColor Gray' + #13#10 + - ' Write-Host " Model: $Vendor $Model" -ForegroundColor Gray' + #13#10 + - '' + #13#10 + - ' # Determine driver name' + #13#10 + - ' if ($Vendor -eq "HP") {' + #13#10 + - ' $driverName = "HP Universal Printing PCL 6"' + #13#10 + - ' } elseif ($Vendor -eq "Xerox") {' + #13#10 + - ' $driverName = "Xerox Global Print Driver PCL6"' + #13#10 + - ' } elseif ($Vendor -eq "HID") {' + #13#10 + - ' $driverName = "DTC4500e Card Printer"' + #13#10 + - ' } else {' + #13#10 + - ' Write-Host " Unknown vendor: $Vendor" -ForegroundColor Red' + #13#10 + - ' return $false' + #13#10 + - ' }' + #13#10 + - '' + #13#10 + - ' # For USB card printers, driver installation is enough' + #13#10 + - ' # User plugs in USB device and Windows auto-configures it' + #13#10 + - ' if ($PortAddress -eq "USB") {' + #13#10 + - ' Write-Host " USB card printer - driver installed" -ForegroundColor Green' + #13#10 + - ' Write-Host " Connect printer via USB and Windows will detect it" -ForegroundColor Gray' + #13#10 + - ' Write-Host " SUCCESS: $driverName ready for USB device" -ForegroundColor Green' + #13#10 + - ' return $true' + #13#10 + - ' }' + #13#10 + - '' + #13#10 + - ' # For network printers, check if printer already exists' + #13#10 + - ' $existingPrinter = Get-Printer -Name $PrinterName -ErrorAction SilentlyContinue' + #13#10 + - ' if ($existingPrinter) {' + #13#10 + - ' Write-Host " SKIPPED: Printer already exists" -ForegroundColor Yellow' + #13#10 + - ' return $true' + #13#10 + - ' }' + #13#10 + - '' + #13#10 + - ' # Create port name for network printers' + #13#10 + - ' $portName = "IP_$PortAddress"' + #13#10 + - '' + #13#10 + - ' # Check if port exists' + #13#10 + - ' $existingPort = Get-PrinterPort -Name $portName -ErrorAction SilentlyContinue' + #13#10 + - ' if (-not $existingPort) {' + #13#10 + - ' Write-Host " Creating printer port..." -ForegroundColor Yellow' + #13#10 + - ' Add-PrinterPort -Name $portName -PrinterHostAddress $PortAddress -ErrorAction Stop' + #13#10 + - ' Write-Host " Port created: $portName" -ForegroundColor Green' + #13#10 + - ' } else {' + #13#10 + - ' Write-Host " Using existing port: $portName" -ForegroundColor Gray' + #13#10 + - ' }' + #13#10 + - '' + #13#10 + - ' # Add printer' + #13#10 + - ' Write-Host " Adding printer..." -ForegroundColor Yellow' + #13#10 + - ' Add-Printer -Name $PrinterName -DriverName $driverName -PortName $portName -ErrorAction Stop' + #13#10 + - ' Write-Host " SUCCESS: $PrinterName installed" -ForegroundColor Green' + #13#10 + - ' return $true' + #13#10 + - ' } catch {' + #13#10 + - ' Write-Host " ERROR: $_" -ForegroundColor Red' + #13#10 + - ' return $false' + #13#10 + - ' }' + #13#10 + - '}' + #13#10 + - '' + #13#10 + - 'Write-Host "Step 1: Installing printer drivers..." -ForegroundColor Cyan' + #13#10 + - 'Write-Host ""' + #13#10 + - '' + #13#10; - - // Install HP driver if needed - PSScript := PSScript + - 'if (Install-PrinterDriver -Vendor "HP" -DriverPath "' + HPDriverPath + '") {' + #13#10 + - ' Write-Host ""' + #13#10 + - '} else {' + #13#10 + - ' Write-Host "Failed to install HP driver. Continuing anyway..." -ForegroundColor Yellow' + #13#10 + - ' Write-Host ""' + #13#10 + - '}' + #13#10; - - // Install Xerox driver (both x64 and x32 now supported) - PSScript := PSScript + - 'if (Install-PrinterDriver -Vendor "Xerox" -DriverPath "' + XeroxDriverPath + '") {' + #13#10 + - ' Write-Host ""' + #13#10 + - '} else {' + #13#10 + - ' Write-Host "Failed to install Xerox driver. Continuing anyway..." -ForegroundColor Yellow' + #13#10 + - ' Write-Host ""' + #13#10 + - '}' + #13#10 + - '' + #13#10; - - PSScript := PSScript + - 'Write-Host "Step 2: Processing printer changes..." -ForegroundColor Cyan' + #13#10 + - '' + #13#10 + - '$installedCount = 0' + #13#10 + - '$removedCount = 0' + #13#10 + - '$failCount = 0' + #13#10 + - '' + #13#10; - - // Process each printer: install, remove, or skip + // Determine which vendor drivers are needed for new installs + HasHP := False; + HasXerox := False; + HasDesignJet := False; for I := 0 to GetArrayLength(PrinterDataArray) - 1 do begin - // If was installed but now unchecked - REMOVE - if PrinterDataArray[I].IsInstalled and (not PrinterSelectionPage.Values[I]) then + if (not PrinterDataArray[I].IsInstalled) and PrinterSelectionPage.Values[I] then begin - PSScript := PSScript + - 'if (Remove-NetworkPrinter -PrinterName "' + PrinterDataArray[I].PrinterName + '" ' + - '-PortAddress "' + PrinterDataArray[I].FQDN + '") {' + #13#10 + - ' $removedCount++' + #13#10 + - '} else {' + #13#10 + - ' $failCount++' + #13#10 + - '}' + #13#10; - end - // If not installed and now checked - INSTALL - else if (not PrinterDataArray[I].IsInstalled) and PrinterSelectionPage.Values[I] then - begin - PSScript := PSScript + - 'if (Install-NetworkPrinter -PrinterName "' + PrinterDataArray[I].PrinterName + '" ' + - '-PortAddress "' + PrinterDataArray[I].FQDN + '" ' + - '-Vendor "' + PrinterDataArray[I].Vendor + '" ' + - '-Model "' + PrinterDataArray[I].Model + '") {' + #13#10 + - ' $installedCount++' + #13#10 + - '} else {' + #13#10 + - ' $failCount++' + #13#10 + - '}' + #13#10; + if PrinterDataArray[I].Vendor = 'Xerox' then HasXerox := True + else if PrinterDataArray[I].Vendor = 'HP' then + begin + if IsDesignJetModel(PrinterDataArray[I].Model) then HasDesignJet := True + else HasHP := True; + end; end; - // If state hasn't changed - SKIP (no code generated) end; - // Add summary - PSScript := PSScript + - '' + #13#10 + - 'Write-Host ""' + #13#10 + - 'Write-Host "========================================" -ForegroundColor Cyan' + #13#10 + - 'Write-Host " Operation Complete" -ForegroundColor Cyan' + #13#10 + - 'Write-Host "========================================" -ForegroundColor Cyan' + #13#10 + - 'Write-Host ""' + #13#10 + - 'if ($installedCount -gt 0) {' + #13#10 + - ' Write-Host " Printers installed: $installedCount" -ForegroundColor Green' + #13#10 + - '}' + #13#10 + - 'if ($removedCount -gt 0) {' + #13#10 + - ' Write-Host " Printers removed: $removedCount" -ForegroundColor Magenta' + #13#10 + - '}' + #13#10 + - 'if ($failCount -gt 0) {' + #13#10 + - ' Write-Host " Failed operations: $failCount" -ForegroundColor Red' + #13#10 + - '}' + #13#10 + - 'if ($installedCount -eq 0 -and $removedCount -eq 0 -and $failCount -eq 0) {' + #13#10 + - ' Write-Host " No changes were made" -ForegroundColor Gray' + #13#10 + - '}' + #13#10 + - 'Write-Host ""' + #13#10 + - 'Write-Host "Press any key to close..." -ForegroundColor Gray' + #13#10 + - '$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")'; + NeedSpoolerRestart := False; - // Save and execute script - TempScriptPath := ExpandConstant('{tmp}\install_printers.ps1'); - SaveStringToFile(TempScriptPath, PSScript, False); + // ── Step 1: Stage drivers with pnputil ────────────────────────────────── + AddLog('Step 1: Staging printer drivers...'); - Exec('powershell.exe', - '-NoProfile -ExecutionPolicy Bypass -File "' + TempScriptPath + '"', - '', SW_SHOW, ewWaitUntilTerminated, ResultCode); + if HasHP then + begin + if IsDriverInstalled('HP Universal Printing PCL 6', IsX64) then + AddLog(' HP driver already installed') + else + begin + AddLog(' Staging HP Universal Printing PCL 6...'); + if StageDriver(HPInfPath) then + begin + AddLog(' HP driver staged OK'); + NeedSpoolerRestart := True; + end + else + AddLog(' WARNING: HP staging failed — INF: ' + HPInfPath); + end; + end; - DeleteFile(TempScriptPath); + if HasDesignJet then + begin + if IsDriverInstalled('HP DesignJet T1700dr PS3', IsX64) then + AddLog(' HP DesignJet T1700dr PS3 driver already installed') + else + begin + AddLog(' Staging HP DesignJet T1700dr PS3...'); + if StageDriver(DesignJetInfPath) then + begin + AddLog(' HP DesignJet driver staged OK'); + NeedSpoolerRestart := True; + end + else + AddLog(' WARNING: HP DesignJet staging failed — INF: ' + DesignJetInfPath); + end; + end; + + if HasXerox then + begin + if IsDriverInstalled('Xerox Global Print Driver PCL6', IsX64) then + AddLog(' Xerox driver already installed') + else + begin + AddLog(' Staging Xerox Global Print Driver PCL6...'); + if StageDriver(XeroxInfPath) then + begin + AddLog(' Xerox driver staged OK'); + NeedSpoolerRestart := True; + end + else + AddLog(' WARNING: Xerox staging failed — INF: ' + XeroxInfPath); + end; + end; + + // ── Step 2: Restart spooler to register staged drivers ────────────────── + if NeedSpoolerRestart then + begin + AddLog('Step 2: Restarting print spooler to register staged drivers...'); + RestartSpooler(); + AddLog(' Spooler restarted'); + end; + + // ── Step 3: Create TCP/IP ports ────────────────────────────────────────── + AddLog('Step 3: Creating printer ports...'); + + for I := 0 to GetArrayLength(PrinterDataArray) - 1 do + begin + if (not PrinterDataArray[I].IsInstalled) and PrinterSelectionPage.Values[I] then + begin + PortAddress := PrinterDataArray[I].FQDN; + PortName := 'IP_' + PortAddress; + + if IsPortInstalled(PortName) then + AddLog(' Port exists (re-registering with spooler): ' + PortName) + else + AddLog(' Creating port: ' + PortName); + + // Always call CreatePortViaWMI — it handles both new and stale-existing + // ports by attempting live WMI registration and refreshing registry values. + CreatePortViaWMI(PortAddress); + end; + end; + + // ── Step 4: Register drivers with print subsystem (fallback after restart) ─ + if HasHP and not IsDriverInstalled('HP Universal Printing PCL 6', IsX64) then + begin + AddLog(' Registering HP driver via printui...'); + InstallDriverViaUI('HP Universal Printing PCL 6', HPInfPath); + if IsDriverInstalled('HP Universal Printing PCL 6', IsX64) then + AddLog(' HP driver registered OK') + else + AddLog(' WARNING: HP driver not confirmed after registration'); + end; + + if HasDesignJet and not IsDriverInstalled('HP DesignJet T1700dr PS3', IsX64) then + begin + AddLog(' Registering HP DesignJet driver via printui...'); + InstallDriverViaUI('HP DesignJet T1700dr PS3', DesignJetInfPath); + if IsDriverInstalled('HP DesignJet T1700dr PS3', IsX64) then + AddLog(' HP DesignJet driver registered OK') + else + AddLog(' WARNING: HP DesignJet driver not confirmed after registration'); + end; + + if HasXerox and not IsDriverInstalled('Xerox Global Print Driver PCL6', IsX64) then + begin + AddLog(' Registering Xerox driver via printui...'); + InstallDriverViaUI('Xerox Global Print Driver PCL6', XeroxInfPath); + if IsDriverInstalled('Xerox Global Print Driver PCL6', IsX64) then + AddLog(' Xerox driver registered OK') + else + AddLog(' WARNING: Xerox driver not confirmed after registration'); + end; + + // ── Step 5: Restart spooler before printer changes to ensure all ports + // and drivers are fully loaded in the live spooler session ────── + AddLog('Step 5: Restarting spooler before printer installation...'); + RestartSpooler(); + AddLog(' Spooler restarted'); + + // ── Step 6: Install / remove printers ─────────────────────────────────── + InstalledCount := 0; + RemovedCount := 0; + FailCount := 0; + SetArrayLength(InstalledNames, GetArrayLength(PrinterDataArray)); + + AddLog('Step 6: Processing printer changes...'); + + for I := 0 to GetArrayLength(PrinterDataArray) - 1 do + begin + // Remove: currently installed but now unchecked + if PrinterDataArray[I].IsInstalled and (not PrinterSelectionPage.Values[I]) then + begin + AddLog('Removing: ' + PrinterDataArray[I].PrinterName); + if RemoveNetworkPrinter(PrinterDataArray[I].PrinterName) then + begin + AddLog(' Removed successfully'); + RemovedCount := RemovedCount + 1; + end + else + begin + AddLog(' FAILED to remove: ' + PrinterDataArray[I].PrinterName); + FailCount := FailCount + 1; + end; + end + + // Install: not installed but now checked + else if (not PrinterDataArray[I].IsInstalled) and PrinterSelectionPage.Values[I] then + begin + AddLog('Installing: ' + PrinterDataArray[I].PrinterName); + + if PrinterDataArray[I].Vendor = 'HP' then + begin + if IsDesignJetModel(PrinterDataArray[I].Model) then + begin + DriverName := 'HP DesignJet T1700dr PS3'; + InfPath := DesignJetInfPath; + end + else + begin + DriverName := 'HP Universal Printing PCL 6'; + InfPath := HPInfPath; + end; + end + else if PrinterDataArray[I].Vendor = 'Xerox' then + begin + DriverName := 'Xerox Global Print Driver PCL6'; + InfPath := XeroxInfPath; + end + else + begin + DriverName := ''; + InfPath := ''; + end; + + if DriverName = '' then + AddLog(' SKIPPED: unsupported vendor ' + PrinterDataArray[I].Vendor) + else + begin + PortAddress := PrinterDataArray[I].FQDN; + PortName := 'IP_' + PortAddress; + + if not IsPortInstalled(PortName) then + AddLog(' WARNING: port ' + PortName + ' not found'); + if not IsDriverInstalled(DriverName, IsX64) then + AddLog(' WARNING: driver "' + DriverName + '" not found'); + + if AddNetworkPrinter(PrinterDataArray[I].PrinterName, DriverName, PortName, InfPath) then + begin + AddLog(' Installed successfully'); + InstalledNames[InstalledCount] := PrinterDataArray[I].PrinterName; + InstalledCount := InstalledCount + 1; + end + else + begin + AddLog(' FAILED to install'); + FailCount := FailCount + 1; + end; + end; + end; + // Unchanged entries: skip + end; + + // Build list of ALL currently installed printers (pre-existing + just installed) + // so the user can pick any of them as the new default. + SetArrayLength(InstalledNames, GetArrayLength(PrinterDataArray)); + AllCount := 0; + for I := 0 to GetArrayLength(PrinterDataArray) - 1 do + if IsPrinterInstalled(PrinterDataArray[I].PrinterName) then + begin + InstalledNames[AllCount] := PrinterDataArray[I].PrinterName; + AllCount := AllCount + 1; + end; + SetArrayLength(InstalledNames, AllCount); + + DefaultPrinter := ''; + if InstalledCount > 0 then + begin + DefaultPrinter := PromptDefaultPrinter(InstalledNames); + if DefaultPrinter <> '' then + SetDefaultPrinterAllUsers(DefaultPrinter); + end; + + // Save log + InstallLog.SaveToFile(LogPath); + + // Show summary + SummaryMsg := 'Printer installation complete!' + #13#10 + #13#10; + if InstalledCount > 0 then + SummaryMsg := SummaryMsg + 'Installed: ' + IntToStr(InstalledCount) + #13#10; + if RemovedCount > 0 then + SummaryMsg := SummaryMsg + 'Removed: ' + IntToStr(RemovedCount) + #13#10; + if CleanedCount > 0 then + SummaryMsg := SummaryMsg + 'Cleaned: ' + IntToStr(CleanedCount) + ' legacy printer(s)' + #13#10; + if FailCount > 0 then + SummaryMsg := SummaryMsg + 'Failed: ' + IntToStr(FailCount) + #13#10; + if DefaultPrinter <> '' then + SummaryMsg := SummaryMsg + 'Default: ' + DefaultPrinter + #13#10; + MsgBox(SummaryMsg, mbInformation, MB_OK); + + finally + InstallLog.Free; + InstallLog := nil; end; end; -// Skip directory selection +// ─── Page skip ─────────────────────────────────────────────────────────────── + function ShouldSkipPage(PageID: Integer): Boolean; begin Result := False;