first commit
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,794 @@
|
||||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Installiert bzw. entfernt das Firefox-Add-on "Swyx Tab Bridge" auf einem
|
||||
Windows-Rechner. Laeuft lokal oder ueber SSH (siehe deploy-windows.sh).
|
||||
|
||||
.DESCRIPTION
|
||||
Zwei Installationsarten:
|
||||
|
||||
-Mode Policy (Default, empfohlen fuer den Dauerbetrieb)
|
||||
Schreibt eine Enterprise-Policy nach <FirefoxDir>\distribution\policies.json
|
||||
mit installation_mode = force_installed. Das Add-on wird beim naechsten
|
||||
Firefox-Start automatisch, ohne Rueckfrage und fuer alle Profile des
|
||||
Rechners installiert und kann vom Benutzer nicht deaktiviert werden.
|
||||
Benoetigt Schreibrecht im Firefox-Installationsverzeichnis (bei einer
|
||||
systemweiten Installation also Administratorrechte, bei einer
|
||||
Benutzerinstallation unter %LOCALAPPDATA% keine) und ein signiertes .xpi.
|
||||
|
||||
-Mode Profile
|
||||
Legt das .xpi als <Profil>\extensions\<ExtensionId>.xpi ab. Ohne
|
||||
Adminrechte moeglich, wirkt nur auf die Profile des Zielbenutzers, und
|
||||
Firefox fragt beim naechsten Start einmal nach Bestaetigung.
|
||||
|
||||
Firefox Release und ESR installieren nur signierte Add-ons. Ein unsigniertes
|
||||
Paket laesst sich nur mit -AllowUnsigned und nur auf Firefox ESR, Developer
|
||||
Edition oder Nightly betreiben; -AllowUnsigned hinterlegt dafuer eine
|
||||
autoconfig-Datei, die xpinstall.signatures.required auf false sperrt.
|
||||
|
||||
.PARAMETER XpiPath
|
||||
Pfad zum .xpi (lokal auf dem Windows-Rechner). Pflicht ausser bei -Uninstall.
|
||||
|
||||
.PARAMETER Mode
|
||||
Policy (Default) oder Profile.
|
||||
|
||||
.PARAMETER ExtensionId
|
||||
Add-on-ID, muss zu browser_specific_settings.gecko.id im Manifest passen.
|
||||
|
||||
.PARAMETER FirefoxDir
|
||||
Installationsverzeichnis von Firefox. Wird normalerweise automatisch erkannt;
|
||||
nur noetig, wenn mehrere Installationen gefunden werden.
|
||||
|
||||
.PARAMETER TargetUser
|
||||
Benutzer, dessen Profile bei -Mode Profile bespielt werden. Default: der
|
||||
aktuelle Benutzer. Fremde Benutzer erfordern Adminrechte.
|
||||
|
||||
.PARAMETER AllowUnsigned
|
||||
Signaturpruefung per autoconfig abschalten (nur ESR / Developer Edition).
|
||||
|
||||
.PARAMETER StopFirefox
|
||||
Laufende firefox.exe-Prozesse beenden, damit die Installation sofort greift.
|
||||
Firefox stellt die Sitzung beim naechsten Start wieder her.
|
||||
|
||||
.PARAMETER Uninstall
|
||||
Policy-Eintrag, hinterlegtes .xpi und Profil-Kopien wieder entfernen.
|
||||
|
||||
.EXAMPLE
|
||||
.\Install-SwyxTabBridge.ps1 -XpiPath .\swyx_tab_bridge-1.0.0.xpi
|
||||
|
||||
.EXAMPLE
|
||||
.\Install-SwyxTabBridge.ps1 -XpiPath .\swyx.xpi -Mode Profile -StopFirefox
|
||||
|
||||
.EXAMPLE
|
||||
.\Install-SwyxTabBridge.ps1 -Uninstall
|
||||
|
||||
.NOTES
|
||||
Exitcodes: 0 = Erfolg, 1 = Fehler, 2 = Erfolg, aber Firefox-Neustart noetig.
|
||||
Die Datei ist bewusst rein ASCII, damit Windows PowerShell 5.1 sie unabhaengig
|
||||
von der Codepage korrekt liest.
|
||||
#>
|
||||
|
||||
[CmdletBinding(SupportsShouldProcess)]
|
||||
param(
|
||||
[string]$XpiPath,
|
||||
|
||||
[ValidateSet('Policy', 'Profile')]
|
||||
[string]$Mode = 'Policy',
|
||||
|
||||
[string]$ExtensionId = 'swyx-tab-bridge@appcreation.de',
|
||||
|
||||
[string]$FirefoxDir,
|
||||
|
||||
[string]$TargetUser = $env:USERNAME,
|
||||
|
||||
[switch]$AllowUnsigned,
|
||||
|
||||
[switch]$StopFirefox,
|
||||
|
||||
[switch]$Uninstall
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
# Ablageort des .xpi fuer den Policy-Modus. Muss fuer alle Benutzer lesbar sein,
|
||||
# weil Firefox die Datei bei jedem Profil-Start ueber die file:-URL liest.
|
||||
$Script:XpiStore = Join-Path $env:ProgramData 'SwyxTabBridge'
|
||||
$Script:RestartNeeded = $false
|
||||
|
||||
# ------------------------------------------------------------------ Ausgabe
|
||||
|
||||
# Ausgabe bewusst ueber [Console] statt Write-Host: bei umgeleitetem stdout -
|
||||
# genau der Fall bei "ssh host powershell ..." - serialisiert Windows PowerShell
|
||||
# den Information-Stream sonst als CLIXML und die Ausgabe wird unlesbar.
|
||||
function Write-Line { param([string]$Message = '') [Console]::Out.WriteLine($Message) }
|
||||
function Write-Step { param([string]$Message) Write-Line ''; Write-Line "==> $Message" }
|
||||
function Write-Info { param([string]$Message) Write-Line " $Message" }
|
||||
function Write-Good { param([string]$Message) Write-Line " [ok] $Message" }
|
||||
function Write-Note { param([string]$Message) Write-Line " [!] $Message" }
|
||||
|
||||
# ------------------------------------------------------------------ Helfer
|
||||
|
||||
function Test-Administrator {
|
||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
|
||||
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
}
|
||||
|
||||
# Massgeblich ist nicht "ist der Benutzer Admin", sondern "darf hier geschrieben
|
||||
# werden": eine Firefox-Benutzerinstallation unter %LOCALAPPDATA% laesst sich
|
||||
# ohne jedes Sonderrecht mit einer Policy versehen.
|
||||
function Test-DirectoryWritable {
|
||||
param([string]$Path)
|
||||
|
||||
$probeRoot = $Path
|
||||
while ($probeRoot -and -not (Test-Path -LiteralPath $probeRoot)) {
|
||||
$probeRoot = Split-Path -Parent $probeRoot
|
||||
}
|
||||
if (-not $probeRoot) { return $false }
|
||||
|
||||
$probe = Join-Path $probeRoot ([System.IO.Path]::GetRandomFileName())
|
||||
try {
|
||||
[System.IO.File]::WriteAllText($probe, 'probe')
|
||||
Remove-Item -LiteralPath $probe -Force
|
||||
return $true
|
||||
} catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function ConvertTo-FileUrl {
|
||||
param([string]$Path)
|
||||
return ([uri](Resolve-Path -LiteralPath $Path).ProviderPath).AbsoluteUri
|
||||
}
|
||||
|
||||
# JSON -> verschachtelte Hashtables. ConvertFrom-Json liefert PSCustomObjects,
|
||||
# die sich nicht sinnvoll zusammenfuehren lassen; -AsHashtable gibt es erst ab
|
||||
# PowerShell 6, hier laeuft aber oft noch Windows PowerShell 5.1.
|
||||
function ConvertTo-HashtableDeep {
|
||||
param($InputObject)
|
||||
|
||||
if ($null -eq $InputObject) { return $null }
|
||||
|
||||
if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) {
|
||||
$list = @()
|
||||
foreach ($item in $InputObject) { $list += ,(ConvertTo-HashtableDeep $item) }
|
||||
return ,$list
|
||||
}
|
||||
|
||||
if ($InputObject -is [psobject] -and $InputObject.PSObject.Properties.Name.Count -gt 0 -and
|
||||
$InputObject.GetType().Name -eq 'PSCustomObject') {
|
||||
$map = @{}
|
||||
foreach ($property in $InputObject.PSObject.Properties) {
|
||||
$map[$property.Name] = ConvertTo-HashtableDeep $property.Value
|
||||
}
|
||||
return $map
|
||||
}
|
||||
|
||||
return $InputObject
|
||||
}
|
||||
|
||||
function Write-JsonFile {
|
||||
param([string]$Path, $Data)
|
||||
$json = $Data | ConvertTo-Json -Depth 32
|
||||
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
||||
[System.IO.File]::WriteAllText($Path, $json, $utf8NoBom)
|
||||
}
|
||||
|
||||
function Backup-File {
|
||||
param([string]$Path)
|
||||
if (-not (Test-Path -LiteralPath $Path)) { return }
|
||||
$stamp = (Get-Date).ToString('yyyyMMdd-HHmmss')
|
||||
$backup = "$Path.bak-$stamp"
|
||||
Copy-Item -LiteralPath $Path -Destination $backup -Force
|
||||
Write-Info "Sicherung: $backup"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ Firefox finden
|
||||
|
||||
function Get-FirefoxInstallation {
|
||||
param([string]$Explicit)
|
||||
|
||||
if ($Explicit) {
|
||||
$exe = Join-Path $Explicit 'firefox.exe'
|
||||
if (-not (Test-Path -LiteralPath $exe)) {
|
||||
throw "In '$Explicit' liegt keine firefox.exe."
|
||||
}
|
||||
return @( New-FirefoxInfo -Directory $Explicit )
|
||||
}
|
||||
|
||||
$directories = New-Object System.Collections.Generic.List[string]
|
||||
|
||||
# Registrierung: HK{LM,CU}\SOFTWARE\[WOW6432Node\]Mozilla\<Produkt>\<Version>\Main
|
||||
# HKCU deckt Benutzerinstallationen nach %LOCALAPPDATA% ab - der Standard,
|
||||
# wenn Firefox ohne Adminrechte installiert wurde.
|
||||
foreach ($root in @('HKLM:\SOFTWARE\Mozilla', 'HKLM:\SOFTWARE\WOW6432Node\Mozilla',
|
||||
'HKCU:\SOFTWARE\Mozilla', 'HKCU:\SOFTWARE\WOW6432Node\Mozilla')) {
|
||||
if (-not (Test-Path -LiteralPath $root)) { continue }
|
||||
foreach ($product in (Get-ChildItem -LiteralPath $root -ErrorAction SilentlyContinue)) {
|
||||
foreach ($version in (Get-ChildItem -LiteralPath $product.PSPath -ErrorAction SilentlyContinue)) {
|
||||
$main = Join-Path $version.PSPath 'Main'
|
||||
if (-not (Test-Path -LiteralPath $main)) { continue }
|
||||
$value = (Get-ItemProperty -LiteralPath $main -ErrorAction SilentlyContinue).'Install Directory'
|
||||
if ($value -and (Test-Path -LiteralPath (Join-Path $value 'firefox.exe'))) {
|
||||
$directories.Add($value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Fallback: Standardpfade, inklusive Benutzerinstallation in %LOCALAPPDATA%
|
||||
foreach ($base in @($env:ProgramFiles, ${env:ProgramFiles(x86)}, $env:LOCALAPPDATA)) {
|
||||
if (-not $base) { continue }
|
||||
foreach ($name in @('Mozilla Firefox', 'Firefox Developer Edition', 'Firefox Nightly')) {
|
||||
$candidate = Join-Path $base $name
|
||||
if (Test-Path -LiteralPath (Join-Path $candidate 'firefox.exe')) {
|
||||
$directories.Add($candidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$unique = $directories | Sort-Object -Unique
|
||||
return @($unique | ForEach-Object { New-FirefoxInfo -Directory $_ })
|
||||
}
|
||||
|
||||
function New-FirefoxInfo {
|
||||
param([string]$Directory)
|
||||
|
||||
$exe = Join-Path $Directory 'firefox.exe'
|
||||
$info = (Get-Item -LiteralPath $exe).VersionInfo
|
||||
$product = $info.ProductName
|
||||
$version = $info.ProductVersion
|
||||
|
||||
# Der Kanal steht nirgends direkt; application.ini verraet ihn indirekt.
|
||||
# Reihenfolge ist wichtig: Developer Edition wird aus mozilla-beta gebaut,
|
||||
# erlaubt aber - anders als Beta - unsignierte Add-ons. Deshalb hat der
|
||||
# RemotingName (dort "firefox-dev") das letzte Wort.
|
||||
$channel = 'release'
|
||||
$appIni = Join-Path $Directory 'application.ini'
|
||||
if (Test-Path -LiteralPath $appIni) {
|
||||
$content = Get-Content -LiteralPath $appIni -Raw
|
||||
|
||||
if ($content -match '(?m)^SourceRepository=.*mozilla-esr') { $channel = 'esr' }
|
||||
elseif ($content -match '(?m)^SourceRepository=.*mozilla-beta') { $channel = 'beta' }
|
||||
elseif ($content -match '(?m)^SourceRepository=.*mozilla-central') { $channel = 'nightly' }
|
||||
|
||||
if ($content -match '(?m)^RemotingName=(.+)$') {
|
||||
$remoting = $Matches[1].Trim()
|
||||
if ($remoting -match '(?i)dev') { $channel = 'developer' }
|
||||
elseif ($remoting -match '(?i)nightly') { $channel = 'nightly' }
|
||||
}
|
||||
}
|
||||
|
||||
return [pscustomobject]@{
|
||||
Directory = $Directory
|
||||
Exe = $exe
|
||||
Product = $product
|
||||
Version = $version
|
||||
Channel = $channel
|
||||
}
|
||||
}
|
||||
|
||||
function Select-FirefoxInstallation {
|
||||
param([string]$Explicit)
|
||||
|
||||
$found = @(Get-FirefoxInstallation -Explicit $Explicit)
|
||||
|
||||
if ($found.Count -eq 0) {
|
||||
throw "Keine Firefox-Installation gefunden. Pfad mit -FirefoxDir angeben."
|
||||
}
|
||||
if ($found.Count -gt 1) {
|
||||
Write-Note "Mehrere Firefox-Installationen gefunden:"
|
||||
foreach ($item in $found) { Write-Note " $($item.Directory) ($($item.Product) $($item.Version))" }
|
||||
throw "Bitte die gewuenschte Installation mit -FirefoxDir angeben."
|
||||
}
|
||||
|
||||
return $found[0]
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ Profile finden
|
||||
|
||||
function Get-FirefoxProfile {
|
||||
param([string]$User)
|
||||
|
||||
if ($User -eq $env:USERNAME) {
|
||||
$roaming = $env:APPDATA
|
||||
} else {
|
||||
$roaming = Join-Path (Join-Path $env:SystemDrive "Users\$User") 'AppData\Roaming'
|
||||
}
|
||||
|
||||
$profilesIni = Join-Path $roaming 'Mozilla\Firefox\profiles.ini'
|
||||
if (-not (Test-Path -LiteralPath $profilesIni)) {
|
||||
throw "Keine profiles.ini fuer Benutzer '$User' gefunden ($profilesIni). Firefox muss dort mindestens einmal gestartet worden sein."
|
||||
}
|
||||
|
||||
$firefoxRoot = Split-Path -Parent $profilesIni
|
||||
$section = $null
|
||||
$sections = @{}
|
||||
|
||||
foreach ($line in (Get-Content -LiteralPath $profilesIni)) {
|
||||
$line = $line.Trim()
|
||||
if ($line -match '^\[(.+)\]$') {
|
||||
$section = $Matches[1]
|
||||
$sections[$section] = @{}
|
||||
continue
|
||||
}
|
||||
if ($section -and $line -match '^([^=]+)=(.*)$') {
|
||||
$sections[$section][$Matches[1].Trim()] = $Matches[2].Trim()
|
||||
}
|
||||
}
|
||||
|
||||
$result = @()
|
||||
foreach ($name in $sections.Keys) {
|
||||
if ($name -notlike 'Profile*') { continue }
|
||||
$entry = $sections[$name]
|
||||
if (-not $entry.ContainsKey('Path')) { continue }
|
||||
|
||||
$path = $entry['Path'] -replace '/', '\'
|
||||
if ($entry.ContainsKey('IsRelative') -and $entry['IsRelative'] -eq '0') {
|
||||
$full = $path
|
||||
} else {
|
||||
$full = Join-Path $firefoxRoot $path
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $full)) { continue }
|
||||
|
||||
$displayName = $name
|
||||
if ($entry.ContainsKey('Name')) { $displayName = $entry['Name'] }
|
||||
|
||||
$result += [pscustomobject]@{
|
||||
Name = $displayName
|
||||
Path = $full
|
||||
IsDefault = ($entry.ContainsKey('Default') -and $entry['Default'] -eq '1')
|
||||
}
|
||||
}
|
||||
|
||||
if ($result.Count -eq 0) {
|
||||
throw "In '$profilesIni' ist kein existierendes Profil eingetragen."
|
||||
}
|
||||
return $result
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ XPI pruefen
|
||||
|
||||
function Get-XpiInfo {
|
||||
param([string]$Path)
|
||||
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem | Out-Null
|
||||
$archive = [System.IO.Compression.ZipFile]::OpenRead($Path)
|
||||
try {
|
||||
$signed = $false
|
||||
foreach ($entry in $archive.Entries) {
|
||||
if ($entry.FullName -like 'META-INF/*.rsa' -or $entry.FullName -like 'META-INF/*.RSA') {
|
||||
$signed = $true
|
||||
}
|
||||
}
|
||||
|
||||
$manifestEntry = $archive.Entries | Where-Object { $_.FullName -eq 'manifest.json' } | Select-Object -First 1
|
||||
if (-not $manifestEntry) {
|
||||
throw "'$Path' enthaelt keine manifest.json auf oberster Ebene. Beim Packen muss der *Inhalt* des Ordners gezippt werden, nicht der Ordner selbst."
|
||||
}
|
||||
|
||||
$reader = New-Object System.IO.StreamReader($manifestEntry.Open())
|
||||
try { $manifestJson = $reader.ReadToEnd() } finally { $reader.Dispose() }
|
||||
$manifest = $manifestJson | ConvertFrom-Json
|
||||
|
||||
$id = $null
|
||||
if ($manifest.PSObject.Properties.Name -contains 'browser_specific_settings') {
|
||||
$id = $manifest.browser_specific_settings.gecko.id
|
||||
} elseif ($manifest.PSObject.Properties.Name -contains 'applications') {
|
||||
$id = $manifest.applications.gecko.id
|
||||
}
|
||||
|
||||
return [pscustomobject]@{
|
||||
Id = $id
|
||||
Name = $manifest.name
|
||||
Version = $manifest.version
|
||||
Signed = $signed
|
||||
}
|
||||
} finally {
|
||||
$archive.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ Policy
|
||||
|
||||
function Set-ExtensionPolicy {
|
||||
param(
|
||||
[string]$InstallDirectory,
|
||||
[string]$Id,
|
||||
[string]$InstallUrl
|
||||
)
|
||||
|
||||
$distribution = Join-Path $InstallDirectory 'distribution'
|
||||
$policyFile = Join-Path $distribution 'policies.json'
|
||||
|
||||
if (-not (Test-Path -LiteralPath $distribution)) {
|
||||
New-Item -ItemType Directory -Path $distribution -Force | Out-Null
|
||||
}
|
||||
|
||||
$document = @{}
|
||||
if (Test-Path -LiteralPath $policyFile) {
|
||||
Backup-File -Path $policyFile
|
||||
$raw = Get-Content -LiteralPath $policyFile -Raw
|
||||
if ($raw.Trim()) {
|
||||
$document = ConvertTo-HashtableDeep ($raw | ConvertFrom-Json)
|
||||
}
|
||||
}
|
||||
|
||||
if (-not ($document -is [hashtable])) { $document = @{} }
|
||||
if (-not $document.ContainsKey('policies') -or -not ($document['policies'] -is [hashtable])) {
|
||||
$document['policies'] = @{}
|
||||
}
|
||||
$policies = $document['policies']
|
||||
|
||||
if (-not $policies.ContainsKey('ExtensionSettings') -or -not ($policies['ExtensionSettings'] -is [hashtable])) {
|
||||
$policies['ExtensionSettings'] = @{}
|
||||
}
|
||||
|
||||
$policies['ExtensionSettings'][$Id] = @{
|
||||
installation_mode = 'force_installed'
|
||||
install_url = $InstallUrl
|
||||
updates_disabled = $true
|
||||
}
|
||||
|
||||
if ($PSCmdlet.ShouldProcess($policyFile, "ExtensionSettings fuer $Id schreiben")) {
|
||||
Write-JsonFile -Path $policyFile -Data $document
|
||||
Write-Good "Policy geschrieben: $policyFile"
|
||||
}
|
||||
}
|
||||
|
||||
function Remove-ExtensionPolicy {
|
||||
param([string]$InstallDirectory, [string]$Id)
|
||||
|
||||
$policyFile = Join-Path (Join-Path $InstallDirectory 'distribution') 'policies.json'
|
||||
if (-not (Test-Path -LiteralPath $policyFile)) {
|
||||
Write-Info "Keine policies.json vorhanden - nichts zu entfernen."
|
||||
return
|
||||
}
|
||||
|
||||
$raw = Get-Content -LiteralPath $policyFile -Raw
|
||||
if (-not $raw.Trim()) { return }
|
||||
|
||||
$document = ConvertTo-HashtableDeep ($raw | ConvertFrom-Json)
|
||||
if (-not ($document -is [hashtable]) -or -not $document.ContainsKey('policies')) { return }
|
||||
|
||||
$policies = $document['policies']
|
||||
if (-not ($policies -is [hashtable]) -or -not $policies.ContainsKey('ExtensionSettings')) {
|
||||
Write-Info "Kein ExtensionSettings-Block - nichts zu entfernen."
|
||||
return
|
||||
}
|
||||
|
||||
$settings = $policies['ExtensionSettings']
|
||||
if (-not $settings.ContainsKey($Id)) {
|
||||
Write-Info "Kein Eintrag fuer $Id - nichts zu entfernen."
|
||||
return
|
||||
}
|
||||
|
||||
Backup-File -Path $policyFile
|
||||
$settings.Remove($Id)
|
||||
|
||||
# Leere Container nicht stehen lassen.
|
||||
if ($settings.Count -eq 0) { $policies.Remove('ExtensionSettings') }
|
||||
|
||||
if ($PSCmdlet.ShouldProcess($policyFile, "Eintrag $Id entfernen")) {
|
||||
if ($policies.Count -eq 0) {
|
||||
Remove-Item -LiteralPath $policyFile -Force
|
||||
Write-Good "policies.json enthielt nur diesen Eintrag und wurde geloescht."
|
||||
} else {
|
||||
Write-JsonFile -Path $policyFile -Data $document
|
||||
Write-Good "Eintrag aus $policyFile entfernt."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ Signaturpflicht
|
||||
|
||||
function Set-SignatureEnforcement {
|
||||
param([string]$InstallDirectory, [bool]$Required)
|
||||
|
||||
$prefDirectory = Join-Path $InstallDirectory 'defaults\pref'
|
||||
$autoconfigJs = Join-Path $prefDirectory 'autoconfig.js'
|
||||
$configCfg = Join-Path $InstallDirectory 'swyx-tab-bridge.cfg'
|
||||
|
||||
if ($Required) {
|
||||
foreach ($file in @($autoconfigJs, $configCfg)) {
|
||||
if (Test-Path -LiteralPath $file) {
|
||||
Remove-Item -LiteralPath $file -Force
|
||||
Write-Good "Entfernt: $file"
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $prefDirectory)) {
|
||||
New-Item -ItemType Directory -Path $prefDirectory -Force | Out-Null
|
||||
}
|
||||
|
||||
$autoconfigContent = @'
|
||||
// Swyx Tab Bridge - autoconfig aktivieren
|
||||
pref("general.config.filename", "swyx-tab-bridge.cfg");
|
||||
pref("general.config.obscure_value", 0);
|
||||
pref("general.config.sandbox_enabled", false);
|
||||
'@
|
||||
|
||||
# Die erste Zeile einer .cfg wird von Firefox immer ignoriert - deshalb der Kommentar.
|
||||
$configContent = @'
|
||||
// Swyx Tab Bridge
|
||||
lockPref("xpinstall.signatures.required", false);
|
||||
'@
|
||||
|
||||
if ($PSCmdlet.ShouldProcess($InstallDirectory, "Signaturpruefung per autoconfig abschalten")) {
|
||||
$ascii = New-Object System.Text.ASCIIEncoding
|
||||
[System.IO.File]::WriteAllText($autoconfigJs, $autoconfigContent, $ascii)
|
||||
[System.IO.File]::WriteAllText($configCfg, $configContent, $ascii)
|
||||
Write-Good "Signaturpruefung abgeschaltet ($configCfg)"
|
||||
}
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ Firefox-Prozess
|
||||
|
||||
# Stehen mehrere Firefox-Installationen nebeneinander, laufen sie alle als
|
||||
# firefox.exe. Beruecksichtigt wird deshalb nur, was aus dem Zielverzeichnis
|
||||
# gestartet wurde - eine daneben laufende zweite Installation bleibt in Ruhe.
|
||||
function Get-FirefoxProcess {
|
||||
param([string]$InstallDirectory)
|
||||
|
||||
$all = @(Get-Process -Name 'firefox' -ErrorAction SilentlyContinue)
|
||||
if (-not $InstallDirectory) { return $all }
|
||||
|
||||
$matching = @()
|
||||
foreach ($process in $all) {
|
||||
$path = $null
|
||||
try { $path = $process.Path } catch { $path = $null }
|
||||
# Ohne lesbaren Pfad (fremder Benutzer) im Zweifel nicht anfassen.
|
||||
if ($path -and $path.StartsWith($InstallDirectory, [StringComparison]::OrdinalIgnoreCase)) {
|
||||
$matching += $process
|
||||
}
|
||||
}
|
||||
return $matching
|
||||
}
|
||||
|
||||
function Stop-FirefoxProcess {
|
||||
param([string]$InstallDirectory)
|
||||
|
||||
$running = @(Get-FirefoxProcess -InstallDirectory $InstallDirectory)
|
||||
if ($running.Count -eq 0) {
|
||||
Write-Info "Aus diesem Verzeichnis laeuft kein Firefox."
|
||||
return $false
|
||||
}
|
||||
|
||||
if ($PSCmdlet.ShouldProcess("firefox.exe ($($running.Count) Prozesse)", "beenden")) {
|
||||
$running | Stop-Process -Force
|
||||
Start-Sleep -Seconds 2
|
||||
Write-Good "Firefox beendet ($($running.Count) Prozesse) - die Sitzung wird beim naechsten Start wiederhergestellt."
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
function Test-FirefoxRunning {
|
||||
param([string]$InstallDirectory)
|
||||
return @(Get-FirefoxProcess -InstallDirectory $InstallDirectory).Count -gt 0
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ Ergebnis pruefen
|
||||
|
||||
function Get-InstalledAddonState {
|
||||
param([string]$ProfilePath, [string]$Id)
|
||||
|
||||
$extensionsJson = Join-Path $ProfilePath 'extensions.json'
|
||||
if (-not (Test-Path -LiteralPath $extensionsJson)) { return $null }
|
||||
|
||||
try {
|
||||
$data = Get-Content -LiteralPath $extensionsJson -Raw | ConvertFrom-Json
|
||||
} catch {
|
||||
return $null
|
||||
}
|
||||
|
||||
if (-not ($data.PSObject.Properties.Name -contains 'addons')) { return $null }
|
||||
|
||||
$addon = $data.addons | Where-Object { $_.id -eq $Id } | Select-Object -First 1
|
||||
if (-not $addon) { return $null }
|
||||
|
||||
return [pscustomobject]@{
|
||||
Version = $addon.version
|
||||
Active = [bool]$addon.active
|
||||
Location = $addon.location
|
||||
}
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ Ablauf
|
||||
|
||||
function Invoke-Install {
|
||||
param([string]$Xpi)
|
||||
|
||||
Write-Step "Paket pruefen"
|
||||
if (-not (Test-Path -LiteralPath $Xpi)) { throw "Datei nicht gefunden: $Xpi" }
|
||||
$Xpi = (Resolve-Path -LiteralPath $Xpi).ProviderPath
|
||||
|
||||
$xpiInfo = Get-XpiInfo -Path $Xpi
|
||||
Write-Info "Paket: $($xpiInfo.Name) $($xpiInfo.Version)"
|
||||
Write-Info "ID: $($xpiInfo.Id)"
|
||||
Write-Info "Signiert: $(if ($xpiInfo.Signed) { 'ja' } else { 'nein' })"
|
||||
|
||||
if ($xpiInfo.Id -and $xpiInfo.Id -ne $ExtensionId) {
|
||||
throw "ID im Paket ('$($xpiInfo.Id)') passt nicht zu -ExtensionId ('$ExtensionId')."
|
||||
}
|
||||
if (-not $xpiInfo.Id) {
|
||||
throw "Im Manifest fehlt browser_specific_settings.gecko.id - ohne feste ID laesst sich das Add-on nicht per Policy verwalten."
|
||||
}
|
||||
|
||||
Write-Step "Firefox suchen"
|
||||
$firefox = Select-FirefoxInstallation -Explicit $FirefoxDir
|
||||
Write-Info "$($firefox.Product) $($firefox.Version) [$($firefox.Channel)]"
|
||||
Write-Info "$($firefox.Directory)"
|
||||
|
||||
if (-not $xpiInfo.Signed -and -not $AllowUnsigned) {
|
||||
throw "Das Paket ist nicht signiert. Firefox installiert es so nicht. Entweder mit 'web-ext sign --channel=unlisted' signieren, oder auf ESR/Developer Edition -AllowUnsigned verwenden."
|
||||
}
|
||||
# Nur ESR, Developer Edition und Nightly werten xpinstall.signatures.required
|
||||
# ueberhaupt aus; Release und Beta erzwingen die Signatur fest im Build.
|
||||
if ($AllowUnsigned -and @('release', 'beta') -contains $firefox.Channel) {
|
||||
Write-Note "Achtung: Diese Installation ist Firefox $($firefox.Channel). Dort wird xpinstall.signatures.required ignoriert - ein unsigniertes Add-on laeuft trotz -AllowUnsigned nicht."
|
||||
}
|
||||
|
||||
if ($AllowUnsigned) {
|
||||
Write-Step "Signaturpruefung abschalten"
|
||||
if (-not (Test-DirectoryWritable $firefox.Directory)) {
|
||||
throw "Kein Schreibrecht auf '$($firefox.Directory)'. -AllowUnsigned erfordert Administratorrechte, wenn Firefox systemweit installiert ist."
|
||||
}
|
||||
Set-SignatureEnforcement -InstallDirectory $firefox.Directory -Required $false
|
||||
$Script:RestartNeeded = $true
|
||||
}
|
||||
|
||||
if ($Mode -eq 'Policy') {
|
||||
Write-Step "Installation per Enterprise-Policy"
|
||||
if (-not (Test-DirectoryWritable $firefox.Directory)) {
|
||||
throw "Kein Schreibrecht auf '$($firefox.Directory)\distribution'. Bei systemweiter Installation Administratorrechte verwenden, sonst -Mode Profile."
|
||||
}
|
||||
|
||||
# Bei einer Benutzerinstallation ist %ProgramData% nicht beschreibbar -
|
||||
# dann liegt das Paket im Benutzerprofil.
|
||||
if (-not (Test-DirectoryWritable $Script:XpiStore)) {
|
||||
$Script:XpiStore = Join-Path $env:LOCALAPPDATA 'SwyxTabBridge'
|
||||
Write-Info "Kein Schreibrecht in ProgramData - Ablage im Benutzerprofil."
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $Script:XpiStore)) {
|
||||
New-Item -ItemType Directory -Path $Script:XpiStore -Force | Out-Null
|
||||
}
|
||||
$target = Join-Path $Script:XpiStore "$ExtensionId.xpi"
|
||||
Copy-Item -LiteralPath $Xpi -Destination $target -Force
|
||||
Write-Info "Paket abgelegt: $target"
|
||||
|
||||
Set-ExtensionPolicy -InstallDirectory $firefox.Directory -Id $ExtensionId -InstallUrl (ConvertTo-FileUrl $target)
|
||||
$Script:RestartNeeded = $true
|
||||
|
||||
} else {
|
||||
Write-Step "Installation in die Profile von '$TargetUser'"
|
||||
$profiles = Get-FirefoxProfile -User $TargetUser
|
||||
|
||||
foreach ($profileEntry in $profiles) {
|
||||
$extensionsDir = Join-Path $profileEntry.Path 'extensions'
|
||||
if (-not (Test-Path -LiteralPath $extensionsDir)) {
|
||||
New-Item -ItemType Directory -Path $extensionsDir -Force | Out-Null
|
||||
}
|
||||
$target = Join-Path $extensionsDir "$ExtensionId.xpi"
|
||||
if ($PSCmdlet.ShouldProcess($target, 'Add-on kopieren')) {
|
||||
Copy-Item -LiteralPath $Xpi -Destination $target -Force
|
||||
Write-Good "$($profileEntry.Name): $target"
|
||||
}
|
||||
}
|
||||
$Script:RestartNeeded = $true
|
||||
Write-Note "Firefox fragt beim naechsten Start einmal, ob das Add-on aktiviert werden soll."
|
||||
}
|
||||
|
||||
Write-Step "Firefox"
|
||||
if ($StopFirefox) {
|
||||
Stop-FirefoxProcess -InstallDirectory $firefox.Directory | Out-Null
|
||||
$Script:RestartNeeded = $false
|
||||
} elseif (Test-FirefoxRunning -InstallDirectory $firefox.Directory) {
|
||||
Write-Note "Firefox laeuft. Das Add-on wird erst nach einem Neustart des Browsers aktiv (oder Skript mit -StopFirefox aufrufen)."
|
||||
} else {
|
||||
Write-Info "Firefox laeuft nicht - das Add-on wird beim naechsten Start aktiv."
|
||||
}
|
||||
|
||||
Write-Step "Ergebnis"
|
||||
Write-Info "Ein Nachweis der aktiven Installation steht erst nach dem naechsten Firefox-Start in extensions.json."
|
||||
try {
|
||||
foreach ($profileEntry in (Get-FirefoxProfile -User $TargetUser)) {
|
||||
$state = Get-InstalledAddonState -ProfilePath $profileEntry.Path -Id $ExtensionId
|
||||
if ($state) {
|
||||
Write-Good "$($profileEntry.Name): Version $($state.Version), aktiv=$($state.Active), Quelle=$($state.Location)"
|
||||
} else {
|
||||
Write-Info "$($profileEntry.Name): noch nicht eingetragen (erwartet vor dem ersten Start)"
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Write-Info "Profilstatus nicht lesbar: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
Write-Line
|
||||
Write-Good "Fertig. Danach im Firefox pruefen: about:addons, und about:policies#active fuer die Policy."
|
||||
Write-Info "Server-Adresse (Default ws://127.0.0.1:17655) ggf. ueber das Symbolleisten-Icon -> Einstellungen anpassen."
|
||||
}
|
||||
|
||||
function Invoke-Uninstall {
|
||||
Write-Step "Firefox suchen"
|
||||
$firefox = Select-FirefoxInstallation -Explicit $FirefoxDir
|
||||
Write-Info "$($firefox.Product) $($firefox.Version) - $($firefox.Directory)"
|
||||
|
||||
Write-Step "Policy entfernen"
|
||||
if (Test-DirectoryWritable $firefox.Directory) {
|
||||
Remove-ExtensionPolicy -InstallDirectory $firefox.Directory -Id $ExtensionId
|
||||
Set-SignatureEnforcement -InstallDirectory $firefox.Directory -Required $true
|
||||
} else {
|
||||
Write-Note "Kein Schreibrecht auf '$($firefox.Directory)' - Policy bleibt unveraendert."
|
||||
}
|
||||
|
||||
Write-Step "Hinterlegtes Paket entfernen"
|
||||
$found = $false
|
||||
foreach ($store in @($Script:XpiStore, (Join-Path $env:LOCALAPPDATA 'SwyxTabBridge'))) {
|
||||
$stored = Join-Path $store "$ExtensionId.xpi"
|
||||
if (Test-Path -LiteralPath $stored) {
|
||||
Remove-Item -LiteralPath $stored -Force
|
||||
Write-Good "Geloescht: $stored"
|
||||
$found = $true
|
||||
}
|
||||
}
|
||||
if (-not $found) { Write-Info "Kein hinterlegtes Paket gefunden." }
|
||||
|
||||
Write-Step "Profil-Kopien entfernen"
|
||||
try {
|
||||
foreach ($profileEntry in (Get-FirefoxProfile -User $TargetUser)) {
|
||||
$target = Join-Path (Join-Path $profileEntry.Path 'extensions') "$ExtensionId.xpi"
|
||||
if (Test-Path -LiteralPath $target) {
|
||||
Remove-Item -LiteralPath $target -Force
|
||||
Write-Good "$($profileEntry.Name): geloescht"
|
||||
} else {
|
||||
Write-Info "$($profileEntry.Name): nichts vorhanden"
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Write-Info "Profile nicht lesbar: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
if ($StopFirefox) {
|
||||
Stop-FirefoxProcess -InstallDirectory $firefox.Directory | Out-Null
|
||||
} else {
|
||||
$Script:RestartNeeded = Test-FirefoxRunning -InstallDirectory $firefox.Directory
|
||||
}
|
||||
Write-Line
|
||||
Write-Good "Deinstallation abgeschlossen."
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ Einstieg
|
||||
|
||||
try {
|
||||
Write-Line "Swyx Tab Bridge - Windows-Installation"
|
||||
Write-Info "Rechner: $env:COMPUTERNAME Benutzer: $env:USERNAME Admin: $(if (Test-Administrator) { 'ja' } else { 'nein' })"
|
||||
|
||||
if ($Uninstall) {
|
||||
Invoke-Uninstall
|
||||
} else {
|
||||
if (-not $XpiPath) { throw "-XpiPath fehlt (Pfad zum .xpi auf diesem Rechner)." }
|
||||
Invoke-Install -Xpi $XpiPath
|
||||
}
|
||||
|
||||
# Der Exitcode allein traegt nicht weit genug: laeuft das Skript ueber SSH,
|
||||
# reicht die Login-Shell auf dem Windows-Host alles ausser 0 als 1 durch.
|
||||
# Deshalb steht das Ergebnis zusaetzlich als Markerzeile in der Ausgabe.
|
||||
if ($Script:RestartNeeded) {
|
||||
Write-Note "Firefox muss noch neu gestartet werden."
|
||||
Write-Line "SWYX-STATUS: RESTART"
|
||||
exit 2
|
||||
}
|
||||
Write-Line "SWYX-STATUS: OK"
|
||||
exit 0
|
||||
|
||||
} catch {
|
||||
Write-Line
|
||||
Write-Line "FEHLER: $($_.Exception.Message)"
|
||||
if ($_.ScriptStackTrace) { Write-Verbose $_.ScriptStackTrace }
|
||||
Write-Line "SWYX-STATUS: ERROR"
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
# Windows-Rollout
|
||||
|
||||
Zwei Skripte:
|
||||
|
||||
| Datei | Laeuft auf | Zweck |
|
||||
|-----------------------------|------------|-----------------------------------------------------------|
|
||||
| `deploy-windows.sh` | macOS/Linux| Version hochzaehlen, `.xpi` bauen, per SSH ausrollen |
|
||||
| `Install-SwyxTabBridge.ps1` | Windows | Die eigentliche Installation; auch allein verwendbar |
|
||||
|
||||
## Schnellstart
|
||||
|
||||
```bash
|
||||
./deploy/deploy-windows.sh
|
||||
```
|
||||
|
||||
Ohne Parameter: Version hochzaehlen, bei AMO signieren, auf den Testrechner
|
||||
uebertragen, Policy schreiben und den laufenden Firefox beenden, damit die
|
||||
Installation sofort greift. Die Voreinstellungen stehen als Konstanten oben im
|
||||
Skript:
|
||||
|
||||
| Konstante | Wert |
|
||||
|--------------------------|-----------------------------------------------|
|
||||
| `DEFAULT_HOST` | `swyx-dev` (Alias aus `~/.ssh/config`) |
|
||||
| `DEFAULT_FIREFOX_DIR` | `C:\Users\dev\AppData\Local\Mozilla Firefox` |
|
||||
| `DEFAULT_SIGN` | `true` |
|
||||
| `DEFAULT_ALLOW_UNSIGNED` | `false` |
|
||||
| `DEFAULT_STOP_FIREFOX` | `true` |
|
||||
|
||||
Ziel ist die **Release**-Installation, und die nimmt ausschliesslich signierte
|
||||
Add-ons — deshalb signiert jeder Lauf. Das kostet pro Aufruf eine Versionsnummer
|
||||
bei AMO; die wird ohnehin bei jedem Lauf hochgezaehlt.
|
||||
|
||||
Der Firefox-Pfad steht fest, weil auf dem Rechner zusaetzlich die Developer
|
||||
Edition liegt und die automatische Suche bei zwei Funden abbricht. Fuer einen
|
||||
anderen Rechner:
|
||||
|
||||
```bash
|
||||
./deploy/deploy-windows.sh --host admin@ws-042 --firefox-dir auto
|
||||
```
|
||||
|
||||
Ein bereits signiertes Paket laesst sich ohne neuen AMO-Durchlauf erneut
|
||||
ausrollen:
|
||||
|
||||
```bash
|
||||
./deploy/deploy-windows.sh --xpi build/56806207b7434031914b-1.0.16.xpi --no-bump
|
||||
```
|
||||
|
||||
Jeder Aufruf zaehlt die Version in `manifest.json` hoch (Default: Patch-Stelle).
|
||||
Das ist keine Kosmetik: Firefox installiert ein Paket mit gleicher oder
|
||||
kleinerer Version nicht erneut, und AMO nimmt eine Version nur einmal zum
|
||||
Signieren an. Steuern laesst es sich mit `--bump major|minor|patch|build|none`,
|
||||
`--set-version X.Y.Z` oder `--no-bump`.
|
||||
|
||||
## Die Signatur ist der Knackpunkt
|
||||
|
||||
Firefox **Release und Beta** installieren ausschliesslich signierte Add-ons —
|
||||
`xpinstall.signatures.required` wird dort ignoriert. Damit bleiben zwei Wege:
|
||||
|
||||
**Signieren (empfohlen, funktioniert ueberall).** Kanal `unlisted` =
|
||||
Selbst-Hosting ohne Store-Eintrag:
|
||||
|
||||
```bash
|
||||
./deploy/deploy-windows.sh --firefox-dir auto --require-signed --sign
|
||||
```
|
||||
|
||||
Die Zugangsdaten liegen in `deploy/.amo-credentials` (Konto *AppCreation GmbH*)
|
||||
und werden vom Skript selbst eingelesen. Die Datei traegt `chmod 600`, steht in
|
||||
`.gitignore` und ist nicht Teil des Pakets — der Schluessel gilt fuer das
|
||||
**gesamte AMO-Konto**, nicht nur fuer dieses Add-on. Neu erzeugen laesst er sich
|
||||
unter addons.mozilla.org → *Tools* → *Manage API Keys* → *Revoke and regenerate
|
||||
credentials*.
|
||||
|
||||
Bereits gesetzte Umgebungsvariablen haben Vorrang, ein einzelner Lauf laesst sich
|
||||
also uebersteuern:
|
||||
|
||||
```bash
|
||||
WEB_EXT_API_KEY="user:…" WEB_EXT_API_SECRET="…" ./deploy/deploy-windows.sh --sign
|
||||
```
|
||||
|
||||
Die `gecko.id` im Manifest muss ueber alle Versionen stabil bleiben, und AMO nimmt
|
||||
jede Versionsnummer nur einmal an — dafuer zaehlt das Skript bei jedem Aufruf hoch.
|
||||
|
||||
**Unsigniert (nur ESR, Developer Edition, Nightly).** Mit `--no-sign
|
||||
--allow-unsigned` hinterlegt das Skript eine autoconfig-Datei, die
|
||||
`xpinstall.signatures.required` auf `false` sperrt. Auf Firefox Release oder Beta
|
||||
warnt es, weil die Einstellung dort wirkungslos bleibt. Fuer den normalen Betrieb
|
||||
wird das nicht gebraucht.
|
||||
|
||||
Das Manifest fuehrt ausserdem `data_collection_permissions` mit
|
||||
`required: ["browsingActivity"]` — seit dem 3. November 2025 Pflicht fuer neue
|
||||
Erweiterungen auf AMO. Begruendung siehe Haupt-README.
|
||||
|
||||
## Policy oder Profil
|
||||
|
||||
`--mode policy` (Default) schreibt `<FirefoxDir>\distribution\policies.json` mit
|
||||
`installation_mode: force_installed`. Das Add-on wird beim naechsten Start ohne
|
||||
Rueckfrage fuer alle Profile installiert und laesst sich nicht deaktivieren.
|
||||
Vorhandene Policies bleiben erhalten (die Datei wird zusammengefuehrt und vorher
|
||||
gesichert). Das `.xpi` landet in `%ProgramData%\SwyxTabBridge\`, bei einer
|
||||
Benutzerinstallation in `%LOCALAPPDATA%\SwyxTabBridge\`.
|
||||
|
||||
`--mode profile` legt das `.xpi` direkt in `<Profil>\extensions\` ab. Das wirkt
|
||||
nur fuer die Profile eines Benutzers, und Firefox fragt beim naechsten Start
|
||||
einmal nach Bestaetigung.
|
||||
|
||||
Massgeblich fuer die Rechte ist nicht "Admin ja/nein", sondern das Schreibrecht
|
||||
im Firefox-Verzeichnis: eine Benutzerinstallation unter `%LOCALAPPDATA%\Mozilla
|
||||
Firefox` laesst sich ohne jedes Sonderrecht mit einer Policy versehen, eine
|
||||
Installation unter `C:\Program Files` braucht Administratorrechte.
|
||||
|
||||
## Laufender Firefox
|
||||
|
||||
Voreingestellt beendet das Skript den laufenden Firefox, damit die Installation
|
||||
sofort greift statt erst beim naechsten Neustart; `--keep-firefox` laesst ihn in
|
||||
Ruhe. Beendet wird dabei nur, was aus dem Zielverzeichnis gestartet wurde —
|
||||
liegen wie auf dem Testrechner zwei Installationen nebeneinander, laufen beide
|
||||
als `firefox.exe`, und die nicht adressierte bleibt offen. Firefox stellt die
|
||||
Sitzung beim naechsten Start wieder her.
|
||||
|
||||
## SSH ohne Passwort
|
||||
|
||||
Der Testrechner ist bereits auf Schluessel-Anmeldung eingerichtet:
|
||||
|
||||
- Schluessel: `~/.ssh/id_ed25519_swyx_deploy` (ohne Passphrase, damit das
|
||||
Deployment ohne Rueckfrage durchlaeuft)
|
||||
- `~/.ssh/config` enthaelt den Alias `swyx-dev` mit Benutzer und Schluessel
|
||||
- Auf dem Windows-Rechner liegt der oeffentliche Schluessel in
|
||||
`C:\ProgramData\ssh\administrators_authorized_keys`
|
||||
|
||||
Der letzte Punkt ist die uebliche Stolperfalle: fuer Mitglieder der
|
||||
Administratorengruppe liest der Windows-SSH-Dienst **nicht**
|
||||
`~/.ssh/authorized_keys`, sondern jene zentrale Datei — und nur dann, wenn deren
|
||||
ACL auf SYSTEM und Administratoren beschraenkt ist:
|
||||
|
||||
```powershell
|
||||
icacls C:\ProgramData\ssh\administrators_authorized_keys `
|
||||
/inheritance:r /grant "*S-1-5-18:F" /grant "*S-1-5-32-544:F"
|
||||
```
|
||||
|
||||
Die SIDs statt der Namen, damit es auf deutschem Windows genauso greift.
|
||||
|
||||
Fuer einen Rechner ohne Schluessel oder mit Jumphost laesst sich ein eigener
|
||||
Aufrufer davorhaengen:
|
||||
|
||||
```bash
|
||||
SSH_CMD="sshpass -e ssh" SCP_CMD="sshpass -e scp" SSHPASS='…' \
|
||||
./deploy/deploy-windows.sh --host dev@192.168.180.135
|
||||
```
|
||||
|
||||
Die Kommandos gehen als UTF-16LE-Base64 (`powershell -EncodedCommand`) ueber die
|
||||
Leitung. Dadurch ist egal, ob auf dem Ziel `cmd.exe` oder PowerShell die
|
||||
Standard-Shell ist, und es gibt kein Quoting-Problem mit Leerzeichen oder
|
||||
Backslashes.
|
||||
|
||||
## Weitere Optionen
|
||||
|
||||
```
|
||||
--no-sign nicht signieren (nur mit --allow-unsigned sinnvoll)
|
||||
--keep-firefox laufenden Firefox nicht beenden
|
||||
--firefox-dir Installationsverzeichnis vorgeben (bei mehreren Installationen noetig)
|
||||
--target-user Windows-Benutzer fuer --mode profile
|
||||
--uninstall Policy, hinterlegtes Paket und Profil-Kopien entfernen
|
||||
--build-only nur bauen
|
||||
--dry-run nur anzeigen, was passieren wuerde
|
||||
```
|
||||
|
||||
Exitcodes von `Install-SwyxTabBridge.ps1`: `0` Erfolg, `1` Fehler, `2` Erfolg,
|
||||
aber Firefox muss noch neu gestartet werden.
|
||||
|
||||
## Danach pruefen
|
||||
|
||||
- `about:addons` — ist das Add-on da und aktiv?
|
||||
- `about:policies#active` — hat Firefox die Policy gelesen?
|
||||
- Symbolleisten-Icon: kein Badge = verbunden, `…` = Verbindungsaufbau, `!` = getrennt.
|
||||
- Die Server-Adresse (Default `ws://127.0.0.1:17655`) liegt in
|
||||
`browser.storage.local` und laesst sich von aussen nicht vorbelegen — bei
|
||||
abweichender Adresse einmal ueber *Einstellungen* im Popup setzen.
|
||||
Executable
+458
@@ -0,0 +1,458 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Baut das Add-on "Swyx Tab Bridge" und installiert es per SSH auf einem
|
||||
# Windows-Rechner mit Firefox.
|
||||
#
|
||||
# Ablauf:
|
||||
# 1. Version in manifest.json hochzaehlen (immer, ausser --no-bump)
|
||||
# 2. .xpi packen, optional per web-ext signieren
|
||||
# 3. .xpi + Install-SwyxTabBridge.ps1 per scp uebertragen
|
||||
# 4. Install-SwyxTabBridge.ps1 per ssh mit PowerShell ausfuehren
|
||||
#
|
||||
# Beispiele:
|
||||
# ./deploy/deploy-windows.sh # signieren + ausrollen, ohne Parameter
|
||||
# ./deploy/deploy-windows.sh --keep-firefox # laufenden Firefox nicht beenden
|
||||
# ./deploy/deploy-windows.sh --uninstall
|
||||
# ./deploy/deploy-windows.sh --xpi build/xyz-1.0.16.xpi --no-bump
|
||||
# # bereits signiertes Paket erneut ausrollen
|
||||
# ./deploy/deploy-windows.sh --host admin@ws-042 --firefox-dir auto
|
||||
# # anderer Rechner, Firefox selbst suchen
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SRC_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
MANIFEST="$SRC_DIR/manifest.json"
|
||||
BUILD_DIR="$SRC_DIR/build"
|
||||
PS_SCRIPT="$SCRIPT_DIR/Install-SwyxTabBridge.ps1"
|
||||
EXTENSION_ID="swyx-tab-bridge@appcreation.de"
|
||||
|
||||
# Inhalte des Add-ons - bewusst explizit, damit weder test-server/ noch build/
|
||||
# noch .DS_Store im Paket landen.
|
||||
PACKAGE_FILES=(manifest.json background.js popup.html popup.js options.html options.js icons)
|
||||
|
||||
# Voreinstellungen fuer den Windows-Testrechner. Der Alias 'swyx-dev' steht in
|
||||
# ~/.ssh/config und bringt Benutzer und Schluessel mit; --host bzw.
|
||||
# --firefox-dir ueberschreiben das fuer andere Rechner.
|
||||
DEFAULT_HOST="swyx-dev"
|
||||
|
||||
# Ziel ist die Release-Installation. Sie liegt als Benutzerinstallation unter
|
||||
# %LOCALAPPDATA% und laesst sich damit ohne Administratorrechte bespielen. Der
|
||||
# Pfad steht fest, weil auf dem Rechner zusaetzlich die Developer Edition
|
||||
# installiert ist und die automatische Suche bei zwei Funden abbricht.
|
||||
DEFAULT_FIREFOX_DIR='C:\Users\dev\AppData\Local\Mozilla Firefox'
|
||||
|
||||
# Firefox Release erzwingt die Signatur fest im Build. Jeder Rollout muss also
|
||||
# ueber AMO signiert werden, und an der Signaturpruefung wird nichts gedreht.
|
||||
DEFAULT_SIGN=true
|
||||
DEFAULT_ALLOW_UNSIGNED=false
|
||||
# Beendet wird nur, was aus DEFAULT_FIREFOX_DIR gestartet wurde; eine daneben
|
||||
# laufende zweite Installation bleibt offen. Firefox stellt die Sitzung beim
|
||||
# naechsten Start wieder her.
|
||||
DEFAULT_STOP_FIREFOX=true
|
||||
|
||||
HOST="$DEFAULT_HOST"
|
||||
SSH_PORT=""
|
||||
MODE="Policy"
|
||||
BUMP="patch"
|
||||
FORCED_VERSION=""
|
||||
XPI_OVERRIDE=""
|
||||
REMOTE_DIR="swyx-deploy"
|
||||
DO_SIGN=$DEFAULT_SIGN
|
||||
ALLOW_UNSIGNED=$DEFAULT_ALLOW_UNSIGNED
|
||||
STOP_FIREFOX=$DEFAULT_STOP_FIREFOX
|
||||
UNINSTALL=false
|
||||
FIREFOX_DIR="$DEFAULT_FIREFOX_DIR"
|
||||
TARGET_USER=""
|
||||
DRY_RUN=false
|
||||
BUILD_ONLY=false
|
||||
|
||||
# ------------------------------------------------------------------ Ausgabe
|
||||
|
||||
c_step() { printf '\n\033[36m==> %s\033[0m\n' "$*"; }
|
||||
c_info() { printf ' %s\n' "$*"; }
|
||||
c_good() { printf ' \033[32m%s\033[0m\n' "$*"; }
|
||||
c_note() { printf ' \033[33m%s\033[0m\n' "$*"; }
|
||||
c_fail() { printf '\n\033[31mFEHLER: %s\033[0m\n' "$*" >&2; exit 1; }
|
||||
|
||||
usage() {
|
||||
# Kopfkommentar ab Zeile 3 ausgeben, bis die erste Nicht-Kommentarzeile kommt.
|
||||
awk 'NR > 2 { if ($0 !~ /^#/) exit; sub(/^# ?/, ""); print }' "${BASH_SOURCE[0]}"
|
||||
cat <<'EOF'
|
||||
|
||||
Optionen:
|
||||
--host USER@HOST Ziel-Rechner (Default: swyx-dev aus ~/.ssh/config)
|
||||
--port N SSH-Port
|
||||
--mode policy|profile Installationsart (Default: policy; braucht Schreibrecht
|
||||
im Firefox-Installationsverzeichnis)
|
||||
--bump LEVEL major | minor | patch | build | none (Default: patch)
|
||||
--set-version X.Y.Z Version fest vorgeben statt hochzuzaehlen
|
||||
--no-bump Version unveraendert lassen (Kurzform fuer --bump none)
|
||||
--xpi DATEI Fertiges (z.B. signiertes) .xpi verwenden, nicht bauen
|
||||
--sign Per 'web-ext sign --channel=unlisted' bei AMO signieren.
|
||||
Voreingestellt an, weil Firefox Release nur signierte
|
||||
Add-ons installiert. Jeder Lauf verbraucht dabei eine
|
||||
Versionsnummer bei AMO
|
||||
--no-sign Nicht signieren (nur fuer ESR/Developer Edition sinnvoll,
|
||||
dann zusammen mit --allow-unsigned)
|
||||
--allow-unsigned Signaturpflicht auf dem Ziel abschalten (nur ESR/Dev).
|
||||
Voreingestellt aus
|
||||
--require-signed Signaturpflicht auf dem Ziel unangetastet lassen (Default)
|
||||
--stop-firefox Laufenden Firefox auf dem Ziel beenden, damit die
|
||||
Installation sofort greift. Voreingestellt an
|
||||
--keep-firefox Laufenden Firefox nicht anfassen
|
||||
--firefox-dir PFAD Firefox-Installationsverzeichnis auf dem Ziel
|
||||
(Default: C:\Users\dev\AppData\Local\Mozilla Firefox;
|
||||
"auto" laesst das Skript selbst suchen)
|
||||
--target-user NAME Windows-Benutzer fuer --mode profile
|
||||
--remote-dir NAME Uebertragungsordner im Home des SSH-Benutzers
|
||||
--uninstall Add-on und Policy auf dem Ziel entfernen
|
||||
--build-only Nur bauen, nichts uebertragen
|
||||
--dry-run Nur anzeigen, was passieren wuerde
|
||||
-h, --help Diese Hilfe
|
||||
|
||||
Umgebungsvariablen:
|
||||
SSH_CMD / SCP_CMD Eigener Aufrufer statt 'ssh'/'scp', z.B. fuer
|
||||
Passwort-Login: SSH_CMD="sshpass -e ssh"
|
||||
SCP_CMD="sshpass -e scp" SSHPASS=geheim ...
|
||||
WEB_EXT_API_KEY / WEB_EXT_API_SECRET AMO-Zugangsdaten fuer --sign. Werden
|
||||
sonst aus deploy/.amo-credentials gelesen
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ Argumente
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--host) HOST="${2:?}"; shift 2 ;;
|
||||
--port) SSH_PORT="${2:?}"; shift 2 ;;
|
||||
--mode) MODE="${2:?}"; shift 2 ;;
|
||||
--bump) BUMP="${2:?}"; shift 2 ;;
|
||||
--set-version) FORCED_VERSION="${2:?}"; shift 2 ;;
|
||||
--no-bump) BUMP="none"; shift ;;
|
||||
--xpi) XPI_OVERRIDE="${2:?}"; shift 2 ;;
|
||||
--sign) DO_SIGN=true; shift ;;
|
||||
--no-sign) DO_SIGN=false; shift ;;
|
||||
--allow-unsigned) ALLOW_UNSIGNED=true; shift ;;
|
||||
--require-signed) ALLOW_UNSIGNED=false; shift ;;
|
||||
--stop-firefox) STOP_FIREFOX=true; shift ;;
|
||||
--keep-firefox) STOP_FIREFOX=false; shift ;;
|
||||
--firefox-dir) FIREFOX_DIR="${2:?}"; shift 2
|
||||
[[ "$FIREFOX_DIR" == "auto" ]] && FIREFOX_DIR="" ;;
|
||||
--target-user) TARGET_USER="${2:?}"; shift 2 ;;
|
||||
--remote-dir) REMOTE_DIR="${2:?}"; shift 2 ;;
|
||||
--uninstall) UNINSTALL=true; shift ;;
|
||||
--build-only) BUILD_ONLY=true; shift ;;
|
||||
--dry-run) DRY_RUN=true; shift ;;
|
||||
-h|--help) usage ;;
|
||||
*) c_fail "Unbekannte Option: $1 (--help fuer die Uebersicht)" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$(printf '%s' "$MODE" | tr '[:upper:]' '[:lower:]')" in
|
||||
policy) MODE="Policy" ;;
|
||||
profile) MODE="Profile" ;;
|
||||
*) c_fail "--mode muss 'policy' oder 'profile' sein." ;;
|
||||
esac
|
||||
|
||||
case "$BUMP" in
|
||||
major|minor|patch|build|none) ;;
|
||||
*) c_fail "--bump muss major, minor, patch, build oder none sein." ;;
|
||||
esac
|
||||
|
||||
if ! $BUILD_ONLY && [[ -z "$HOST" ]]; then
|
||||
c_fail "Kein Ziel-Rechner: --host angeben."
|
||||
fi
|
||||
|
||||
SSH_OPTS=()
|
||||
SCP_OPTS=()
|
||||
if [[ -n "$SSH_PORT" ]]; then
|
||||
SSH_OPTS+=(-p "$SSH_PORT")
|
||||
SCP_OPTS+=(-P "$SSH_PORT")
|
||||
fi
|
||||
|
||||
# AMO-Zugangsdaten fuer --sign. Bereits gesetzte Umgebungsvariablen haben
|
||||
# Vorrang, damit sich der Schluessel fuer einen einzelnen Lauf uebersteuern laesst.
|
||||
AMO_CREDENTIALS="$SCRIPT_DIR/.amo-credentials"
|
||||
if [[ -f "$AMO_CREDENTIALS" ]]; then
|
||||
if [[ -z "${WEB_EXT_API_KEY:-}" || -z "${WEB_EXT_API_SECRET:-}" ]]; then
|
||||
set -a
|
||||
# shellcheck source=/dev/null
|
||||
. "$AMO_CREDENTIALS"
|
||||
set +a
|
||||
fi
|
||||
# Der Schluessel gilt fuer das ganze AMO-Konto - er hat auf einer
|
||||
# mitlesbaren Datei nichts verloren.
|
||||
CRED_PERMS="$(stat -f '%Lp' "$AMO_CREDENTIALS" 2>/dev/null || stat -c '%a' "$AMO_CREDENTIALS" 2>/dev/null || echo '')"
|
||||
if [[ -n "$CRED_PERMS" && "${CRED_PERMS: -2}" != "00" ]]; then
|
||||
c_note "$AMO_CREDENTIALS ist auch fuer andere lesbar - 'chmod 600' empfohlen."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Normalfall ist Anmeldung per Schluessel. Wer Passwort-Login oder einen
|
||||
# Jumphost braucht, haengt ueber SSH_CMD/SCP_CMD einen eigenen Aufrufer davor:
|
||||
# SSH_CMD="sshpass -e ssh" SCP_CMD="sshpass -e scp" SSHPASS=... ./deploy-windows.sh ...
|
||||
read -r -a SSH_BIN <<< "${SSH_CMD:-ssh}"
|
||||
read -r -a SCP_BIN <<< "${SCP_CMD:-scp}"
|
||||
|
||||
# ------------------------------------------------------------------ Hilfsfunktionen
|
||||
|
||||
# PowerShell-Kommando als UTF-16LE-Base64 uebergeben. Damit ist voellig egal,
|
||||
# ob auf dem Ziel cmd.exe oder PowerShell die Standard-Shell ist - es gibt kein
|
||||
# Quoting-Problem mit Leerzeichen, Backslashes oder Anfuehrungszeichen.
|
||||
encode_ps() {
|
||||
printf '%s' "$1" | iconv -f UTF-8 -t UTF-16LE | base64 | tr -d '\n'
|
||||
}
|
||||
|
||||
run_remote_ps() {
|
||||
local command="$1"
|
||||
local encoded
|
||||
# Ohne das schiebt Windows PowerShell beim Nachladen von Modulen einen
|
||||
# Progress-Record als CLIXML in den Ausgabestrom.
|
||||
encoded="$(encode_ps "\$ProgressPreference = 'SilentlyContinue'
|
||||
$command")"
|
||||
|
||||
if $DRY_RUN; then
|
||||
c_info "[dry-run] ${SSH_BIN[*]} ${SSH_OPTS[*]:-} $HOST powershell -EncodedCommand <${#encoded} Zeichen>"
|
||||
c_info "[dry-run] ${command//$'\n'/ }"
|
||||
return 0
|
||||
fi
|
||||
|
||||
"${SSH_BIN[@]}" ${SSH_OPTS[@]+"${SSH_OPTS[@]}"} "$HOST" powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand "$encoded"
|
||||
}
|
||||
|
||||
# Ein Argument fuer die PowerShell-Kommandozeile in einfache Anfuehrungszeichen setzen.
|
||||
ps_quote() {
|
||||
printf "'%s'" "${1//\'/\'\'}"
|
||||
}
|
||||
|
||||
read_manifest_version() {
|
||||
python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["version"])' "$MANIFEST"
|
||||
}
|
||||
|
||||
# Version in manifest.json ersetzen, ohne die Formatierung der Datei anzutasten.
|
||||
write_manifest_version() {
|
||||
python3 - "$MANIFEST" "$1" <<'PY'
|
||||
import re, sys
|
||||
|
||||
path, version = sys.argv[1], sys.argv[2]
|
||||
with open(path, encoding="utf-8") as handle:
|
||||
text = handle.read()
|
||||
|
||||
new_text, count = re.subn(
|
||||
r'("version"\s*:\s*)"[^"]*"',
|
||||
lambda m: '%s"%s"' % (m.group(1), version),
|
||||
text,
|
||||
count=1,
|
||||
)
|
||||
if count != 1:
|
||||
sys.exit("Feld 'version' nicht in %s gefunden." % path)
|
||||
|
||||
with open(path, "w", encoding="utf-8") as handle:
|
||||
handle.write(new_text)
|
||||
PY
|
||||
}
|
||||
|
||||
# Toolkit-Versionen duerfen bis zu vier Zahlen haben (1.0.0.7). 'build' zaehlt
|
||||
# den vierten Teil hoch und laesst die eigentliche Release-Nummer in Ruhe.
|
||||
next_version() {
|
||||
python3 - "$1" "$2" <<'PY'
|
||||
import sys
|
||||
|
||||
version, level = sys.argv[1], sys.argv[2]
|
||||
parts = [int(p) for p in version.split(".")]
|
||||
while len(parts) < 3:
|
||||
parts.append(0)
|
||||
|
||||
if level == "major":
|
||||
parts = [parts[0] + 1, 0, 0]
|
||||
elif level == "minor":
|
||||
parts = [parts[0], parts[1] + 1, 0]
|
||||
elif level == "patch":
|
||||
parts = [parts[0], parts[1], parts[2] + 1]
|
||||
elif level == "build":
|
||||
parts = parts[:3] + [(parts[3] if len(parts) > 3 else 0) + 1]
|
||||
|
||||
print(".".join(str(p) for p in parts))
|
||||
PY
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ Version
|
||||
|
||||
VERSION=""
|
||||
|
||||
if ! $UNINSTALL && [[ -z "$XPI_OVERRIDE" ]]; then
|
||||
c_step "Version"
|
||||
CURRENT="$(read_manifest_version)"
|
||||
|
||||
if [[ -n "$FORCED_VERSION" ]]; then
|
||||
VERSION="$FORCED_VERSION"
|
||||
elif [[ "$BUMP" == "none" ]]; then
|
||||
VERSION="$CURRENT"
|
||||
else
|
||||
VERSION="$(next_version "$CURRENT" "$BUMP")"
|
||||
fi
|
||||
|
||||
if [[ "$VERSION" == "$CURRENT" ]]; then
|
||||
c_info "unveraendert: $CURRENT"
|
||||
c_note "Firefox installiert ein Paket mit gleicher oder kleinerer Version nicht erneut."
|
||||
elif $DRY_RUN; then
|
||||
c_info "[dry-run] $CURRENT -> $VERSION (manifest.json bliebe unveraendert)"
|
||||
else
|
||||
write_manifest_version "$VERSION"
|
||||
c_good "$CURRENT -> $VERSION (manifest.json aktualisiert)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------ Paket bauen
|
||||
|
||||
XPI=""
|
||||
|
||||
if $UNINSTALL; then
|
||||
:
|
||||
elif [[ -n "$XPI_OVERRIDE" ]]; then
|
||||
[[ -f "$XPI_OVERRIDE" ]] || c_fail "Datei nicht gefunden: $XPI_OVERRIDE"
|
||||
XPI="$(cd "$(dirname "$XPI_OVERRIDE")" && pwd)/$(basename "$XPI_OVERRIDE")"
|
||||
c_step "Paket"
|
||||
c_info "verwende vorhandenes Paket: $XPI"
|
||||
else
|
||||
c_step "Paket bauen"
|
||||
for entry in "${PACKAGE_FILES[@]}"; do
|
||||
[[ -e "$SRC_DIR/$entry" ]] || c_fail "Im Add-on fehlt: $entry"
|
||||
done
|
||||
|
||||
mkdir -p "$BUILD_DIR"
|
||||
|
||||
if $DO_SIGN; then
|
||||
# Signieren erzeugt das Paket selbst - ein vorher gebautes Zip waere nur
|
||||
# eine zweite, unsignierte Datei mit demselben Namen.
|
||||
if [[ -z "${WEB_EXT_API_KEY:-}" || -z "${WEB_EXT_API_SECRET:-}" ]]; then
|
||||
c_fail "Keine AMO-Zugangsdaten: WEB_EXT_API_KEY/WEB_EXT_API_SECRET setzen oder in $AMO_CREDENTIALS hinterlegen."
|
||||
fi
|
||||
|
||||
if $DRY_RUN; then
|
||||
c_info "[dry-run] npx web-ext sign --channel=unlisted (Version $VERSION)"
|
||||
XPI="$BUILD_DIR/swyx_tab_bridge-$VERSION.xpi"
|
||||
else
|
||||
SIGN_MARKER="$BUILD_DIR/.sign-marker"
|
||||
: > "$SIGN_MARKER"
|
||||
|
||||
( cd "$SRC_DIR" && npx --yes web-ext sign \
|
||||
--channel=unlisted \
|
||||
--source-dir="$SRC_DIR" \
|
||||
--artifacts-dir="$BUILD_DIR" \
|
||||
--ignore-files 'test-server/**' 'deploy/**' 'build/**' '**/.DS_Store' )
|
||||
|
||||
# Nur Dateien akzeptieren, die nach dem Start des Signierlaufs entstanden sind.
|
||||
XPI="$(find "$BUILD_DIR" -maxdepth 1 -name '*.xpi' -newer "$SIGN_MARKER" | head -n1)"
|
||||
rm -f "$SIGN_MARKER"
|
||||
[[ -n "$XPI" ]] || c_fail "web-ext hat kein signiertes .xpi in $BUILD_DIR hinterlassen."
|
||||
c_good "signiert: $XPI"
|
||||
fi
|
||||
else
|
||||
XPI="$BUILD_DIR/swyx_tab_bridge-$VERSION.xpi"
|
||||
if $DRY_RUN; then
|
||||
c_info "[dry-run] wuerde bauen: $XPI"
|
||||
else
|
||||
rm -f "$XPI"
|
||||
# -X: keine macOS-Metadaten; gezippt wird der *Inhalt*, nicht der Ordner.
|
||||
( cd "$SRC_DIR" && zip -q -r -X "$XPI" "${PACKAGE_FILES[@]}" -x '*.DS_Store' '*/__MACOSX/*' )
|
||||
c_good "$XPI ($(du -h "$XPI" | cut -f1 | tr -d ' '))"
|
||||
fi
|
||||
if ! $ALLOW_UNSIGNED; then
|
||||
c_note "Nicht signiert - Firefox Release/Beta verweigert die Installation. Entweder --sign verwenden oder auf ESR/Dev die Signaturpflicht abschalten."
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if $BUILD_ONLY; then
|
||||
c_step "Fertig"
|
||||
c_good "${XPI:-kein Paket gebaut}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------ Uebertragen
|
||||
|
||||
c_step "Verbindung zu $HOST"
|
||||
if $DRY_RUN; then
|
||||
c_info "[dry-run] ssh-Test uebersprungen"
|
||||
else
|
||||
# Auch der Test laeuft ueber -EncodedCommand: die Standard-Shell des
|
||||
# SSH-Benutzers ist unter Windows mal cmd.exe, mal PowerShell, und ein
|
||||
# blankes $env:COMPUTERNAME wuerde von der aeusseren Shell aufgeloest.
|
||||
REMOTE_NAME="$(run_remote_ps 'Write-Output "$env:COMPUTERNAME|$($PSVersionTable.PSVersion)"' | tr -d '\r')" \
|
||||
|| c_fail "SSH-Verbindung zu $HOST fehlgeschlagen."
|
||||
c_good "erreichbar: ${REMOTE_NAME:-unbekannt}"
|
||||
fi
|
||||
|
||||
c_step "Dateien uebertragen"
|
||||
run_remote_ps "New-Item -ItemType Directory -Force -Path \"\$HOME\\$REMOTE_DIR\" | Out-Null"
|
||||
|
||||
if $DRY_RUN; then
|
||||
c_info "[dry-run] scp $PS_SCRIPT -> $HOST:$REMOTE_DIR/"
|
||||
[[ -n "$XPI" ]] && c_info "[dry-run] scp $XPI -> $HOST:$REMOTE_DIR/"
|
||||
else
|
||||
"${SCP_BIN[@]}" ${SCP_OPTS[@]+"${SCP_OPTS[@]}"} -q "$PS_SCRIPT" "$HOST:$REMOTE_DIR/Install-SwyxTabBridge.ps1"
|
||||
c_good "Install-SwyxTabBridge.ps1"
|
||||
if [[ -n "$XPI" ]]; then
|
||||
"${SCP_BIN[@]}" ${SCP_OPTS[@]+"${SCP_OPTS[@]}"} -q "$XPI" "$HOST:$REMOTE_DIR/$(basename "$XPI")"
|
||||
c_good "$(basename "$XPI")"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------ Ausfuehren
|
||||
|
||||
c_step "Installation auf $HOST"
|
||||
|
||||
PS_ARGS=""
|
||||
add_arg() { PS_ARGS="$PS_ARGS $1"; }
|
||||
|
||||
add_arg "-ExtensionId $(ps_quote "$EXTENSION_ID")"
|
||||
if $UNINSTALL; then
|
||||
add_arg "-Uninstall"
|
||||
else
|
||||
add_arg "-XpiPath \"\$base\\$(basename "$XPI")\""
|
||||
add_arg "-Mode $(ps_quote "$MODE")"
|
||||
fi
|
||||
[[ -n "$FIREFOX_DIR" ]] && add_arg "-FirefoxDir $(ps_quote "$FIREFOX_DIR")"
|
||||
[[ -n "$TARGET_USER" ]] && add_arg "-TargetUser $(ps_quote "$TARGET_USER")"
|
||||
$ALLOW_UNSIGNED && add_arg "-AllowUnsigned"
|
||||
$STOP_FIREFOX && add_arg "-StopFirefox"
|
||||
|
||||
REMOTE_COMMAND="\$ErrorActionPreference = 'Stop'
|
||||
\$ProgressPreference = 'SilentlyContinue'
|
||||
\$base = Join-Path \$HOME '$REMOTE_DIR'
|
||||
try {
|
||||
& (Join-Path \$base 'Install-SwyxTabBridge.ps1')$PS_ARGS
|
||||
exit \$LASTEXITCODE
|
||||
} catch {
|
||||
Write-Host \"FEHLER: \$(\$_.Exception.Message)\" -ForegroundColor Red
|
||||
exit 1
|
||||
}"
|
||||
|
||||
REMOTE_LOG="$(mktemp -t swyx-deploy)"
|
||||
trap 'rm -f "$REMOTE_LOG"' EXIT
|
||||
|
||||
set +e
|
||||
run_remote_ps "$REMOTE_COMMAND" 2>&1 | tee "$REMOTE_LOG"
|
||||
STATUS=${PIPESTATUS[0]}
|
||||
set -e
|
||||
|
||||
# Die Login-Shell des Windows-SSH-Dienstes reicht nur 0 und "ungleich 0" durch;
|
||||
# aus 2 wird unterwegs 1. Massgeblich ist deshalb die Markerzeile des Skripts,
|
||||
# der Exitcode dient nur als Rueckfallebene.
|
||||
MARKER="$(grep -o 'SWYX-STATUS: [A-Z]*' "$REMOTE_LOG" | tail -n1 | awk '{print $2}')"
|
||||
[[ -z "$MARKER" && "$STATUS" -eq 0 ]] && MARKER="OK"
|
||||
|
||||
c_step "Ergebnis"
|
||||
case "$MARKER" in
|
||||
OK) c_good "Erfolgreich abgeschlossen." ;;
|
||||
RESTART) c_good "Erfolgreich abgeschlossen."
|
||||
c_note "Auf $HOST muss Firefox noch neu gestartet werden." ;;
|
||||
*) c_fail "Das Installationsskript auf $HOST ist abgebrochen (Exitcode $STATUS)." ;;
|
||||
esac
|
||||
|
||||
if [[ -n "$VERSION" ]]; then
|
||||
c_info "Ausgerollte Version: $VERSION"
|
||||
fi
|
||||
Reference in New Issue
Block a user