$ErrorActionPreference = "Stop" # ------------------------- # CONFIG # ------------------------- $ScriptStart = Get-Date $BaseFolder = "C:\CAREpoint3" $LogFolder = Join-Path $BaseFolder "Logs" $SettingsPath = "C:\ProgramData\General Devices\D-Scribe X Service\settings.json" $BackupDir = Split-Path -Parent $SettingsPath $SettingsDir = Split-Path -Parent $SettingsPath # Adjust these if your actual service name is different $ServiceNameCandidates = @( "D-Scribe X Service", "D-ScribeX Service" ) # ------------------------- # LOG LOCATION + ROTATION # ------------------------- if (!(Test-Path -LiteralPath $BaseFolder)) { New-Item -ItemType Directory -Path $BaseFolder -Force | Out-Null } if (!(Test-Path -LiteralPath $LogFolder)) { New-Item -ItemType Directory -Path $LogFolder -Force | Out-Null } $ScriptName = $MyInvocation.MyCommand.Name if ([string]::IsNullOrWhiteSpace($ScriptName)) { $ScriptName = "PowerShellScript" } $TimeStamp = Get-Date -Format "yyyy-MM-dd_HH-mm-ss" $LogFile = Join-Path $LogFolder "$ScriptName-$TimeStamp.log" function Rotate-Logs { param( [Parameter(Mandatory)] [string]$Folder, [Parameter(Mandatory)] [string]$NamePrefix, [int]$KeepCount = 10 ) try { $pattern = "$NamePrefix-*.log" $files = Get-ChildItem -Path $Folder -Filter $pattern -File -ErrorAction Stop | Sort-Object LastWriteTime -Descending if ($files.Count -gt $KeepCount) { $files | Select-Object -Skip $KeepCount | ForEach-Object { Remove-Item -LiteralPath $_.FullName -Force -ErrorAction Stop } } } catch { Write-Host "WARN: Log rotation warning: $($_.Exception.Message)" -ForegroundColor Yellow } } Rotate-Logs -Folder $LogFolder -NamePrefix $ScriptName -KeepCount 10 function Log { param( [Parameter(Mandatory = $true)][string]$Message, [ValidateSet("INFO","WARN","ERROR","SUCCESS")][string]$Level = "INFO" ) $color = switch ($Level) { "INFO" { "Green" } "SUCCESS" { "Green" } "WARN" { "Yellow" } "ERROR" { "Red" } } $ts = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") $line = "$ts|$Level|$Message" Write-Host $line -ForegroundColor $color Add-Content -Path $LogFile -Value $line } function Test-IsAdministrator { try { $currentIdentity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object Security.Principal.WindowsPrincipal($currentIdentity) return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } catch { return $false } } Log "===== Script Started =====" "INFO" Log ("Log file: {0}" -f $LogFile) "INFO" Log ("Running as: {0} on {1}" -f $env:USERNAME, $env:COMPUTERNAME) "INFO" Log ("PowerShell version: {0}" -f $PSVersionTable.PSVersion) "INFO" Log ("PowerShell bitness (IntPtr::Size): {0} (8=64bit, 4=32bit)" -f [IntPtr]::Size) "INFO" if (-not (Test-IsAdministrator)) { throw "This script must be run as Administrator." } Log "Administrator check passed." "INFO" # ------------------------- # ENSURE SETTINGS PATH # ------------------------- if (!(Test-Path -LiteralPath $SettingsDir)) { New-Item -ItemType Directory -Path $SettingsDir -Force | Out-Null Log ("Created settings directory: {0}" -f $SettingsDir) "INFO" } # ------------------------- # HELPERS # ------------------------- function Read-HostWithDefault { param( [Parameter(Mandatory = $true)][string]$Prompt, [Parameter(Mandatory = $true)][string]$Default ) $val = Read-Host "$Prompt [$Default]" if ([string]::IsNullOrWhiteSpace($val)) { return $Default } return $val } function Read-JsonIfExists { param([Parameter(Mandatory = $true)][string]$Path) if (Test-Path -LiteralPath $Path) { try { return (Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json) } catch { Log "Found settings.json but could not parse JSON (will ignore and prompt as needed)." "WARN" return $null } } return $null } function ConvertFrom-SecureStringToPlain { param([Parameter(Mandatory)][securestring]$Secure) $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($Secure) try { return [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) } finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } } function Read-SecretPlain { param( [Parameter(Mandatory)][string]$Prompt, [string]$Default = $null ) if ([string]::IsNullOrWhiteSpace($Default)) { $sec = Read-Host $Prompt -AsSecureString $plain = ConvertFrom-SecureStringToPlain -Secure $sec return $plain } Write-Host "$Prompt [Press Enter to keep existing value]" -ForegroundColor Yellow $sec = Read-Host $Prompt -AsSecureString $plain = ConvertFrom-SecureStringToPlain -Secure $sec if ([string]::IsNullOrWhiteSpace($plain)) { return $Default } return $plain } function Parse-ConnectionStringPieces { param([Parameter(Mandatory = $true)][string]$ConnStr) $result = @{ Server = $null Database = $null User = $null Port = $null } if ([string]::IsNullOrWhiteSpace($ConnStr)) { return $result } $pairs = $ConnStr -split ';' | Where-Object { $_ -match '=' } foreach ($p in $pairs) { $kv = $p -split '=', 2 if ($kv.Count -lt 2) { continue } $k = $kv[0].Trim().ToLowerInvariant() $v = $kv[1].Trim() switch ($k) { 'server' { $result.Server = $v } 'data source' { $result.Server = $v } 'host' { $result.Server = $v } 'database' { $result.Database = $v } 'initial catalog' { $result.Database = $v } 'user' { $result.User = $v } 'user id' { $result.User = $v } 'uid' { $result.User = $v } 'port' { $result.Port = $v } } } return $result } function Get-MySqlProviderPath { $candidates = @( "C:\Program Files\MySQL\MySQL Connector NET 9.0\MySql.Data.dll", "C:\Program Files\MySQL\MySQL Connector NET 8.4\MySql.Data.dll", "C:\Program Files\MySQL\MySQL Connector NET 8.3\MySql.Data.dll", "C:\Program Files\MySQL\MySQL Connector NET 8.2\MySql.Data.dll", "C:\Program Files\MySQL\MySQL Connector NET 8.1\MySql.Data.dll", "C:\Program Files\MySQL\MySQL Connector NET 8.0\MySql.Data.dll", "C:\Program Files\MySQL\MySQL Connector Net 8.0\Assemblies\v4.5.2\MySql.Data.dll", "C:\Program Files\MySQL\MySQL Connector Net 8.0\Assemblies\v4.8\MySql.Data.dll", "C:\Program Files (x86)\MySQL\MySQL Connector NET 9.0\MySql.Data.dll", "C:\Program Files (x86)\MySQL\MySQL Connector NET 8.4\MySql.Data.dll", "C:\Program Files (x86)\MySQL\MySQL Connector NET 8.3\MySql.Data.dll", "C:\Program Files (x86)\MySQL\MySQL Connector NET 8.2\MySql.Data.dll", "C:\Program Files (x86)\MySQL\MySQL Connector NET 8.1\MySql.Data.dll", "C:\Program Files (x86)\MySQL\MySQL Connector NET 8.0\MySql.Data.dll", "C:\Program Files (x86)\MySQL\MySQL Connector Net 8.0\Assemblies\v4.5.2\MySql.Data.dll", "C:\Program Files (x86)\MySQL\MySQL Connector Net 8.0\Assemblies\v4.8\MySql.Data.dll" ) foreach ($candidate in $candidates) { if (Test-Path -LiteralPath $candidate) { return $candidate } } try { $found = Get-ChildItem -Path "C:\Program Files","C:\Program Files (x86)" -Recurse -Filter "MySql.Data.dll" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName -First 1 if (-not [string]::IsNullOrWhiteSpace($found)) { return $found } } catch { Log ("DLL search warning: {0}" -f $_.Exception.Message) "WARN" } return $null } function Load-MySqlProvider { param([Parameter(Mandatory)][string]$DllPath) if (-not (Test-Path -LiteralPath $DllPath)) { throw "MySQL provider DLL not found at: $DllPath" } try { $alreadyLoaded = [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { try { $_.GetName().Name -eq "MySql.Data" } catch { $false } } | Select-Object -First 1 if ($alreadyLoaded) { Log ("MySQL provider assembly already loaded: {0}" -f $alreadyLoaded.FullName) "INFO" return } [void][System.Reflection.Assembly]::LoadFrom($DllPath) $loadedNow = [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { try { $_.GetName().Name -eq "MySql.Data" } catch { $false } } | Select-Object -First 1 if (-not $loadedNow) { throw "Assembly load did not throw an error, but MySql.Data is still not loaded." } Log ("Loaded MySQL provider DLL: {0}" -f $DllPath) "INFO" Log ("Loaded MySQL assembly: {0}" -f $loadedNow.FullName) "INFO" } catch [System.Reflection.ReflectionTypeLoadException] { Log ("Failed loading MySQL provider DLL: {0}" -f $DllPath) "ERROR" if ($_.Exception.LoaderExceptions) { foreach ($loaderEx in $_.Exception.LoaderExceptions) { if ($loaderEx) { Log ("LoaderException: {0}" -f $loaderEx.Message) "ERROR" } } } throw } catch { Log ("Failed loading MySQL provider DLL: {0}" -f $DllPath) "ERROR" Log ("Assembly load error: {0}" -f $_.Exception.Message) "ERROR" throw } } function Test-MySqlLoginOnly { param( [Parameter(Mandatory)][string]$Server, [Parameter(Mandatory)][int]$Port, [Parameter(Mandatory)][string]$User, [Parameter(Mandatory)][string]$Password, [int]$TimeoutSeconds = 10 ) $cs = "Server=$Server;Port=$Port;User ID=$User;Password=$Password;Connection Timeout=$TimeoutSeconds;" $conn = $null try { $conn = New-Object MySql.Data.MySqlClient.MySqlConnection($cs) $conn.Open() $cmd = $conn.CreateCommand() $cmd.CommandText = "SELECT 1;" [void]$cmd.ExecuteScalar() $conn.Close() return $true } catch { try { if ($conn) { $conn.Close() } } catch {} Log ("MySQL login-only test failed: {0}" -f $_.Exception.Message) "ERROR" return $false } finally { try { if ($conn) { $conn.Dispose() } } catch {} } } function Build-OrderedConfig { param( [hashtable]$AllValues, [string[]]$PreferredOrder ) $ordered = [ordered]@{} foreach ($k in $PreferredOrder) { if ($AllValues.ContainsKey($k)) { $ordered[$k] = $AllValues[$k] } } foreach ($k in $AllValues.Keys) { if (-not $ordered.Contains($k)) { $ordered[$k] = $AllValues[$k] } } return $ordered } function Get-ExistingService { param([string[]]$Names) foreach ($name in $Names) { try { $svc = Get-Service -Name $name -ErrorAction Stop if ($svc) { return $svc } } catch {} } return $null } function Clear-ReadOnlyIfNeeded { param([Parameter(Mandatory)][string]$Path) if (Test-Path -LiteralPath $Path) { $file = Get-Item -LiteralPath $Path -Force if ($file.Attributes -band [IO.FileAttributes]::ReadOnly) { $file.Attributes = ($file.Attributes -bxor [IO.FileAttributes]::ReadOnly) Log ("Cleared read-only attribute on: {0}" -f $Path) "INFO" } } } function Write-JsonAtomic { param( [Parameter(Mandatory)][object]$Object, [Parameter(Mandatory)][string]$DestinationPath ) $tmpPath = "$DestinationPath.tmp" $destExists = Test-Path -LiteralPath $DestinationPath $service = $null $serviceWasRunning = $false try { $json = $Object | ConvertTo-Json -Depth 50 [System.IO.File]::WriteAllText($tmpPath, $json, [System.Text.UTF8Encoding]::new($false)) Log ("Temporary JSON written: {0}" -f $tmpPath) "INFO" if ($destExists) { Clear-ReadOnlyIfNeeded -Path $DestinationPath } $service = Get-ExistingService -Names $ServiceNameCandidates if ($service -and $service.Status -eq 'Running') { $serviceWasRunning = $true Log ("Stopping service before file replacement: {0}" -f $service.Name) "INFO" Stop-Service -Name $service.Name -Force -ErrorAction Stop $service.WaitForStatus('Stopped', '00:00:20') Log ("Service stopped: {0}" -f $service.Name) "INFO" } $written = $false $attempts = 5 for ($i = 1; $i -le $attempts; $i++) { try { if (Test-Path -LiteralPath $DestinationPath) { [System.IO.File]::Copy($tmpPath, $DestinationPath, $true) } else { [System.IO.File]::Move($tmpPath, $DestinationPath) } $written = $true Log ("Replaced settings file on attempt {0}." -f $i) "INFO" break } catch { Log ("Write attempt {0} failed: {1}" -f $i, $_.Exception.Message) "WARN" Start-Sleep -Milliseconds 750 if (Test-Path -LiteralPath $DestinationPath) { try { Clear-ReadOnlyIfNeeded -Path $DestinationPath } catch {} } } } if (-not $written) { throw "Failed to write settings.json after multiple attempts. The file may still be locked or access is restricted." } if (Test-Path -LiteralPath $tmpPath) { Remove-Item -LiteralPath $tmpPath -Force -ErrorAction SilentlyContinue } } catch { try { if (Test-Path -LiteralPath $tmpPath) { Remove-Item -LiteralPath $tmpPath -Force -ErrorAction SilentlyContinue } } catch {} throw } finally { if ($service -and $serviceWasRunning) { try { Log ("Starting service after file replacement: {0}" -f $service.Name) "INFO" Start-Service -Name $service.Name -ErrorAction Stop Log ("Service started: {0}" -f $service.Name) "INFO" } catch { Log ("Could not restart service automatically: {0}" -f $_.Exception.Message) "WARN" } } } } # ------------------------- # MAIN # ------------------------- try { Write-Host "" Write-Host "===== D-Scribe X Settings Configuration =====" -ForegroundColor Cyan Write-Host "" $existing = Read-JsonIfExists -Path $SettingsPath $config = @{} if ($existing) { foreach ($p in $existing.PSObject.Properties) { $config[$p.Name] = $p.Value } } # Remove only the keys requested $keysToRemove = @( "ArtemisApiHost", "ApiKey", "GraphTenant", "GraphClientId", "GraphClientSecret" ) foreach ($key in $keysToRemove) { if ($config.ContainsKey($key)) { $null = $config.Remove($key) Log ("Removed key from config: {0}" -f $key) "INFO" } } $defElasticPass = if ($config.ContainsKey("ElasticSearchPassword")) { [string]$config["ElasticSearchPassword"] } else { "GDdscribeuser#" } $defEbridgeBase = if ($config.ContainsKey("EbridgeApiBaseAddress")) { [string]$config["EbridgeApiBaseAddress"] } else { "https://default-ebridge.gdserver.net/api/v1/" } $defApiUser = if ($config.ContainsKey("ApiUser")) { [string]$config["ApiUser"] } else { "defaultdscribex" } $defApiPassword = if ($config.ContainsKey("ApiPassword")) { [string]$config["ApiPassword"] } else { "" } $defDeviceId = if ($config.ContainsKey("DeviceId")) { [string]$config["DeviceId"] } else { "0000" } $defEncPassword = if ($config.ContainsKey("EncPassword")) { [string]$config["EncPassword"] } else { "" } $existingConn = if ($config.ContainsKey("ConnectionString")) { [string]$config["ConnectionString"] } else { "" } $connPieces = Parse-ConnectionStringPieces -ConnStr $existingConn $defDbServer = if ($connPieces.Server) { [string]$connPieces.Server } else { "localhost" } $defDbName = if ($connPieces.Database) { [string]$connPieces.Database } else { "dscribex" } $defDbUser = if ($connPieces.User) { [string]$connPieces.User } else { "dscribex" } $defDbPort = if ($connPieces.Port) { [string]$connPieces.Port } else { "3306" } # ------------------------- # PROMPTS # ------------------------- $ElasticSearchPassword = Read-SecretPlain -Prompt "ElasticSearchPassword" -Default $defElasticPass $EbridgeApiBaseAddress = Read-HostWithDefault -Prompt "EbridgeApiBaseAddress" -Default $defEbridgeBase $ApiUser = Read-HostWithDefault -Prompt "ApiUser" -Default $defApiUser $ApiPassword = Read-SecretPlain -Prompt "ApiPassword" -Default $defApiPassword $DeviceId = Read-HostWithDefault -Prompt "DeviceId" -Default $defDeviceId Write-Host "" Write-Host "----- ConnectionString + MySQL LOGIN-ONLY TEST (NO DB REQUIRED) -----" -ForegroundColor Yellow $DbServer = Read-HostWithDefault -Prompt "DB Server" -Default $defDbServer $DbName = Read-HostWithDefault -Prompt "DB Database (may be created later)" -Default $defDbName $DbUser = Read-HostWithDefault -Prompt "DB User" -Default $defDbUser $DbPortText = Read-HostWithDefault -Prompt "DB Port" -Default $defDbPort $parsedPort = 0 if (-not [int]::TryParse($DbPortText, [ref]$parsedPort)) { throw "DB Port must be a valid integer. Value entered: '$DbPortText'" } if ($parsedPort -lt 1 -or $parsedPort -gt 65535) { throw "DB Port must be between 1 and 65535. Value entered: '$parsedPort'" } [int]$DbPort = $parsedPort $DbPass = Read-SecretPlain -Prompt "DB Password" # ALWAYS run login-only test and ABORT if it fails $MySqlProviderDllPath = Get-MySqlProviderPath if ([string]::IsNullOrWhiteSpace($MySqlProviderDllPath)) { throw "Could not find MySql.Data.dll in common install paths. Install MySQL Connector/NET or update the script search paths." } Log ("Using MySQL provider DLL: {0}" -f $MySqlProviderDllPath) "INFO" Load-MySqlProvider -DllPath $MySqlProviderDllPath Log ("Testing MySQL LOGIN ONLY to {0}:{1} as '{2}' (no DB selected)..." -f $DbServer, $DbPort, $DbUser) "INFO" $loginOk = Test-MySqlLoginOnly -Server $DbServer -Port $DbPort -User $DbUser -Password $DbPass -TimeoutSeconds 10 if (-not $loginOk) { throw "MySQL login-only test FAILED. Aborting without writing settings.json." } Log "MySQL login-only test SUCCESS." "SUCCESS" $ConnectionString = "Server=$DbServer;Database=$DbName;User=$DbUser;Password=$DbPass;Port=$DbPort;default command timeout=120;" $EncPassword = Read-SecretPlain -Prompt "EncPassword" -Default $defEncPassword # ------------------------- # UPDATE CONFIG VALUES # ------------------------- $config["ElasticSearchPassword"] = $ElasticSearchPassword $config["EbridgeApiBaseAddress"] = $EbridgeApiBaseAddress $config["ApiUser"] = $ApiUser $config["ApiPassword"] = $ApiPassword $config["DeviceId"] = $DeviceId $config["ConnectionString"] = $ConnectionString $config["EncPassword"] = $EncPassword $preferredOrder = @( "LoadFormTimeout", "BulkDeleteRequestMaxSize", "ElasticSearchHost", "ElasticSearchUser", "ElasticSearchPassword", "EbridgeApiBaseAddress", "ApiUser", "ApiPassword", "SyncServiceUrl", "DeviceId", "ConnectionString", "EncPassword", "EncIterations", "SentryDsn", "InstallerHost", "LogUploadUrl", "LogUploadToken", "FormViewerPath", "ProxyUrl", "ProxyUser", "ProxyPassword", "BypassInvalidSslCertificate", "EbridgeMessages", "Cases", "Forms", "VideoCalls", "AudioCalls", "Emails", "Temp", "SyncServiceLogDirectory", "ApiLogDirectory", "FormViewerLogDirectory", "ProgramDataPath", "EnableSensitiveMySqlDataLogging", "ProductName" ) $orderedConfig = Build-OrderedConfig -AllValues $config -PreferredOrder $preferredOrder # ------------------------- # BACKUP EXISTING FILE # ------------------------- if (Test-Path -LiteralPath $SettingsPath) { try { $stamp = (Get-Date).ToString("yyyyMMdd-HHmmss") $backupPath = Join-Path $BackupDir ("settings.json.bak-" + $stamp) Copy-Item -LiteralPath $SettingsPath -Destination $backupPath -Force Log ("Backed up existing settings.json to: {0}" -f $backupPath) "INFO" } catch { Log ("Backup failed — aborting for safety: {0}" -f $_.Exception.Message) "ERROR" throw } } else { Log "No existing settings.json found; skipping backup." "INFO" } # ------------------------- # WRITE JSON # ------------------------- Write-JsonAtomic -Object $orderedConfig -DestinationPath $SettingsPath Log ("settings.json written successfully: {0}" -f $SettingsPath) "SUCCESS" } catch { Log "===== SCRIPT FAILED =====" "ERROR" Log ("Message: {0}" -f $_.Exception.Message) "ERROR" Log ("Exception Type: {0}" -f $_.Exception.GetType().FullName) "ERROR" if ($_.InvocationInfo) { Log ("At: {0} Line {1} Char {2}" -f $_.InvocationInfo.ScriptName, $_.InvocationInfo.ScriptLineNumber, $_.InvocationInfo.OffsetInLine) "ERROR" if ($_.InvocationInfo.Line) { Log ("Line: {0}" -f $_.InvocationInfo.Line.Trim()) "ERROR" } } if ($_.Exception -is [System.Reflection.ReflectionTypeLoadException]) { if ($_.Exception.LoaderExceptions) { foreach ($loaderEx in $_.Exception.LoaderExceptions) { if ($loaderEx) { Log ("LoaderException: {0}" -f $loaderEx.Message) "ERROR" } } } } if ($_.ScriptStackTrace) { Log ("Stack Trace: {0}" -f $_.ScriptStackTrace) "ERROR" } exit 1 } finally { $ScriptEnd = Get-Date $Duration = New-TimeSpan -Start $ScriptStart -End $ScriptEnd Log "===== Script Finished =====" "INFO" Log ("Execution Time: {0:hh\:mm\:ss\.fff} (hh:mm:ss.fff)" -f $Duration) "INFO" Write-Host ("LOG SAVED TO: {0}" -f $LogFile) -ForegroundColor Cyan }