How to: 3CX Windows App GPO auto deployment

Mr. Sea

Silver Partner
Advanced Certified
Joined
Dec 18, 2024
Messages
18
Reaction score
11
Hey all, so... I know there's plenty of people complaining about the lack of an MSI package for v20 (myself included). With that said, I have wrote out a complete script for mass domain deployment. All you need is the MSIX package stored on your typical network app deployment location.


# Define the name of the software
$softwareName = "3CX"

# Define the path to the MSIX package
$msixPath = "\\YOUR_FQDN_HERE\deploy$\3CX\3CX.msix"

# Log file path
$logFile = "$env:TEMP\3CXInstallationLog.txt"

# Function to log messages
function Write-Log {
param (
[string]$Message
)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logMessage = "$timestamp - $Message"
Write-Output $logMessage
Add-Content -Path $logFile -Value $logMessage
}

# Log the user identity
$userIdentity = whoami
Write-Log "Script is being executed by: $userIdentity"

# Check if the package is already installed
$existingPackage = Get-AppxPackage | Where-Object { $_.Name -like "*$softwareName*" }

if ($existingPackage) {
Write-Output "$softwareName is already installed. Skipping installation."
exit
} else {
Write-Log "Starting script execution."

Write-Log "Checking for existing installation of $softwareName..."

if (Test-Path -Path $msixPath) {
Write-Log "Found MSIX package at $msixPath. Starting installation..."

try {
# Install the MSIX package
Add-AppxPackage -Path $msixPath
Write-Log "$softwareName has been installed successfully."
} catch {
$errorMessage = "An error occurred during the installation: $_.Exception.Message"
Write-Log $errorMessage
Write-Error $errorMessage
}
} else {
$errorMessage = "MSIX package for $softwareName not found at $msixPath. Please verify the path."
Write-Log $errorMessage
Write-Error $errorMessage
}

Write-Log "Script execution completed."
}



Hope this helps someone else down the line. You can fire this as a ps1 on logon for your on net users. For off net users, assuming you have a VPN, you can create a scheduled task that triggers on the event id of your successful VPN connection, which then fires off the above script as well.
 
NICE!!!!!!!!!!!!!!!!!!!!!!
 
Thanks for this...

Heres an upgraded version which will not only check for it already being installed but will check the version number and uninstall and reinstall if its different allowing for upgrades and even downgrades if needed.

just save this to a 3CX.ps1 file and put it in the same location as your 3CX.msix and call it from anywhere including GPO login scripts etc.

Code:
<#
.SYNOPSIS
    Installs or upgrades the 3CX MSIX package, uninstalling the old version first if required.

.DESCRIPTION
    This script compares the installed version of 3CX with the version inside the MSIX.
    If they differ, it uninstalls the existing version before installing the new one.
    It logs each step and handles basic error reporting. It expects the 3CX.msix file to be in the same folder as the .ps1 script.
#>

# -------------------------
# Configuration
# -------------------------

$softwareName = "3CX"

# Resolve the path to the MSIX file (same folder as the .ps1 script)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$msixPath = Join-Path $scriptDir "3CX.msix"

# Path to log file
$logFile = "$env:TEMP\3CXInstallationLog.txt"

# -------------------------
# Logging Function
# -------------------------

function Write-Log {
    param (
        [string]$Message
    )
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $logMessage = "$timestamp - $Message"
    Write-Output $logMessage
    Add-Content -Path $logFile -Value $logMessage
}

# -------------------------
# Get MSIX Version From Memory
# -------------------------

function Get-MSIXVersionInMemory {
    param (
        [Parameter(Mandatory = $true)]
        [string]$MSIXPath
    )

    Add-Type -AssemblyName System.IO.Compression.FileSystem

    try {
        $zip = [System.IO.Compression.ZipFile]::OpenRead($MSIXPath)
        $entry = $zip.Entries | Where-Object { $_.FullName -ieq "AppxManifest.xml" }

        if (-not $entry) {
            throw "AppxManifest.xml not found in MSIX"
        }

        $reader = New-Object System.IO.StreamReader($entry.Open())
        $xmlContent = $reader.ReadToEnd()
        $reader.Close()
        $zip.Dispose()

        [xml]$manifest = $xmlContent
        return $manifest.Package.Identity.Version
    } catch {
        throw "Failed to read version from MSIX: $($_.Exception.Message)"
    }
}

