Powershell keeps file open Out-file -append / Add-content

Copper Contributor

I have the following module
When running it it fails to write into the files as it claims to be in use by a process
when running add-content or out-file -append it keeps it open but idk why. the module keeps the file open and when closing it and reopening powershell console it works again once. but running it again does not the file already exists

CodeSignTool.psm1

 

function Set-CodeSignature {
    param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true, Position=0)]
        [string[]]$Path,

        [Parameter(Position=1)]
        [string]$CertificatePath,

        [Parameter(Position=2)]
        [string]$TimestampServer = "http://timestamp.digicert.com",

        [Parameter()]
        [switch]$AllExtensions
    )

    # Define valid extensions for code-signing
    $validExtensions = @('.ps1','.psm1', '.exe', '.dll', '.msi','.cab', '.mst', '.msp', '.vbs')

    foreach ($item in $Path) {

        # Set log file location
        $logFile = Join-Path -Path $item -ChildPath "CodeSigningLog.txt"

        # Retrieve the default code-signing certificate if no path provided
        if (-not $CertificatePath) {
            $cert = Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object -Property EnhancedKeyUsageList -like "*(1.3.6.1.5.5.7.3.3)*" | Select-Object -First 1
            if (-not $cert) {
                Write-Error "No code-signing certificate found in the current user's personal store!" | Out-File -FilePath $logFile -Append
                return
            }
        } else {
            $cert = Get-PfxCertificate -FilePath $CertificatePath
        }

        # Output and log certificate and timestamp server details
        $certMessage = "Using Certificate: $($cert.Subject)"
        $timestampMessage = "Using Timestamp Server: $TimestampServer"

        Write-Output $certMessage | Out-File -FilePath $logFile -Append
        Write-Output $timestampMessage | Out-File -FilePath $logFile -Append

        if (Test-Path -Path $item -PathType Container) {
            if ($AllExtensions) {
                # Get all files from directory without checking extensions
                $files = Get-ChildItem -Path $item -Recurse | Where-Object { $_.PSIsContainer -eq $false }
            } else {
                # Get only files with valid extensions
                $files = Get-ChildItem -Path $item -Recurse | Where-Object { $_.Extension -in $validExtensions }
            }
        } elseif (Test-Path -Path $item -PathType Leaf) {
            # Single file provided
            if ($AllExtensions) {
                $files = Get-Item -Path $item
            } else {
                $files = Get-Item -Path $item | Where-Object { $_.Extension -in $validExtensions }
            }
        } else {
            Write-Warning "Invalid path provided: $item" | Out-File -FilePath $logFile -Append
            continue
        }

        # Sign each file
        foreach ($file in $files) {
            try {
                # Determine the hash algorithm based on file extension
                $hashAlgorithm = 'SHA256'

                # Sign the file with the selected hash algorithm and a timestamp
                Set-AuthenticodeSignature -FilePath $file.FullName -Certificate $cert -HashAlgorithm $hashAlgorithm -TimestampServer $TimestampServer 

                $successMessage = "Successfully signed $($file.FullName) with $hashAlgorithm."
                Write-Output $successMessage | Out-File -FilePath $logFile -Append
            } catch {
                $errorMessage = "Failed to sign $($file.FullName). Error: $_"
                Write-Warning $errorMessage | Out-File -FilePath $logFile -Append
            }
        }
    }
}

 

 
CodeSignTool.psd1

 

#
# Modulmanifest für das Modul "CodeSignTool"
#
# Generiert von:
#
# Generiert am: 27.10.2023
#

