Files
romexis-implant-installer/RomexisImplantInstaller.ps1
Patrick Gniza 42e37e8de7 Initiale Version des Romexis Implant Library Online Installers
- GUI zur Verwaltung von Implantbibliotheken
- Automatischer Download von Planmeca
- Integration der Original-Herstellerinstaller
- Automatisches Romexis-Backup
- Download-Cache und Updateerkennung
- Fortschrittsanzeige und Protokollierung
2026-05-31 17:35:36 +02:00

1338 lines
44 KiB
PowerShell

<#
Romexis Implant Library Online Bulk Installer GUI
Funktion:
- Laedt die Planmeca Implant Library Webseite
- Parst die verfuegbaren Downloadlinks zu content.planmeca.com
- Zeigt die Bibliotheken als auswählbare Liste
- Laedt nur die ausgewaehlten ZIPs herunter
- Entpackt jede ZIP-Datei
- Prueft die mitgelieferten Installationsskripte per SHA256 gegen bekannte kompatible Versionen
- Ruft die originalen Planmeca/Hersteller-Batchdateien auf, statt deren SQL-Logik nachzubauen
Wichtig:
- Am Romexis-Server ausfuehren.
- Romexis 3D Implant Lizenz wird von Planmeca vorausgesetzt.
- Erstellt optional vor der Installation ein SQL-Backup der Romexis-Datenbank.
- Dieses Skript ist ein Orchestrator; die eigentliche Installation bleibt bei den Originalskripten.
#>
# WinForms reicht hier aus, WPF waere etwas zu gross dafuer
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName System.Web
Add-Type -AssemblyName System.IO.Compression.FileSystem
# bei Fehler erstmal hart abbrechen
$ErrorActionPreference = 'Stop'
try {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
if ([enum]::GetNames([Net.SecurityProtocolType]) -contains 'Tls13') {
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls13
}
} catch {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
}
# Standardwerte, koennen in der GUI noch angepasst werden
$DefaultLibraryUrl = 'https://www.planmeca.com/dental-software/planmeca-romexis/3d-implantology-software/implant-library/'
$DefaultCacheDir = Join-Path $env:ProgramData 'RomexisImplantLibraryCache'
# Hashes aus Bekannten install script, bei Änderungen muss wieder geprüft werden
$KnownHashes = @{
'Install_implant.bat' = @(
'8409D680D78A51C194CBCA7EC4B69F8EAC49E63B4BCF515C55D0A170BC385599'
)
'Install_script.bat' = @(
'1A7FD946D555620260D6D58FDFF0E3F623684AE836FA496F6E8E7482286D41E4'
)
}
# kleines Logfenster unten in der Maske
function Write-UiLog {
param([string]$Text)
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
$script:txtLog.AppendText("[$timestamp] $Text`r`n")
$script:txtLog.SelectionStart = $script:txtLog.Text.Length
$script:txtLog.ScrollToCaret()
[System.Windows.Forms.Application]::DoEvents()
}
function Select-Folder {
param([string]$Description)
$dlg = New-Object System.Windows.Forms.FolderBrowserDialog
$dlg.Description = $Description
$dlg.ShowNewFolderButton = $true
if ($dlg.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { return $dlg.SelectedPath }
return $null
}
function Clean-HtmlText {
param([string]$Html)
if ([string]::IsNullOrWhiteSpace($Html)) { return '' }
$text = [regex]::Replace($Html, '<[^>]+>', ' ')
$text = [System.Web.HttpUtility]::HtmlDecode($text)
$text = [regex]::Replace($text, '\s+', ' ').Trim()
return $text
}
function Resolve-Url {
param([string]$BaseUrl, [string]$Href)
if ($Href -match '^https?://') { return $Href }
$base = [Uri]$BaseUrl
return ([Uri]::new($base, $Href)).AbsoluteUri
}
# Seite auslesen und die Downloadlinks rausfischen
# nicht schoen, aber aktuell stabil genug
function Get-PlanmecaLibraries {
param(
[string]$Url
)
$response = Invoke-WebRequest -Uri $Url -UseBasicParsing
$html = $response.Content
# Planmeca baut die Liste aktuell mit h3 + Download link auf
$pattern = '(?s)<h3>\s*(?<name>.*?)\s*</h3>.*?<a[^>]+href="(?<url>https://content\.planmeca\.com/implantlibrary/[^"]+?\.zip)"[^>]*>\s*Download library\s*</a>'
$items = @()
foreach ($m in [regex]::Matches($html, $pattern)) {
$name = [System.Net.WebUtility]::HtmlDecode($m.Groups['name'].Value).Trim()
$downloadUrl = [System.Net.WebUtility]::HtmlDecode($m.Groups['url'].Value).Trim()
if (-not [string]::IsNullOrWhiteSpace($name) -and
-not [string]::IsNullOrWhiteSpace($downloadUrl)) {
Write-UiLog "Gefunden: $name"
Write-UiLog "URL: $downloadUrl"
$items += [pscustomobject]@{
Name = $name
Url = $downloadUrl
}
}
}
$items |
Sort-Object Name -Unique
}
function Get-SafeFileName {
param([string]$Name)
$safe = $Name -replace '[\\/:*?"<>|]', '_'
$safe = $safe -replace '\s+', '_'
return $safe.Trim('_')
}
# fuer Cache/Update pruefung, wenn Planmeca die ZIP ersetzt
function Get-RemoteFileInfo {
param([string]$Url)
$request = [System.Net.HttpWebRequest]::Create($Url)
$request.Method = 'HEAD'
$request.UserAgent = 'Mozilla/5.0'
$response = $request.GetResponse()
try {
return [pscustomobject]@{
Url = $Url
ETag = $response.Headers['ETag']
LastModified = $response.LastModified.ToString('o')
ContentLength = [int64]$response.ContentLength
}
}
finally {
$response.Close()
}
}
function Get-CacheMetaPath {
param([string]$ZipPath)
return "$ZipPath.meta.json"
}
function Read-CacheMeta {
param([string]$ZipPath)
$metaPath = Get-CacheMetaPath -ZipPath $ZipPath
if (-not (Test-Path -LiteralPath $metaPath)) {
return $null
}
return Get-Content -LiteralPath $metaPath -Raw | ConvertFrom-Json
}
function Write-CacheMeta {
param(
[string]$ZipPath,
[object]$RemoteInfo
)
$metaPath = Get-CacheMetaPath -ZipPath $ZipPath
$meta = [pscustomobject]@{
Url = $RemoteInfo.Url
ETag = $RemoteInfo.ETag
LastModified = $RemoteInfo.LastModified
ContentLength = $RemoteInfo.ContentLength
DownloadedAt = (Get-Date).ToString('o')
Sha256 = (Get-FileHash -LiteralPath $ZipPath -Algorithm SHA256).Hash
}
$meta | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $metaPath -Encoding UTF8
}
# Cache nur verwenden wenn Groesse/Datum/ETag noch passen
function Test-CacheIsCurrent {
param(
[string]$ZipPath,
[object]$RemoteInfo
)
if (-not (Test-Path -LiteralPath $ZipPath)) {
return $false
}
$localFile = Get-Item -LiteralPath $ZipPath
if ($localFile.Length -le 0) {
return $false
}
$meta = Read-CacheMeta -ZipPath $ZipPath
if (-not $meta) {
return $false
}
if ($RemoteInfo.ContentLength -gt 0 -and [int64]$meta.ContentLength -ne [int64]$RemoteInfo.ContentLength) {
return $false
}
if ($RemoteInfo.ETag -and $meta.ETag -and $RemoteInfo.ETag -ne $meta.ETag) {
return $false
}
if ($RemoteInfo.LastModified -and $meta.LastModified -and $RemoteInfo.LastModified -ne $meta.LastModified) {
return $false
}
return $true
}
# eigener Download, damit der Balken in der GUI was tut
function Download-FileWithProgress {
param(
[string]$Url,
[string]$TargetFile,
[string]$DisplayName
)
$request = [System.Net.HttpWebRequest]::Create($Url)
$request.UserAgent = 'Mozilla/5.0'
$response = $request.GetResponse()
try {
$totalBytes = $response.ContentLength
$inputStream = $response.GetResponseStream()
$outputStream = [System.IO.File]::Create($TargetFile)
try {
$buffer = New-Object byte[] 1048576
$totalRead = 0
while (($read = $inputStream.Read($buffer, 0, $buffer.Length)) -gt 0) {
$outputStream.Write($buffer, 0, $read)
$totalRead += $read
if ($totalBytes -gt 0) {
$percent = [int](($totalRead / $totalBytes) * 100)
$script:progressBar.Value = [Math]::Min(100, $percent)
$script:lblProgress.Text = "Download $DisplayName - $percent% ($([math]::Round($totalRead / 1MB, 1)) / $([math]::Round($totalBytes / 1MB, 1)) MB)"
}
else {
$script:progressBar.Style = 'Marquee'
$script:lblProgress.Text = "Download $DisplayName - $([math]::Round($totalRead / 1MB, 1)) MB"
}
[System.Windows.Forms.Application]::DoEvents()
}
}
finally {
$outputStream.Close()
$inputStream.Close()
}
}
finally {
$response.Close()
$script:progressBar.Style = 'Blocks'
}
}
# Expand-Archive hat keine vernuenftige Anzeige, daher hier per ZIP API
function Expand-ZipWithProgress {
param(
[string]$ZipPath,
[string]$DestinationPath,
[string]$DisplayName
)
$zip = [System.IO.Compression.ZipFile]::OpenRead($ZipPath)
try {
$entries = @($zip.Entries)
$total = $entries.Count
$current = 0
foreach ($entry in $entries) {
$current++
$percent = [int](($current / $total) * 100)
$script:progressBar.Value = [Math]::Min(100, $percent)
$script:lblProgress.Text = "Entpacke $DisplayName - $percent% ($current / $total Dateien)"
[System.Windows.Forms.Application]::DoEvents()
$targetPath = Join-Path $DestinationPath $entry.FullName
$targetDir = Split-Path $targetPath -Parent
if (-not (Test-Path -LiteralPath $targetDir)) {
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
}
if ($entry.FullName.EndsWith('/')) {
continue
}
[System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $targetPath, $true)
}
}
finally {
$zip.Dispose()
}
}
# Download mit lokalem Cache
# alte ZIPs werden ersetzt sobald die Online Datei anders aussieht
function Download-LibraryZip {
param(
[pscustomobject]$Library,
[string]$CacheDir
)
if ([string]::IsNullOrWhiteSpace($Library.Url)) {
throw "Keine Download-URL fuer Bibliothek '$($Library.Name)' vorhanden."
}
New-Item -ItemType Directory -Path $CacheDir -Force | Out-Null
$uri = [Uri]$Library.Url
$leaf = [System.IO.Path]::GetFileName($uri.AbsolutePath)
if ([string]::IsNullOrWhiteSpace($leaf) -or $leaf -notmatch '\.zip$') {
$leaf = (Get-SafeFileName $Library.Name) + '.zip'
}
$target = Join-Path $CacheDir $leaf
$tmpFile = "$target.download"
Write-UiLog "Download: $($Library.Name)"
Write-UiLog "URL: $($Library.Url)"
Write-UiLog "Ziel: $target"
Write-UiLog "Prüfe Online-Version..."
$remoteInfo = Get-RemoteFileInfo -Url $Library.Url
Write-UiLog "Remote Größe: $($remoteInfo.ContentLength) Bytes"
Write-UiLog "Remote Last-Modified: $($remoteInfo.LastModified)"
if ($remoteInfo.ETag) {
Write-UiLog "Remote ETag: $($remoteInfo.ETag)"
}
if (Test-CacheIsCurrent -ZipPath $target -RemoteInfo $remoteInfo) {
$existingSize = (Get-Item -LiteralPath $target).Length
Write-UiLog "ZIP im Cache ist aktuell: $leaf ($existingSize Bytes)"
return $target
}
if (Test-Path -LiteralPath $target) {
Write-UiLog "Cache veraltet oder ohne Metadaten, lade neu: $leaf"
Remove-Item -LiteralPath $target -Force
}
$oldMeta = Get-CacheMetaPath -ZipPath $target
if (Test-Path -LiteralPath $oldMeta) {
Remove-Item -LiteralPath $oldMeta -Force
}
if (Test-Path -LiteralPath $tmpFile) {
Remove-Item -LiteralPath $tmpFile -Force
}
try {
Write-UiLog "Starte Download..."
$script:progressBar.Value = 0
$script:lblProgress.Text = "Download $($Library.Name) wird gestartet..."
Download-FileWithProgress `
-Url $Library.Url `
-TargetFile $tmpFile `
-DisplayName $Library.Name
Write-UiLog "Download abgeschlossen."
$script:progressBar.Value = 100
$script:lblProgress.Text = "Download $($Library.Name) abgeschlossen."
}
catch {
$script:progressBar.Value = 0
$script:lblProgress.Text = "Download fehlgeschlagen."
throw "Download fehlgeschlagen fuer '$($Library.Name)': $($_.Exception.Message)"
}
if (-not (Test-Path -LiteralPath $tmpFile)) {
throw "Download fehlgeschlagen. Temporäre Datei wurde nicht erzeugt: $tmpFile"
}
$fileSize = (Get-Item -LiteralPath $tmpFile).Length
if ($fileSize -le 0) {
Remove-Item -LiteralPath $tmpFile -Force -ErrorAction SilentlyContinue
throw "Download fehlgeschlagen. ZIP-Datei ist leer: $tmpFile"
}
Write-UiLog "Download abgeschlossen, verschiebe Datei in Cache..."
Move-Item -LiteralPath $tmpFile -Destination $target -Force
Write-UiLog "Datei im Cache gespeichert."
if (-not (Test-Path -LiteralPath $target)) {
throw "Download wurde nicht korrekt abgeschlossen. Datei fehlt: $target"
}
Write-CacheMeta -ZipPath $target -RemoteInfo $remoteInfo
$zipHash = (Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash
Write-UiLog "Download erfolgreich: $leaf ($fileSize Bytes)"
Write-UiLog "ZIP SHA256: $zipHash"
Write-UiLog "Cache-Metadaten gespeichert: $(Get-CacheMetaPath -ZipPath $target)"
return $target
}
function Get-Sha256 {
param([string]$Path)
if (-not (Test-Path -LiteralPath $Path)) { return $null }
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToUpperInvariant()
}
function Convert-ManufacturerNameFromFolder {
param([string]$FolderName, [string]$FallbackName)
if (-not [string]::IsNullOrWhiteSpace($FallbackName)) { return $FallbackName }
$name = $FolderName -replace '_', ' '
return (Get-Culture).TextInfo.ToTitleCase($name.ToLowerInvariant())
}
function Find-ImplantRoot {
param([string]$WorkDir)
return Get-ChildItem -LiteralPath $WorkDir -Directory -Recurse -Filter 'Implant_library_files' | Select-Object -First 1
}
function Get-SqlServerFiles {
param([string]$ManufacturerDir)
$sqlFiles = @(Get-ChildItem -LiteralPath $ManufacturerDir -Recurse -File -Filter '*sqlsrv.sql' | Select-Object -ExpandProperty FullName)
if ($sqlFiles.Count -eq 0) {
$sqlFiles = @(Get-ChildItem -LiteralPath $ManufacturerDir -Recurse -File -Filter '*.sql' | Where-Object { $_.Name -notmatch 'fb\.sql$' } | Select-Object -ExpandProperty FullName)
}
return @($sqlFiles)
}
# Romexis Installationspfad aus Registry holen
# die zweite GUID ist fuer alte Installationen drin geblieben
function Get-RomexisInstallDir {
$guids = @(
'{B77EAE13-6B8D-477C-93F1-9C9A9ABA4355}',
'{A9256EA8-FAD2-4B23-90A9-B78CD122C0BF}'
)
foreach ($guid in $guids) {
$key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$guid"
$keyWow = "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\$guid"
foreach ($path in @($key, $keyWow)) {
if (Test-Path $path) {
$installDir = (Get-ItemProperty $path).InstallLocation
if ($installDir -and (Test-Path $installDir)) {
return $installDir.TrimEnd('\')
}
}
}
}
throw 'Romexis Installationsverzeichnis wurde nicht gefunden.'
}
# DB Daten aus Romexis Config lesen
# moeglichst nah an den original Batchscripten
function Get-RomexisSqlConfig {
param(
[string]$RomexisInstallDir
)
$propsFile = Join-Path $RomexisInstallDir 'sconfig\romexis_server.properties'
if (-not (Test-Path -LiteralPath $propsFile)) {
throw "Romexis Server-Konfiguration nicht gefunden: $propsFile"
}
$props = @{}
Get-Content -LiteralPath $propsFile | ForEach-Object {
$line = $_.Trim()
if ($line -eq '' -or $line.StartsWith('#')) {
return
}
$idx = $line.IndexOf('=')
if ($idx -lt 1) {
return
}
$key = $line.Substring(0, $idx).Trim()
$value = $line.Substring($idx + 1).Trim()
$props[$key] = $value
}
$dbType = $props['SERVER_DB']
$dbUrl = $props['SERVER_DB_URL']
$uid = $props['SERVER_DB_UID']
$pwd = $props['SERVER_DB_PWD']
if ([string]::IsNullOrWhiteSpace($pwd)) {
$pwd = 'romexis'
}
$instance = $null
$database = $null
if ($dbType -eq '3') {
# jTDS:
# jdbc:jtds:sqlserver://SERVER/DB;instance=ROMEXIS
if ($dbUrl -match 'sqlserver://([^/;]+)/([^;]+)') {
$server = $Matches[1]
$database = $Matches[2]
}
if ($dbUrl -match 'instance=([^;]+)') {
$instance = "$server\$($Matches[1])"
}
else {
$instance = $server
}
}
elseif ($dbType -eq '5') {
# Microsoft JDBC:
# jdbc:sqlserver://SERVER\INSTANCE;databaseName=romexis_db
if ($dbUrl -match 'sqlserver://([^;]+)') {
$instance = $Matches[1]
}
if ($dbUrl -match 'databaseName=([^;]+)') {
$database = $Matches[1]
}
}
else {
throw "Unbekannter Romexis SQL-Verbindungstyp SERVER_DB=$dbType"
}
if ([string]::IsNullOrWhiteSpace($instance)) {
throw "SQL Server/Instanz konnte aus SERVER_DB_URL nicht gelesen werden: $dbUrl"
}
if ([string]::IsNullOrWhiteSpace($database)) {
throw "SQL Datenbank konnte aus SERVER_DB_URL nicht gelesen werden: $dbUrl"
}
if ([string]::IsNullOrWhiteSpace($uid)) {
throw "SQL Benutzer konnte nicht gelesen werden."
}
[pscustomobject]@{
Instance = $instance
Database = $database
User = $uid
Password = $pwd
DbType = $dbType
DbUrl = $dbUrl
}
}
# Java aus der Romexis Installation suchen
function Get-RomexisJavaPath {
param([string]$RomexisInstallDir)
$candidates = @()
if ($env:PROCESSOR_ARCHITECTURE -eq 'x86') {
$candidates += Join-Path $RomexisInstallDir 'tools\jre_x86\bin\java.exe'
}
else {
$candidates += Join-Path $RomexisInstallDir 'tools\jre_x64\bin\java.exe'
}
$candidates += Join-Path $RomexisInstallDir 'tools\jre\bin\java.exe'
foreach ($candidate in $candidates) {
if (Test-Path -LiteralPath $candidate) { return $candidate }
}
return $null
}
function Get-RomexisVersion {
param([string]$RomexisInstallDir)
$java = Get-RomexisJavaPath -RomexisInstallDir $RomexisInstallDir
$jar = Join-Path $RomexisInstallDir 'server\RomexisServer.jar'
if (-not $java -or -not (Test-Path -LiteralPath $jar)) {
return 'unknown'
}
try {
$old = Get-Location
Set-Location -LiteralPath (Split-Path $jar -Parent)
$out = & $java -jar 'RomexisServer.jar' -version 2>&1 | Out-String
Set-Location $old
if ($out -match '([0-9]+)\.([0-9]+)') {
return "$($Matches[1])$($Matches[2])"
}
}
catch {
try { Set-Location $old } catch {}
}
return 'unknown'
}
function Request-PasswordDialog {
param([string]$Title, [string]$Prompt)
$dlg = New-Object System.Windows.Forms.Form
$dlg.Text = $Title
$dlg.Size = New-Object System.Drawing.Size(430, 160)
$dlg.StartPosition = 'CenterParent'
$dlg.FormBorderStyle = 'FixedDialog'
$dlg.MaximizeBox = $false
$dlg.MinimizeBox = $false
$lbl = New-Object System.Windows.Forms.Label
$lbl.Text = $Prompt
$lbl.Location = New-Object System.Drawing.Point(12, 15)
$lbl.Size = New-Object System.Drawing.Size(390, 22)
$dlg.Controls.Add($lbl)
$txt = New-Object System.Windows.Forms.TextBox
$txt.Location = New-Object System.Drawing.Point(15, 43)
$txt.Size = New-Object System.Drawing.Size(385, 24)
$txt.UseSystemPasswordChar = $true
$dlg.Controls.Add($txt)
$ok = New-Object System.Windows.Forms.Button
$ok.Text = 'OK'
$ok.Location = New-Object System.Drawing.Point(244, 80)
$ok.Size = New-Object System.Drawing.Size(75, 28)
$ok.DialogResult = [System.Windows.Forms.DialogResult]::OK
$dlg.AcceptButton = $ok
$dlg.Controls.Add($ok)
$cancel = New-Object System.Windows.Forms.Button
$cancel.Text = 'Abbrechen'
$cancel.Location = New-Object System.Drawing.Point(325, 80)
$cancel.Size = New-Object System.Drawing.Size(75, 28)
$cancel.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
$dlg.CancelButton = $cancel
$dlg.Controls.Add($cancel)
if ($dlg.ShowDialog($form) -eq [System.Windows.Forms.DialogResult]::OK) {
return $txt.Text
}
return $null
}
function Invoke-SqlcmdWithCredential {
param(
[object]$SqlConfig,
[object]$Credential,
[string]$Database,
[string]$Query
)
$args = @('-S', $SqlConfig.Instance)
$args += $Credential.Args
if (-not [string]::IsNullOrWhiteSpace($Database)) {
$args += @('-d', $Database)
}
# WICHTIG:
# Kein "-r 1", weil SQL Server auch Erfolgsmeldungen wie
# "Processed xxxx pages..." ausgibt. Mit "-r 1" landen diese im Fehlerstrom.
$args += @('-b', '-Q', $Query)
$oldErrorActionPreference = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
try {
$output = & sqlcmd @args 2>&1
$exitCode = $LASTEXITCODE
}
finally {
$ErrorActionPreference = $oldErrorActionPreference
}
[pscustomobject]@{
ExitCode = $exitCode
Output = ($output | Out-String)
}
}
function Get-SqlCredentialCandidates {
param([object]$SqlConfig, [string]$CustomSaPassword)
$candidates = @()
$candidates += [pscustomobject]@{
Name = 'Windows Authentifizierung'
Args = @('-E')
}
if (-not [string]::IsNullOrWhiteSpace($SqlConfig.User) -and -not [string]::IsNullOrWhiteSpace($SqlConfig.Password)) {
$candidates += [pscustomobject]@{
Name = 'Romexis DB Benutzer'
Args = @('-U', $SqlConfig.User, '-P', $SqlConfig.Password)
}
}
if (-not [string]::IsNullOrWhiteSpace($CustomSaPassword)) {
$candidates += [pscustomobject]@{
Name = 'SQL sa Benutzer (eingegeben)'
Args = @('-U', 'sa', '-P', $CustomSaPassword)
}
}
$candidates += [pscustomobject]@{
Name = 'SQL sa Standard 1'
Args = @('-U', 'sa', '-P', 'pwr0mex!s')
}
$candidates += [pscustomobject]@{
Name = 'SQL sa Standard 2'
Args = @('-U', 'sa', '-P', 'Pwr0mex!s!!!')
}
return @($candidates)
}
function Get-WorkingSqlCredential {
param([object]$SqlConfig)
$testQuery = "SET NOCOUNT ON;SELECT 'Database connection OK'"
foreach ($cred in (Get-SqlCredentialCandidates -SqlConfig $SqlConfig)) {
$res = Invoke-SqlcmdWithCredential -SqlConfig $SqlConfig -Credential $cred -Database $SqlConfig.Database -Query $testQuery
if ($res.ExitCode -eq 0) {
Write-UiLog "SQL-Zugriff OK mit: $($cred.Name)"
return $cred
}
}
$custom = Request-PasswordDialog -Title 'SQL sa Passwort' -Prompt 'SQL sa Passwort eingeben, falls Standardzugänge nicht funktionieren:'
if (-not [string]::IsNullOrWhiteSpace($custom)) {
foreach ($cred in (Get-SqlCredentialCandidates -SqlConfig $SqlConfig -CustomSaPassword $custom)) {
if ($cred.Name -ne 'SQL sa Benutzer (eingegeben)') { continue }
$res = Invoke-SqlcmdWithCredential -SqlConfig $SqlConfig -Credential $cred -Database $SqlConfig.Database -Query $testQuery
if ($res.ExitCode -eq 0) {
Write-UiLog "SQL-Zugriff OK mit: $($cred.Name)"
return $cred
}
}
}
throw 'Kein funktionierender SQL-Zugang gefunden. Backup wurde nicht erstellt.'
}
function Invoke-SqlScalar {
param(
[object]$SqlConfig,
[object]$Credential,
[string]$Database,
[string]$Query
)
$args = @('-S', $SqlConfig.Instance)
$args += $Credential.Args
if (-not [string]::IsNullOrWhiteSpace($Database)) { $args += @('-d', $Database) }
$args += @('-h', '-1', '-W', '-Q', $Query)
$output = & sqlcmd @args 2>&1
if ($LASTEXITCODE -ne 0) {
throw "SQL-Abfrage fehlgeschlagen: $($output | Out-String)"
}
foreach ($line in $output) {
$value = ($line.ToString()).Trim()
if ($value -and $value -notmatch '^\([0-9]+ rows? affected\)$') { return $value }
}
return $null
}
function Escape-SqlLiteral {
param([string]$Value)
return $Value -replace "'", "''"
}
function Escape-SqlName {
param([string]$Value)
return $Value -replace ']', ']]'
}
# Sicherheitsbackup vor DB Aenderungen
# kein Ersatz fuer das normale Backupscript
function New-RomexisDatabaseBackup {
param(
[string]$RomexisInstallDir,
[object]$SqlConfig
)
if (-not (Get-Command sqlcmd -ErrorAction SilentlyContinue)) {
throw 'sqlcmd wurde nicht gefunden. Bitte SQL Server Command Line Tools installieren oder PATH prüfen.'
}
Write-UiLog 'Prüfe SQL-Zugriff für Backup...'
$cred = Get-WorkingSqlCredential -SqlConfig $SqlConfig
$romexisVersion = Get-RomexisVersion -RomexisInstallDir $RomexisInstallDir
Write-UiLog "Romexis Version fuer Backup-Dateiname: $romexisVersion"
$imageDir = Invoke-SqlScalar -SqlConfig $SqlConfig -Credential $cred -Database $SqlConfig.Database -Query 'SET NOCOUNT ON;SELECT param_value FROM RBA_Server_Param_S WHERE param_number=1'
if ([string]::IsNullOrWhiteSpace($imageDir)) {
throw 'Romexis Image-Verzeichnis konnte nicht aus der Datenbank gelesen werden.'
}
$imageDir = $imageDir -replace '/', '\'
Write-UiLog "Romexis Image Dir: $imageDir"
$backupDir = Join-Path $imageDir 'Backup'
if (-not (Test-Path -LiteralPath $backupDir)) {
New-Item -ItemType Directory -Path $backupDir -Force | Out-Null
}
$dateTime = Get-Date -Format 'yyyyMMddHHmmss'
$backupFile = Join-Path $backupDir ("$dateTime#$($SqlConfig.Database)#$romexisVersion#.BAK")
Write-UiLog "Erstelle Datenbankbackup: $backupFile"
$dbName = Escape-SqlName $SqlConfig.Database
$backupPath = Escape-SqlLiteral $backupFile
$query = "BACKUP DATABASE [$dbName] TO DISK=N'$backupPath';"
$res = Invoke-SqlcmdWithCredential -SqlConfig $SqlConfig -Credential $cred -Database '' -Query $query
if (-not [string]::IsNullOrWhiteSpace($res.Output)) {
Write-UiLog ($res.Output.Trim())
}
if ($res.ExitCode -ne 0) {
throw "Backup fehlgeschlagen. sqlcmd ExitCode: $($res.ExitCode)"
}
if (-not (Test-Path -LiteralPath $backupFile)) {
throw "Backup wurde laut sqlcmd ausgeführt, aber die Datei wurde nicht gefunden: $backupFile"
}
Write-UiLog "Backup erstellt: $backupFile"
return $backupFile
}
# Geometrie Dateien kopieren
# manche Hersteller haben implants und sleeves, andere nur eins davon
function Copy-LibraryFiles {
param(
[string]$ManufacturerDir,
[string]$RomexisInstallDir
)
$manufacturerFolder = Split-Path $ManufacturerDir -Leaf
$copyJobs = @(
[pscustomobject]@{
Name = 'Implantatdateien'
Source = Join-Path $ManufacturerDir 'implants\files'
Target = Join-Path $RomexisInstallDir "geometries\implants\$manufacturerFolder"
},
[pscustomobject]@{
Name = 'Sleeve-Dateien'
Source = Join-Path $ManufacturerDir 'sleeves\files'
Target = Join-Path $RomexisInstallDir "geometries\sleeves\$manufacturerFolder"
}
)
foreach ($job in $copyJobs) {
if (-not (Test-Path -LiteralPath $job.Source)) {
Write-UiLog "$($job.Name) nicht vorhanden, überspringe: $($job.Source)"
continue
}
Write-UiLog "Kopiere $($job.Name):"
Write-UiLog "Quelle: $($job.Source)"
Write-UiLog "Ziel: $($job.Target)"
New-Item -ItemType Directory -Path $job.Target -Force | Out-Null
Copy-Item -LiteralPath (Join-Path $job.Source '*') -Destination $job.Target -Recurse -Force
}
}
function Quote-CmdArg {
param([string]$Value)
if ($null -eq $Value) { return '""' }
return '"' + ($Value -replace '"', '\"') + '"'
}
# Wichtiger Teil:
# SQL Import nicht selbst nachbauen, sondern Hersteller Batch verwenden
function Invoke-OriginalInstaller {
param(
[string]$ImplantRoot,
[string]$ManufacturerDir,
[string]$ManufacturerName,
[string[]]$SqlFiles,
[string]$DbUser,
[string]$DbPassword,
[bool]$AllowUnknownScripts
)
$installImplant = Join-Path $ImplantRoot 'Install_implant.bat'
$installScript = Join-Path $ImplantRoot 'Install_script.bat'
if (-not (Test-Path -LiteralPath $installImplant)) {
throw 'Install_implant.bat fehlt.'
}
$hash1 = Get-Sha256 $installImplant
$hash2 = Get-Sha256 $installScript
$hashOk = (
($hash1 -and $KnownHashes['Install_implant.bat'] -contains $hash1) -or
($hash2 -and $KnownHashes['Install_script.bat'] -contains $hash2)
)
Write-UiLog "Install_implant.bat: $installImplant"
Write-UiLog "Install_implant SHA256: $hash1"
if ($hash2) {
Write-UiLog "Install_script.bat SHA256: $hash2"
}
if (-not $hashOk -and -not $AllowUnknownScripts) {
throw "Unbekannte Installer-Skriptversion. Hashes: Install_implant=$hash1 Install_script=$hash2"
}
if (-not $hashOk) {
Write-UiLog "WARNUNG: unbekannte Skript-Hashes erlaubt: $ManufacturerName"
}
if (-not $SqlFiles -or $SqlFiles.Count -eq 0) {
throw "Keine SQL-Server-SQL-Datei gefunden für $ManufacturerName."
}
Write-UiLog "Starte Originalskript fuer: $ManufacturerName"
Write-UiLog "SQL-Dateien fuer $ManufacturerName :"
foreach ($sqlFile in $SqlFiles) {
Write-UiLog " SQL: $sqlFile"
if (-not (Test-Path -LiteralPath $sqlFile)) {
throw "SQL-Datei existiert nicht: $sqlFile"
}
}
$cmdParts = @()
$cmdParts += 'call'
$cmdParts += (Quote-CmdArg $installImplant)
$cmdParts += (Quote-CmdArg $DbUser)
$cmdParts += (Quote-CmdArg $DbPassword)
$cmdParts += (Quote-CmdArg $ManufacturerName)
foreach ($sqlFile in $SqlFiles) {
$cmdParts += (Quote-CmdArg $sqlFile)
}
$cmdLine = $cmdParts -join ' '
Write-UiLog "Arbeitsverzeichnis: $ImplantRoot"
Write-UiLog "CMD: $cmdLine"
Push-Location -LiteralPath $ImplantRoot
try {
$output = & cmd.exe /d /s /c $cmdLine 2>&1
$exitCode = $LASTEXITCODE
}
finally {
Pop-Location
}
if ($output) {
Write-UiLog "Ausgabe Originalskript:"
foreach ($line in $output) {
$text = $line.ToString().TrimEnd()
if (-not [string]::IsNullOrWhiteSpace($text)) {
Write-UiLog " $text"
}
}
}
Write-UiLog "Originalskript ExitCode: $exitCode"
if ($exitCode -ne 0) {
throw "Originalskript beendet mit ExitCode $exitCode."
}
}
# Ablauf fuer eine Bibliothek: holen, entpacken, kopieren, SQL import
function Install-Library {
param(
[pscustomobject]$Library,
[string]$CacheDir,
[object]$SqlConfig,
[bool]$AllowUnknownScripts,
[bool]$CopyFiles
)
$zip = Download-LibraryZip -Library $Library -CacheDir $CacheDir
Write-UiLog "Verwende ZIP: $zip"
if (-not (Test-Path -LiteralPath $zip)) {
throw "ZIP-Datei wurde nicht gefunden: $zip"
}
$work = Join-Path $env:TEMP ('RomexisOnlineInstall_' + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $work | Out-Null
try {
Write-UiLog "Entpacke: $($Library.Name)"
$script:progressBar.Value = 0
$script:lblProgress.Text = "Entpacke $($Library.Name) ..."
Expand-ZipWithProgress `
-ZipPath $zip `
-DestinationPath $work `
-DisplayName $Library.Name
$script:progressBar.Value = 100
$script:lblProgress.Text = "Entpacken $($Library.Name) abgeschlossen."
$root = Find-ImplantRoot $work
if (-not $root) { throw 'Implant_library_files nicht gefunden.' }
$manufacturerDirs = @(Get-ChildItem -LiteralPath $root.FullName -Directory | Where-Object { $_.Name -notin @('scripts') })
if ($manufacturerDirs.Count -eq 0) { throw 'Kein Herstellerordner unter Implant_library_files gefunden.' }
foreach ($manufacturerDir in $manufacturerDirs) {
$manufacturerName = Convert-ManufacturerNameFromFolder -FolderName $manufacturerDir.Name -FallbackName $Library.Name
$sqlFiles = @(Get-SqlServerFiles -ManufacturerDir $manufacturerDir.FullName)
Write-UiLog "Herstellerordner: $($manufacturerDir.FullName)"
Write-UiLog "Gefundene SQL-Dateien: $($sqlFiles.Count)"
foreach ($sqlFile in $sqlFiles) {
Write-UiLog " gefunden: $sqlFile"
}
$romexisInstallDir = Get-RomexisInstallDir
if ($CopyFiles) {
Copy-LibraryFiles -ManufacturerDir $manufacturerDir.FullName -RomexisInstallDir $romexisInstallDir
}
Invoke-OriginalInstaller -ImplantRoot $root.FullName -ManufacturerDir $manufacturerDir.FullName -ManufacturerName $manufacturerName -SqlFiles $sqlFiles -DbUser $SqlConfig.User -DbPassword $SqlConfig.Password -AllowUnknownScripts $AllowUnknownScripts
}
Write-UiLog "OK: $($Library.Name)"
}
finally {
Remove-Item -LiteralPath $work -Recurse -Force -ErrorAction SilentlyContinue
}
}
# ---------------- GUI ----------------
# gewachsene WinForms Maske, nicht huebsch aber ok
$form = New-Object System.Windows.Forms.Form
$form.Text = 'Romexis Implant Library Online Installer'
$form.Size = New-Object System.Drawing.Size(1080, 760)
$form.StartPosition = 'CenterScreen'
$lblUrl = New-Object System.Windows.Forms.Label
$lblUrl.Text = 'Planmeca URL:'
$lblUrl.Location = New-Object System.Drawing.Point(12, 15)
$lblUrl.Size = New-Object System.Drawing.Size(100, 22)
$form.Controls.Add($lblUrl)
$txtUrl = New-Object System.Windows.Forms.TextBox
$txtUrl.Text = $DefaultLibraryUrl
$txtUrl.Location = New-Object System.Drawing.Point(115, 12)
$txtUrl.Size = New-Object System.Drawing.Size(770, 24)
$form.Controls.Add($txtUrl)
$btnLoad = New-Object System.Windows.Forms.Button
$btnLoad.Text = 'Liste laden'
$btnLoad.Location = New-Object System.Drawing.Point(895, 10)
$btnLoad.Size = New-Object System.Drawing.Size(150, 28)
$form.Controls.Add($btnLoad)
$lblCache = New-Object System.Windows.Forms.Label
$lblCache.Text = 'Download-Cache:'
$lblCache.Location = New-Object System.Drawing.Point(12, 50)
$lblCache.Size = New-Object System.Drawing.Size(100, 22)
$form.Controls.Add($lblCache)
$txtCache = New-Object System.Windows.Forms.TextBox
$txtCache.Text = $DefaultCacheDir
$txtCache.Location = New-Object System.Drawing.Point(115, 47)
$txtCache.Size = New-Object System.Drawing.Size(770, 24)
$form.Controls.Add($txtCache)
$btnCache = New-Object System.Windows.Forms.Button
$btnCache.Text = 'Auswählen'
$btnCache.Location = New-Object System.Drawing.Point(895, 45)
$btnCache.Size = New-Object System.Drawing.Size(150, 28)
$form.Controls.Add($btnCache)
$list = New-Object System.Windows.Forms.CheckedListBox
$list.Location = New-Object System.Drawing.Point(12, 85)
$list.Size = New-Object System.Drawing.Size(1033, 300)
$list.CheckOnClick = $true
$form.Controls.Add($list)
$btnAll = New-Object System.Windows.Forms.Button
$btnAll.Text = 'Alle auswählen'
$btnAll.Location = New-Object System.Drawing.Point(12, 392)
$btnAll.Size = New-Object System.Drawing.Size(130, 28)
$form.Controls.Add($btnAll)
$btnNone = New-Object System.Windows.Forms.Button
$btnNone.Text = 'Alle abwählen'
$btnNone.Location = New-Object System.Drawing.Point(150, 392)
$btnNone.Size = New-Object System.Drawing.Size(130, 28)
$form.Controls.Add($btnNone)
$chkUnknown = New-Object System.Windows.Forms.CheckBox
$chkUnknown.Text = 'Unbekannte Skript-Hashes erlauben'
$chkUnknown.Location = New-Object System.Drawing.Point(12, 433)
$chkUnknown.Size = New-Object System.Drawing.Size(320, 22)
$form.Controls.Add($chkUnknown)
$chkCopy = New-Object System.Windows.Forms.CheckBox
$chkCopy.Text = 'Implantatdateien automatisch nach Romexis kopieren'
$chkCopy.Location = New-Object System.Drawing.Point(12, 470)
$chkCopy.Size = New-Object System.Drawing.Size(420, 22)
$chkCopy.Checked = $true
$form.Controls.Add($chkCopy)
$chkBackup = New-Object System.Windows.Forms.CheckBox
$chkBackup.Text = 'Vor Installation automatisch Romexis-Datenbankbackup erstellen'
$chkBackup.Location = New-Object System.Drawing.Point(455, 470)
$chkBackup.Size = New-Object System.Drawing.Size(420, 22)
$chkBackup.Checked = $true
$form.Controls.Add($chkBackup)
$btnInstall = New-Object System.Windows.Forms.Button
$btnInstall.Text = 'Ausgewählte downloaden und installieren'
$btnInstall.Location = New-Object System.Drawing.Point(12, 505)
$btnInstall.Size = New-Object System.Drawing.Size(250, 34)
$form.Controls.Add($btnInstall)
$btnDownloadOnly = New-Object System.Windows.Forms.Button
$btnDownloadOnly.Text = 'Ausgewählte nur downloaden'
$btnDownloadOnly.Location = New-Object System.Drawing.Point(270, 505)
$btnDownloadOnly.Size = New-Object System.Drawing.Size(130, 34)
$form.Controls.Add($btnDownloadOnly)
$progressBar = New-Object System.Windows.Forms.ProgressBar
$progressBar.Location = New-Object System.Drawing.Point(420, 510)
$progressBar.Size = New-Object System.Drawing.Size(575, 22)
$progressBar.Minimum = 0
$progressBar.Maximum = 100
$progressBar.Value = 0
$form.Controls.Add($progressBar)
$script:progressBar = $progressBar
$lblProgress = New-Object System.Windows.Forms.Label
$lblProgress.Text = 'Bereit.'
$lblProgress.Location = New-Object System.Drawing.Point(420, 535)
$lblProgress.Size = New-Object System.Drawing.Size(575, 18)
$form.Controls.Add($lblProgress)
$script:lblProgress = $lblProgress
$txtLog = New-Object System.Windows.Forms.TextBox
$txtLog.Location = New-Object System.Drawing.Point(12, 560)
$txtLog.Size = New-Object System.Drawing.Size(1033, 150)
$txtLog.Multiline = $true
$txtLog.ScrollBars = 'Vertical'
$txtLog.ReadOnly = $true
$form.Controls.Add($txtLog)
$script:txtLog = $txtLog
$script:Libraries = @()
$btnCache.Add_Click({
$p = Select-Folder 'Download-Cache auswählen'
if ($p) { $txtCache.Text = $p }
})
# Nur downloaden, z.B. fuer anderen Romexis Server mitnehmen
$btnDownloadOnly.Add_Click({
try {
$btnDownloadOnly.Enabled = $false
$btnInstall.Enabled = $false
$form.Cursor = [System.Windows.Forms.Cursors]::WaitCursor
$script:progressBar.Value = 0
$script:lblProgress.Text = 'Download wird vorbereitet...'
if ($script:Libraries.Count -eq 0) {
throw 'Bitte zuerst die Liste laden.'
}
$selected = @()
foreach ($idx in $list.CheckedIndices) {
$selected += $script:Libraries[[int]$idx]
}
if ($selected.Count -eq 0) {
throw 'Keine Bibliothek ausgewählt.'
}
Write-UiLog "Nur-Download gestartet: $($selected.Count) Bibliotheken"
foreach ($lib in $selected) {
$zip = Download-LibraryZip -Library $lib -CacheDir $txtCache.Text
Write-UiLog "Gespeichert: $zip"
}
$script:progressBar.Value = 100
$script:lblProgress.Text = 'Download abgeschlossen.'
Write-UiLog 'Nur-Download fertig.'
[System.Windows.Forms.MessageBox]::Show(
'Download abgeschlossen.',
'Fertig',
'OK',
'Information'
) | Out-Null
}
catch {
$script:progressBar.Value = 0
$script:lblProgress.Text = 'Fehler beim Download.'
Write-UiLog "FEHLER Nur-Download: $($_.Exception.Message)"
[System.Windows.Forms.MessageBox]::Show(
$_.Exception.Message,
'Fehler',
'OK',
'Error'
) | Out-Null
}
finally {
$btnDownloadOnly.Enabled = $true
$btnInstall.Enabled = $true
$form.Cursor = [System.Windows.Forms.Cursors]::Default
}
})
$btnLoad.Add_Click({
try {
$list.Items.Clear()
$script:Libraries = @(Get-PlanmecaLibraries -Url $txtUrl.Text)
foreach ($lib in $script:Libraries) {
$label = $lib.Name
if (-not [string]::IsNullOrWhiteSpace($lib.Contains)) { $label += " [$($lib.Contains)]" }
[void]$list.Items.Add($label, $false)
}
Write-UiLog "$($script:Libraries.Count) Bibliotheken gefunden."
foreach ($lib in $script:Libraries) {
Write-UiLog "$($lib.Name) => $($lib.Url)"
}
} catch {
Write-UiLog "FEHLER beim Laden: $($_.Exception.Message)"
[System.Windows.Forms.MessageBox]::Show($_.Exception.Message, 'Fehler beim Laden', 'OK', 'Error') | Out-Null
}
})
$btnAll.Add_Click({ for ($i = 0; $i -lt $list.Items.Count; $i++) { $list.SetItemChecked($i, $true) } })
$btnNone.Add_Click({ for ($i = 0; $i -lt $list.Items.Count; $i++) { $list.SetItemChecked($i, $false) } })
# Hauptbutton: optional Backup, dann alle gewaehlten Bibliotheken installieren
$btnInstall.Add_Click({
try {
$btnInstall.Enabled = $false
$form.Cursor = [System.Windows.Forms.Cursors]::WaitCursor
$script:progressBar.Value = 0
$script:lblProgress.Text = 'Installation wird vorbereitet...'
if ($script:Libraries.Count -eq 0) { throw 'Bitte zuerst die Liste laden.' }
$romexisInstallDir = Get-RomexisInstallDir
$sqlConfig = Get-RomexisSqlConfig -RomexisInstallDir $romexisInstallDir
Write-UiLog "Romexis Install Dir: $romexisInstallDir"
Write-UiLog "Romexis SQL Instance: $($sqlConfig.Instance)"
Write-UiLog "Romexis SQL Database: $($sqlConfig.Database)"
Write-UiLog "Romexis SQL User: $($sqlConfig.User)"
$selected = @()
foreach ($idx in $list.CheckedIndices) { $selected += $script:Libraries[[int]$idx] }
if ($selected.Count -eq 0) { throw 'Keine Bibliothek ausgewählt.' }
$backupText = if ($chkBackup.Checked) { "Vorher wird automatisch ein Romexis-Datenbankbackup erstellt." } else { "ACHTUNG: Automatisches Backup ist deaktiviert." }
$confirm = [System.Windows.Forms.MessageBox]::Show(
"Es werden $($selected.Count) Bibliotheken heruntergeladen und am Romexis-Server installiert.`r`n`r`n$backupText`r`n`r`nFortfahren?",
'Installation starten', 'YesNo', 'Warning')
if ($confirm -ne [System.Windows.Forms.DialogResult]::Yes) { return }
if ($chkBackup.Checked) {
$backupFile = New-RomexisDatabaseBackup -RomexisInstallDir $romexisInstallDir -SqlConfig $sqlConfig
Write-UiLog "Backup vor Installation erfolgreich: $backupFile"
}
else {
Write-UiLog 'WARNUNG: Installation ohne automatisches Backup gestartet.'
}
foreach ($lib in $selected) {
Install-Library -Library $lib -CacheDir $txtCache.Text -SqlConfig $sqlConfig -AllowUnknownScripts:$chkUnknown.Checked -CopyFiles:$chkCopy.Checked
}
$script:progressBar.Value = 100
$script:lblProgress.Text = 'Installation abgeschlossen.'
Write-UiLog 'Fertig.'
[System.Windows.Forms.MessageBox]::Show('Installation abgeschlossen.', 'Fertig', 'OK', 'Information') | Out-Null
} catch {
$script:progressBar.Value = 0
$script:lblProgress.Text = 'Fehler.'
Write-UiLog "FEHLER: $($_.Exception.Message)"
[System.Windows.Forms.MessageBox]::Show($_.Exception.Message, 'Fehler', 'OK', 'Error') | Out-Null
}
finally {
$btnInstall.Enabled = $true
$form.Cursor = [System.Windows.Forms.Cursors]::Default
}
})
# beim Start gleich versuchen die Romexis Daten und die Onlineliste zu laden
$form.Add_Shown({
try {
$romexisInstallDir = Get-RomexisInstallDir
$sqlConfig = Get-RomexisSqlConfig -RomexisInstallDir $romexisInstallDir
Write-UiLog "Romexis Install Dir: $romexisInstallDir"
Write-UiLog "Romexis SQL Instance: $($sqlConfig.Instance)"
Write-UiLog "Romexis SQL Database: $($sqlConfig.Database)"
Write-UiLog "Romexis SQL User: $($sqlConfig.User)"
}
catch {
Write-UiLog "WARNUNG: Romexis SQL-Konfiguration konnte nicht automatisch gelesen werden: $($_.Exception.Message)"
}
Write-UiLog 'Lade Bibliotheksliste automatisch...'
$btnLoad.PerformClick()
})
[void]$form.ShowDialog()