# -------------------------
# Start Execution
# -------------------------

$userIdentity = whoami
Write-Log "Script is being executed by: $userIdentity"

if (-not (Test-Path -Path $msixPath)) {
    $errorMessage = "MSIX package not found at $msixPath. Check path and try again."
    Write-Log $errorMessage
    Write-Error $errorMessage
    exit 1
}

Write-Log "Checking for existing installation of $softwareName..."

# Get current installed version (if present)
$existingPackage = Get-AppxPackage | Where-Object { $_.Name -like "*$softwareName*" }

try {
    $msixVersion = Get-MSIXVersionInMemory -MSIXPath $msixPath
    Write-Log "MSIX package version: $msixVersion"
} catch {
    Write-Log $_.Exception.Message
    exit 1
}

if ($existingPackage) {
    $installedVersion = $existingPackage.Version
    Write-Log "Installed version: $installedVersion"

    if ($msixVersion -ne $installedVersion) {
        Write-Log "Version mismatch. Uninstalling existing $softwareName version..."

        try {
            Remove-AppxPackage -Package $existingPackage.PackageFullName
            Write-Log "Uninstalled existing $softwareName version successfully."
        } catch {
            $errorMessage = "Failed to uninstall existing version: $($_.Exception.Message)"
            Write-Log $errorMessage
            Write-Error $errorMessage
            exit 1
        }
    } else {
        Write-Log "$softwareName is already up to date. No action required."
        exit 0
    }
} else {
    Write-Log "$softwareName is not currently installed. Proceeding with installation..."
}

# -------------------------
# Install New Package
# -------------------------

try {
    Write-Log "Installing $softwareName from $msixPath..."
    Add-AppxPackage -Path $msixPath
    Write-Log "$softwareName has been installed successfully."
} catch {
    $errorMessage = "Installation failed: $($_.Exception.Message)"
    Write-Log $errorMessage
    Write-Error $errorMessage
    exit 1
}

Write-Log "Script execution completed successfully."
exit 0
 
  • Like
Reactions: Ecki
Ironically I had the need to upgrade the app, much appreciated for the contrib! I have modified a little more, this does not uninstall prior package, but can do an in-place upgrade or downgrade - which in turn keeps your 3CX App provisioning :D

Code:
<#
.SYNOPSIS
    Installs or upgrades the 3CX MSIX package.

.DESCRIPTION
    This script compares the installed version of 3CX with the version inside the MSIX.
    If they differ, it installs the new version without explicitly uninstalling the old one.
    It logs each step, handles basic error reporting, and closes both standard and AppX/UWP-based 3CX processes.
#>

# -------------------------
# Configuration
# -------------------------

$softwareName = "3CX"
$processName = "3CXPhoneApp"  # Regular desktop process name
$msixBaseName = "3CX.msix"

# Resolve the path to the MSIX file (same folder as the .ps1 script)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$msixPath = Join-Path $scriptDir $msixBaseName

# Path to log file
$logFile = "$env:TEMP\3CXInstallationLog.txt"

# -------------------------
# Logging Function
# -------------------------

function Write-Log {
    param ([string]$Message)
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $logMessage = "$timestamp - $Message"
    Write-Output $logMessage
    Add-Content -Path $logFile -Value $logMessage
}

# -------------------------
# Get MSIX Version From Memory
# -------------------------