@{

# Die diesem Manifest zugeordnete Skript- oder Binärmoduldatei.
RootModule = 'CodeSignTool.psm1'

# Die Versionsnummer dieses Moduls
ModuleVersion = '1.0'

# Unterstützte PSEditions
# CompatiblePSEditions = @()

# ID zur eindeutigen Kennzeichnung dieses Moduls
GUID = '5f3c5bb9-e4cc-46d4-84d5-f968f9526472'

# Autor dieses Moduls
Author = ''

# Unternehmen oder Hersteller dieses Moduls
CompanyName = ''

# Urheberrechtserklärung für dieses Modul
Copyright = '(c) 2023. Alle Rechte vorbehalten.'

# Beschreibung der von diesem Modul bereitgestellten Funktionen
Description = 'Tool for Code Signing'

# Die für dieses Modul mindestens erforderliche Version des Windows PowerShell-Moduls
# PowerShellVersion = ''

# Der Name des für dieses Modul erforderlichen Windows PowerShell-Hosts
# PowerShellHostName = ''

# Die für dieses Modul mindestens erforderliche Version des Windows PowerShell-Hosts
# PowerShellHostVersion = ''

# Die für dieses Modul mindestens erforderliche Microsoft .NET Framework-Version. Diese erforderliche Komponente ist nur für die PowerShell Desktop-Edition gültig.
# DotNetFrameworkVersion = ''

# Die für dieses Modul mindestens erforderliche Version der CLR (Common Language Runtime). Diese erforderliche Komponente ist nur für die PowerShell Desktop-Edition gültig.
# CLRVersion = ''

# Die für dieses Modul erforderliche Prozessorarchitektur ("Keine", "X86", "Amd64").
# ProcessorArchitecture = ''

# Die Module, die vor dem Importieren dieses Moduls in die globale Umgebung geladen werden müssen
# RequiredModules = @()

# Die Assemblys, die vor dem Importieren dieses Moduls geladen werden müssen
# RequiredAssemblies = @()

# Die Skriptdateien (PS1-Dateien), die vor dem Importieren dieses Moduls in der Umgebung des Aufrufers ausgeführt werden.
# ScriptsToProcess = @()

# Die Typdateien (.ps1xml), die beim Importieren dieses Moduls geladen werden sollen
# TypesToProcess = @()

# Die Formatdateien (.ps1xml), die beim Importieren dieses Moduls geladen werden sollen
# FormatsToProcess = @()

# Die Module, die als geschachtelte Module des in "RootModule/ModuleToProcess" angegebenen Moduls importiert werden sollen.
# NestedModules = @()

# Aus diesem Modul zu exportierende Funktionen. Um optimale Leistung zu erzielen, verwenden Sie keine Platzhalter und löschen den Eintrag nicht. Verwenden Sie ein leeres Array, wenn keine zu exportierenden Funktionen vorhanden sind.
FunctionsToExport = @('Set-CodeSignature')

# Aus diesem Modul zu exportierende Cmdlets. Um optimale Leistung zu erzielen, verwenden Sie keine Platzhalter und löschen den Eintrag nicht. Verwenden Sie ein leeres Array, wenn keine zu exportierenden Cmdlets vorhanden sind.
CmdletsToExport = @()

# Die aus diesem Modul zu exportierenden Variablen
VariablesToExport = '*'

# Aus diesem Modul zu exportierende Aliase. Um optimale Leistung zu erzielen, verwenden Sie keine Platzhalter und löschen den Eintrag nicht. Verwenden Sie ein leeres Array, wenn keine zu exportierenden Aliase vorhanden sind.
AliasesToExport = @()

# Aus diesem Modul zu exportierende DSC-Ressourcen
# DscResourcesToExport = @()

# Liste aller Module in diesem Modulpaket
# ModuleList = @()

# Liste aller Dateien in diesem Modulpaket
# FileList = @()

# Die privaten Daten, die an das in "RootModule/ModuleToProcess" angegebene Modul übergeben werden sollen. Diese können auch eine PSData-Hashtabelle mit zusätzlichen von PowerShell verwendeten Modulmetadaten enthalten.
PrivateData = @{

    PSData = @{

        # 'Tags' wurde auf das Modul angewendet und unterstützt die Modulermittlung in Onlinekatalogen.
        # Tags = @()

        # Eine URL zur Lizenz für dieses Modul.
        # LicenseUri = ''

        # Eine URL zur Hauptwebsite für dieses Projekt.
        # ProjectUri = ''

        # Eine URL zu einem Symbol, das das Modul darstellt.
        # IconUri = ''

        # 'ReleaseNotes' des Moduls
        # ReleaseNotes = ''

    } # Ende der PSData-Hashtabelle

} # Ende der PrivateData-Hashtabelle

# HelpInfo-URI dieses Moduls
# HelpInfoURI = ''

# Standardpräfix für Befehle, die aus diesem Modul exportiert werden. Das Standardpräfix kann mit "Import-Module -Prefix" überschrieben werden.
# DefaultCommandPrefix = ''

}

 

 

4 Replies

Hi @AkitoTheGambler,

you can address this file locking issue by using the .NET System.IO.File methods for writing to the file. Replace all occurrences of Out-File -FilePath $logFile -Append with [System.IO.File]::AppendAllText($logFile, $yourMessage).

For instance, instead of:

 

Write-Output $certMessage | Out-File -FilePath $logFile -Append

 

You can try:

 

 

[System.IO.File]::AppendAllText($logFile, $certMessage)

 

 

Please click Mark as Best Response & Like if my post helped you to solve your issue.
This will help others to find the correct solution easily. It also closes the item.


If the post was useful in other ways, please consider giving it Like.


Kindest regards,


Leon Pavesic
(LinkedIn)

@LeonPavesic 

 

unfortunately we are using ConstrainedLanguage mode so we cannot use it

but running out-file manually doesnt usually lock the file after running
but somehow in some cases in scriptblocks or functions it does.
i dont understand why tho.  (but not always, idk what the conditions are that cause it to behave like this)


https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_language_m...

i was trying to figure out why this happens as it only breaks inside the Scriptblock but doesnt hangup and keep it open if its everything outside the scriptblock

so

Write-Output $certMessage | Out-File -FilePath $logFile -Append
Write-Output $timestampMessage | Out-File -FilePath $logFile -Append


always works fine

but everything inside the foreach scriptblock fails

Hi @AkitoTheGambler,
Did you find a solution for this? I have the same issue. I use a function to write data to a log file and I get "random" access denied errors when writing to the file.