function Get-MSIXVersionInMemory {
    param ([Parameter(Mandatory = $true)][string]$MSIXPath)

    Add-Type -AssemblyName System.IO.Compression.FileSystem

    try {
        $zip = [System.IO.Compression.ZipFile]::OpenRead($MSIXPath)
        $entry = $zip.Entries | Where-Object { $_.FullName -ieq "AppxManifest.xml" }

        if (-not $entry) { throw "AppxManifest.xml not found in MSIX" }

        $reader = New-Object System.IO.StreamReader($entry.Open())
        $xmlContent = $reader.ReadToEnd()
        $reader.Close()
        $zip.Dispose()

        [xml]$manifest = $xmlContent
        return $manifest.Package.Identity.Version
    } catch {
        throw "Failed to read version from MSIX: $($_.Exception.Message)"
    }
}

# -------------------------
# Ensure Regular App is Not Running
# -------------------------

function Stop-3CXIfRunning {
    param ([string]$ProcessName)

    $running = Get-Process -Name $ProcessName -ErrorAction SilentlyContinue
    if ($running) {
        Write-Log "Detected running process: $ProcessName. Attempting to close it..."

        try {
            $running | Stop-Process -Force
            Write-Log "$ProcessName was stopped successfully."
            Start-Sleep -Seconds 3
        } catch {
            $msg = "Failed to stop $ProcessName`: $($_.Exception.Message)"
            Write-Log $msg
            Write-Error $msg
            exit 1
        }
    } else {
        Write-Log "$ProcessName is not running. Continuing with installation."
    }
}

# -------------------------
# Ensure AppX-Based 3CX App is Not Running
# -------------------------

function Stop-3CXAppxIfRunning {
    $packages = Get-AppxPackage | Where-Object { $_.Name -like "*3CX*" }

    foreach ($pkg in $packages) {
        Write-Log "Checking AppX package: $($pkg.PackageFullName)"

        $appProcesses = Get-Process | Where-Object {
            $_.Path -and $_.Path -like "*$($pkg.PackageFamilyName)*"
        }

        if ($appProcesses) {
            foreach ($proc in $appProcesses) {
                try {
                    Write-Log "Stopping AppX process $($proc.Name) (PID: $($proc.Id))"
                    Stop-Process -Id $proc.Id -Force
                } catch {
                    Write-Log "Failed to stop AppX process $($proc.Name): $($_.Exception.Message)"
                    throw
                }
            }

            Start-Sleep -Seconds 3
        } else {
            # Fallback: brute-force kill 3CX executables with '3CX' in path or name
            $fallback = Get-Process | Where-Object { $_.Name -like "*3CX*" -or ($_.Path -like "*3CX*") } `
                       | Where-Object { $_.Id -ne $PID }  # Avoid killing this script
            foreach ($proc in $fallback) {
                try {
                    Write-Log "Force-killing leftover 3CX-related process: $($proc.Name) (PID: $($proc.Id))"
                    Stop-Process -Id $proc.Id -Force
                } catch {
                    Write-Log "Failed to kill fallback process $($proc.Name): $($_.Exception.Message)"
                }
            }
        }
    }
}

# -------------------------
# Start Execution
# -------------------------

$userIdentity = whoami
Write-Log "Script is being executed by: $userIdentity"

if (-not (Test-Path -Path $msixPath)) {
    $errorMessage = "MSIX package not found at $msixPath. Check path and try again."
    Write-Log $errorMessage
    Write-Error $errorMessage
    exit 1
}

Write-Log "Checking for existing installation of $softwareName..."

$existingPackage = Get-AppxPackage | Where-Object { $_.Name -like "*$softwareName*" }

try {
    $msixVersion = Get-MSIXVersionInMemory -MSIXPath $msixPath
    Write-Log "MSIX package version: $msixVersion"
} catch {
    Write-Log $_.Exception.Message
    exit 1
}

if ($existingPackage) {
    $installedVersion = $existingPackage.Version
    Write-Log "Installed version: $installedVersion"

    if ($msixVersion -eq $installedVersion) {
        Write-Log "$softwareName is already up to date. No action required."
        exit 0
    } else {
        Write-Log "A newer version is available. Proceeding with installation..."
    }
} else {
    Write-Log "$softwareName is not currently installed. Proceeding with installation..."
}

# -------------------------
# Kill All Related Processes
# -------------------------

Write-Log "Stopping running 3CX processes (regular and AppX)..."
Stop-3CXIfRunning -ProcessName $processName
Stop-3CXAppxIfRunning

# -------------------------
# Install New Package
# -------------------------

try {
    Write-Log "Installing $softwareName from $msixPath..."
    Add-AppxPackage -Path $msixPath
    Write-Log "$softwareName has been installed successfully."

    # Optional: Post-install check
    $newPackage = Get-AppxPackage | Where-Object { $_.Name -like "*$softwareName*" }
    if ($newPackage) {
        Write-Log "Verified new version installed: $($newPackage.Version)"
    } else {
        Write-Log "Warning: Post-install package verification failed."
    }

} catch {
    $errorMessage = "Installation failed: $($_.Exception.Message)"
    Write-Log $errorMessage
    Write-Error $errorMessage
    exit 1
}

Write-Log "Script execution completed successfully."
exit 0
 
Ironically I had the need to upgrade the app, much appreciated for the contrib! I have modified a little more, this does not uninstall prior package, but can do an in-place upgrade or downgrade - which in turn keeps your 3CX App provisioning :D

Code:
<#
.SYNOPSIS
    Installs or upgrades the 3CX MSIX package.

.DESCRIPTION
    This script compares the installed version of 3CX with the version inside the MSIX.
    If they differ, it installs the new version without explicitly uninstalling the old one.
    It logs each step, handles basic error reporting, and closes both standard and AppX/UWP-based 3CX processes.
#>

# -------------------------
# Configuration
# -------------------------

$softwareName = "3CX"
$processName = "3CXPhoneApp"  # Regular desktop process name
$msixBaseName = "3CX.msix"

# Resolve the path to the MSIX file (same folder as the .ps1 script)
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$msixPath = Join-Path $scriptDir $msixBaseName

# Path to log file
$logFile = "$env:TEMP\3CXInstallationLog.txt"

# -------------------------
# Logging Function
# -------------------------

function Write-Log {
    param ([string]$Message)
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $logMessage = "$timestamp - $Message"
    Write-Output $logMessage
    Add-Content -Path $logFile -Value $logMessage
}

# -------------------------
# Get MSIX Version From Memory
# -------------------------

function Get-MSIXVersionInMemory {
    param ([Parameter(Mandatory = $true)][string]$MSIXPath)

    Add-Type -AssemblyName System.IO.Compression.FileSystem

    try {
        $zip = [System.IO.Compression.ZipFile]::OpenRead($MSIXPath)
        $entry = $zip.Entries | Where-Object { $_.FullName -ieq "AppxManifest.xml" }

        if (-not $entry) { throw "AppxManifest.xml not found in MSIX" }

        $reader = New-Object System.IO.StreamReader($entry.Open())
        $xmlContent = $reader.ReadToEnd()
        $reader.Close()
        $zip.Dispose()

        [xml]$manifest = $xmlContent
        return $manifest.Package.Identity.Version
    } catch {
        throw "Failed to read version from MSIX: $($_.Exception.Message)"
    }
}

# -------------------------
# Ensure Regular App is Not Running
# -------------------------

function Stop-3CXIfRunning {
    param ([string]$ProcessName)

    $running = Get-Process -Name $ProcessName -ErrorAction SilentlyContinue
    if ($running) {
        Write-Log "Detected running process: $ProcessName. Attempting to close it..."

        try {
            $running | Stop-Process -Force
            Write-Log "$ProcessName was stopped successfully."
            Start-Sleep -Seconds 3
        } catch {
            $msg = "Failed to stop $ProcessName`: $($_.Exception.Message)"
            Write-Log $msg
            Write-Error $msg
            exit 1
        }
    } else {
        Write-Log "$ProcessName is not running. Continuing with installation."
    }
}

# -------------------------
# Ensure AppX-Based 3CX App is Not Running
# -------------------------

function Stop-3CXAppxIfRunning {
    $packages = Get-AppxPackage | Where-Object { $_.Name -like "*3CX*" }

    foreach ($pkg in $packages) {
        Write-Log "Checking AppX package: $($pkg.PackageFullName)"

        $appProcesses = Get-Process | Where-Object {
            $_.Path -and $_.Path -like "*$($pkg.PackageFamilyName)*"
        }

        if ($appProcesses) {
            foreach ($proc in $appProcesses) {
                try {
                    Write-Log "Stopping AppX process $($proc.Name) (PID: $($proc.Id))"
                    Stop-Process -Id $proc.Id -Force
                } catch {
                    Write-Log "Failed to stop AppX process $($proc.Name): $($_.Exception.Message)"
                    throw
                }
            }

            Start-Sleep -Seconds 3
        } else {
            # Fallback: brute-force kill 3CX executables with '3CX' in path or name
            $fallback = Get-Process | Where-Object { $_.Name -like "*3CX*" -or ($_.Path -like "*3CX*") } `
                       | Where-Object { $_.Id -ne $PID }  # Avoid killing this script
            foreach ($proc in $fallback) {
                try {
                    Write-Log "Force-killing leftover 3CX-related process: $($proc.Name) (PID: $($proc.Id))"
                    Stop-Process -Id $proc.Id -Force
                } catch {
                    Write-Log "Failed to kill fallback process $($proc.Name): $($_.Exception.Message)"
                }
            }
        }
    }
}

# -------------------------
# Start Execution
# -------------------------

$userIdentity = whoami
Write-Log "Script is being executed by: $userIdentity"

if (-not (Test-Path -Path $msixPath)) {
    $errorMessage = "MSIX package not found at $msixPath. Check path and try again."
    Write-Log $errorMessage
    Write-Error $errorMessage
    exit 1
}

Write-Log "Checking for existing installation of $softwareName..."

$existingPackage = Get-AppxPackage | Where-Object { $_.Name -like "*$softwareName*" }

try {
    $msixVersion = Get-MSIXVersionInMemory -MSIXPath $msixPath
    Write-Log "MSIX package version: $msixVersion"
} catch {
    Write-Log $_.Exception.Message
    exit 1
}

if ($existingPackage) {
    $installedVersion = $existingPackage.Version
    Write-Log "Installed version: $installedVersion"

    if ($msixVersion -eq $installedVersion) {
        Write-Log "$softwareName is already up to date. No action required."
        exit 0
    } else {
        Write-Log "A newer version is available. Proceeding with installation..."
    }
} else {
    Write-Log "$softwareName is not currently installed. Proceeding with installation..."
}

# -------------------------
# Kill All Related Processes
# -------------------------

Write-Log "Stopping running 3CX processes (regular and AppX)..."
Stop-3CXIfRunning -ProcessName $processName
Stop-3CXAppxIfRunning

# -------------------------
# Install New Package
# -------------------------

try {
    Write-Log "Installing $softwareName from $msixPath..."
    Add-AppxPackage -Path $msixPath
    Write-Log "$softwareName has been installed successfully."

    # Optional: Post-install check
    $newPackage = Get-AppxPackage | Where-Object { $_.Name -like "*$softwareName*" }
    if ($newPackage) {
        Write-Log "Verified new version installed: $($newPackage.Version)"
    } else {
        Write-Log "Warning: Post-install package verification failed."
    }

} catch {
    $errorMessage = "Installation failed: $($_.Exception.Message)"
    Write-Log $errorMessage
    Write-Error $errorMessage
    exit 1
}

Write-Log "Script execution completed successfully."
exit 0
Great - just need a newer or older msix to test with - where can i get an older or newer version ?
 

Forum statistics

Threads
111,956
Messages
589,927
Members
164,859
Latest member
MichaelRussell3