TisaneOnprem.psm1

#Region '.\Public\Check-ServiceCpu.ps1' 0
function Check-ServiceCpu {
    <#
.SYNOPSIS
    Retrieves the CPU usage of a specified service.
 
.DESCRIPTION
    The Check-ServiceCpu function retrieves the CPU usage of a specified service. It calculates the average CPU usage for the process associated with the service and returns the result.
 
.PARAMETER service
    Specifies the name of the service for which to retrieve the CPU usage.
 
.EXAMPLE
    PS C:\> Check-ServiceCpu -service "MyService"
     
    Description
    -----------
    Retrieves the CPU usage of the "MyService" service.
 
.INPUTS
    None. You cannot pipe objects to this function.
 
.OUTPUTS
    System.Int32
    The function returns an integer representing the CPU usage of the specified service.
#>

    param (
        [Parameter(Mandatory=$true)][string]$service
    )
    $NUMBER_OF_CORES = 1
    $procId = (Get-CimInstance Win32_Service | ?{$_.Name -like $service} | SELECT ProcessId).ProcessId
    $procPath = (Get-Counter "\Process(*)\ID Process" -ErrorAction SilentlyContinue).CounterSamples | Where-Object {$_.CookedValue -eq $procId} | Select-Object -ExpandProperty Path
    $procCpuUsage = [Math]::Round(((Get-Counter ($procPath -replace "\\id process$","\% Processor Time")).CounterSamples.CookedValue) / $NUMBER_OF_CORES)
    return $procCpuUsage
}

#EndRegion '.\Public\Check-ServiceCpu.ps1' 36
#Region '.\Public\Check-ServiceStatus.ps1' 0
function Check-ServiceStatus {
    <#
.SYNOPSIS
Checks the status of a service on a specified host.
 
.DESCRIPTION
The Check-ServiceStatus function sends a POST request to the specified host to check the status of a service. It returns $true if the service is available (HTTP status code 200), and $false otherwise.
 
.PARAMETER hostIp
The IP address or URL of the host where the service is running. This parameter is mandatory.
 
.PARAMETER timeout
The timeout value (in seconds) for the request. This parameter is mandatory.
 
.EXAMPLE
Check-ServiceStatus -hostIp "http://localhost:80/" -timeout 30
Checks the status of a service running on "http://localhost:80/" with a timeout of 30 seconds.
 
#>

    param (
        [Parameter(Mandatory=$true)][string]$hostIp,
        [Parameter(Mandatory=$true)][int]$timeout
    )

    try {
        $uri = $hostIp + "parse"
        $response = Invoke-WebRequest -Uri $uri -Method POST -Headers $headers -ContentType 'application/json' -Body '{"language": "en", "content": "this is a test sentence to test the testing of tisane", "settings": {"parses":true,"debug":true}}' -TimeoutSec $timeout

        if ($response.StatusCode -eq 200) {
            return $true
        } else {
            return $false
        }
    } catch {
        # $date = Get-Date
        # Write-Output "[ $date ]WebRequest failed for $hostIp"
        return $false
    }
}
#EndRegion '.\Public\Check-ServiceStatus.ps1' 40
#Region '.\Public\Download-TisaneFromFTP.ps1' 0
function Download-TisaneFromFTP{
  <#
.SYNOPSIS
Downloads Tisane data and web service from an FTP server.
 
.DESCRIPTION
The Download-TisaneFromFTP function allows you to download Tisane data and web service from an FTP server and save them to the specified local folders. It uses the System.Net.WebClient class to perform the FTP file transfer.
 
.PARAMETER user
Specifies the FTP user name for authentication. This parameter is mandatory.
 
.PARAMETER password
Specifies the FTP password for authentication. This parameter is mandatory.
 
.PARAMETER ftp
Specifies the FTP host name. This parameter is mandatory.
 
.EXAMPLE
Download-TisaneFromFTP -user "ftpuser" -password "ftppassword" -ftp "example.com"
Downloads Tisane data and web service files from the FTP server "example.com" using the FTP user "ftpuser" and password "ftppassword".
 
.EXAMPLE
Download-TisaneFromFTP -user "ftpuser" -password "ftppassword" -ftp "example.com" | Do-Something
Downloads Tisane data and web service files from the FTP server "example.com" using the FTP user "ftpuser" and password "ftppassword" and pipes the output to the Do-Something cmdlet.
 
#>


  [CmdletBinding()]
Param(
     [Parameter(Mandatory = $true, valueFromPipeline=$true, HelpMessage="FTP user: ")][String] $user,
     [Parameter(Mandatory = $true, HelpMessage="FTP password: ")][String] $password,
     [Parameter(Mandatory = $true, HelpMessage="FTP host: ")][String] $ftp
   )

$DOWNLOAD_FOLDER = "C:/Users/Administrator/Downloads/Tisane"
$TISANE_ROOT = "C:/Tisane"

$webclient = New-Object System.Net.WebClient 

if(-not(Test-Path($DOWNLOAD_FOLDER))) {
    # file with path $path doesn't exist
  mkdir $DOWNLOAD_FOLDER
}


$webclient.Credentials = New-Object System.Net.NetworkCredential("$user@$ftp", $password)

$uri = New-Object System.Uri("ftp://ftp.$ftp/tisane_db.zip")
Write-Host "Downloading Tisane data to $DOWNLOAD_FOLDER..." -ForegroundColor Green
$webclient.DownloadFile($uri, "$DOWNLOAD_FOLDER/tisane_db.zip")
Write-Host "Downloading Tisane Web Service to $DOWNLOAD_FOLDER..." -ForegroundColor Green
$uri = New-Object System.Uri("ftp://ftp.$ftp/tisane_ws.zip")
$webclient.DownloadFile($uri, "$DOWNLOAD_FOLDER/tisane_ws.zip")

if(Test-Path($TISANE_ROOT)) {
  #mkdir $TISANE_ROOT
  #Remove-Item -Path "$TISANE_ROOT/" -Recurse
  # dir $TISANE_ROOT -Recurse | Where-Object { $_.FullName -imatch '^C:\\tisane\\([^\\]+)\\.+[.]sst' } | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
}

#Add-Type -AssemblyName System.IO.Compression.FileSystem
#Add-Type -Assembly "System.IO.Compression.Filesystem"

Add-Type -Assembly "System.Io.Compression.FileSystem"

[System.IO.Compression.ZipFile]::ExtractToDirectory("$DOWNLOAD_FOLDER/tisane_db.zip", "$DOWNLOAD_FOLDER/db")
}
#EndRegion '.\Public\Download-TisaneFromFTP.ps1' 68
#Region '.\Public\Install-TisaneFromFTP.ps1' 0
function Install-TisaneFromFTP{
  [CmdletBinding()]
  <#
.SYNOPSIS
Installs Tisane from an FTP server.
 
.DESCRIPTION
The Install-TisaneFromFTP function downloads and installs Tisane from an FTP server. It retrieves the necessary files, including the Tisane database, Tisane Web Service, and Tisane Runtime Service, and extracts them to the specified location. It can also install .NET if specified.
 
.PARAMETER user
Specifies the FTP username used to connect to the FTP server.
 
.PARAMETER password
Specifies the FTP password used to authenticate the FTP user.
 
.PARAMETER ftp
Specifies the FTP host to connect to.
 
.PARAMETER dotnet
Specifies whether to install .NET. If set to $true, the function downloads and installs the .NET installer.
 
.PARAMETER instances
Specifies the number of Tisane instances to create.
 
.PARAMETER winuser
Specifies the Windows user account to use. If not specified, the default value is 'Administrator'.
 
.EXAMPLE
Install-TisaneFromFTP -user "ftpuser" -password "ftppassword" -ftp "ftp.example.com" -dotnet $true -instances 2 -winuser "User1"
Downloads and installs Tisane from the FTP server 'ftp.example.com' using the FTP user credentials 'ftpuser' and 'ftppassword'. It installs .NET, creates 2 Tisane instances, and uses the Windows user account 'User1'.
 
#>

Param(
     [Parameter(Mandatory = $true, valueFromPipeline=$true, HelpMessage="FTP user: ")][String] $user,
     [Parameter(Mandatory = $true, HelpMessage="FTP password: ")][String] $password,
     [Parameter(Mandatory = $true, HelpMessage="FTP host: ")][String] $ftp,
     [Parameter(Mandatory = $false, HelpMessage="Install .NET? ")][boolean] $dotnet,
     [Parameter(Mandatory = $true, HelpMessage="Instance count: ")][int] $instances,
     [Parameter(Mandatory = $false, HelpMessage="Windows user: ")][String] $winuser
   )

if (-not($winuser)) {
  $winuser = 'Administrator'
}
$DOWNLOAD_FOLDER = "C:/Users/$winuser/Downloads/Tisane"
$dotNETInstallerUrl = "https://go.microsoft.com/fwlink/?linkid=2088631"
$TISANE_ROOT = "C:/Tisane"


$webclient = New-Object System.Net.WebClient 

if(-not(Test-Path($DOWNLOAD_FOLDER))) {
    # file with path $path doesn't exist
  mkdir $DOWNLOAD_FOLDER
}

if ($dotnet) {
  Write-Host "Downloading .NET installer to $DOWNLOAD_FOLDER..." -ForegroundColor Green
  $webclient.DownloadFile($dotNETInstallerUrl, "$DOWNLOAD_FOLDER/dotnetinstaller.exe")
  Write-Host "Installing .NET..." -ForegroundColor Green
  Start-Process -FilePath $DOWNLOAD_FOLDER/dotnetinstaller.exe -ArgumentList "/q","/norestart" -Wait
}



$webclient.Credentials = New-Object System.Net.NetworkCredential("$user@tisane.ai", $password)

$uri = New-Object System.Uri("ftp://$ftp/tisane_db.zip")
Write-Host "Downloading Tisane data to $DOWNLOAD_FOLDER..." -ForegroundColor Green
$webclient.DownloadFile($uri, "$DOWNLOAD_FOLDER/tisane_db.zip")
Write-Host "Downloading Tisane Web Service to $DOWNLOAD_FOLDER..." -ForegroundColor Green
$uri = New-Object System.Uri("ftp://$ftp/tisane_ws.zip")
$webclient.DownloadFile($uri, "$DOWNLOAD_FOLDER/tisane_ws.zip")
$uri = New-Object System.Uri("ftp://$ftp/Tisane.Runtime.Service.exe.config")
$webclient.DownloadFile($uri, "$DOWNLOAD_FOLDER/Tisane.Runtime.Service.exe.config")


if(Test-Path($TISANE_ROOT)) {
  #mkdir $TISANE_ROOT
  #Remove-Item -Path $TISANE_ROOT -Recurse
}

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

#[System.IO.Compression.ZipFile]::ExtractToDirectory($zipfile, $outpath)
Write-Host "Extracting Tisane data to $TISANE_ROOT..." -ForegroundColor Green
# WE HAVE TO USE Ionic Zip because the standard compression libraries crash with very large archives
[System.Reflection.Assembly]::LoadFrom("$DOWNLOAD_FOLDER/Ionic.Zip.dll")
$zipFile = New-Object Ionic.Zip.ZipFile("$DOWNLOAD_FOLDER/tisane_db.zip")

#Expand-Archive -LiteralPath "$DOWNLOAD_FOLDER/tisane_db.zip" -DestinationPath $TISANE_ROOT
$zipFile.ExtractAll($TISANE_ROOT, 1)

$portNumber = 81
For ($i=1; $i -le $instances; $i++) {
  $formattedI = $i | % tostring 00
  $portNumber = 79 + $i
  Write-Host "Creating instance under $TISANE_ROOT/instance$formattedI" -ForegroundColor Green
  Expand-Archive -LiteralPath "$DOWNLOAD_FOLDER/tisane_ws.zip" -DestinationPath "$TISANE_ROOT/instance$formattedI" -Force
  $webclient.DownloadFile($uri, "$TISANE_ROOT/instance$formattedI/Tisane.Runtime.Service.exe.config")
  ((Get-Content -path "$TISANE_ROOT/instance$formattedI/Tisane.Runtime.Service.exe.config" -Raw) -replace 'localhost:80',"localhost:$portNumber") | Set-Content -Path "$TISANE_ROOT/instance$formattedI/Tisane.Runtime.Service.exe.config"
  Start-Process -FilePath "$TISANE_ROOT/instance$formattedI/Tisane.Runtime.Service.exe" -ArgumentList "-i" -Wait
}
# now open firewall ports
netsh advfirewall firewall add rule name='Tisane ports' dir=in protocol=tcp localport="80-$portNumber" action=allow profile=any

# create a quarterly update task
SchTasks /Create /sc monthly /mo 3 /TN "Tisane update" /TR "$DOWNLOAD_FOLDER/UpdateTisaneFromFTP.ps1 -user $user -password $password -ftp $ftp" /ST 23:00

# start the services
Start-Service "Tisane Runtime *"
}
#EndRegion '.\Public\Install-TisaneFromFTP.ps1' 113
#Region '.\Public\Monitor-Tisane.ps1' 0
function Monitor-Tisane {
    [CmdletBinding()]
    <#
.SYNOPSIS
Monitors specified services for health and restarts them if necessary.
 
.DESCRIPTION
The Monitor-Tisane function allows you to monitor the health of specified services and automatically restart them if they fail consecutive health checks. It checks the status of the services, their CPU consumption, and the availability of their endpoints. If a service fails the health check, it will be restarted and an email notification will be sent.
 
.PARAMETER services
Specifies the names of the services to monitor. Multiple service names can be provided separated by commas.
 
.PARAMETER interval
Specifies the timeout interval in seconds for the monitoring process. The default value is 120 seconds.
 
.PARAMETER thresholdCpu
Specifies the threshold for CPU consumption in percentage. If the CPU consumption of a service exceeds this threshold, it will be considered unhealthy. The default value is 100.
 
.PARAMETER timeout
Specifies the timeout value in seconds for each health check. If a health check takes longer than this timeout, the service will be considered unhealthy. The default value is 2 seconds.
 
.PARAMETER failureBeforeRestart
Specifies the number of consecutive health checks that must fail before a service is restarted. The default value is 1.
 
.EXAMPLE
Monitor-Tisane -services "ServiceA","ServiceB" -interval 60 -thresholdCpu 80
 
Monitors "ServiceA" and "ServiceB" with an interval of 60 seconds and a CPU threshold of 80%.
 
.EXAMPLE
Monitor-Tisane -services "Tisane Runtime [Tisane]","Tisane Runtime [Tisane1]" -interval 180 -thresholdCpu 90 -failureBeforeRestart 3
 
Monitors Tisane Runtime [Tisane] and Tisane Runtime [Tisane1] with an interval of 180 seconds, a CPU threshold of 90%, and restarts the service if it fails 3 consecutive health checks.
#>

    param (
        [Parameter(Mandatory=$true, HelpMessage="Service names to monitor: ")][String[]]$services,
        [Parameter(Mandatory=$false, HelpMessage="Timeout for monitor (Default 120s)")][int]$interval = 120,
        [Parameter(Mandatory=$false, HelpMessage="Threshold for CPU consuption (Default 100)")][int]$thresholdCpu = 100,
        [Parameter(Mandatory=$false, HelpMessage="Timeout for the healthcheck: (Default 2s)")][int]$timeout = 2,
        [Parameter(Mandatory=$false, HelpMessage="Number of health checks to fail consecutively before restart : (Default 1)")][int]$failureBeforeRestart = 1,
        [Parameter(Mandatory=$false, HelpMessage="Name for the server (Default public ip of machine: )")][string]$machine
    )
    Write-Host "The script allows you to get email notification, Please run Setup-Email to configure the credentials"
    # [string[]]$services = $services.Split(",")
    if ([string]::IsNullOrEmpty($machine)){
        $machine = (Invoke-WebRequest -uri "http://ifconfig.me/ip").Content
    }
    $hostName = [System.Net.Dns]::GetHostName()
    $Results = @{}
    $originalServiceNames = @{}
    $servicesOriginal = $services
    Write-Host $services
    $services = $servicesOriginal -replace '\[','?' -replace '\]','?'

    foreach ($service in $services) {
        $oneService = Get-CimInstance Win32_Service | ?{$_.Name -like $service} | SELECT Name , PathName
        if ([string]::IsNullOrEmpty($oneService)){
            Write-Error "$service not found"
            # Write-Host "Abortting !!" -ForegroundColor RED
            return 
        }
        $serviceName = $oneService.Name
        $configFile = $oneService.PathName -replace "\.exe", ".exe.config" -replace '\"', ''
        if (Test-Path $configFile) {
                $config = [xml](Get-Content $configFile)
                $AddNode = Select-Xml -Xml $config -XPath "//services/service/host/baseAddresses"
                $Value = $AddNode.Node.add.baseAddress
                $Results[$service] = $Value
                $originalServiceNames[$service]=$serviceName
        }else{
            Write-Error "Config file for $service not found"
            # Write-Error "Abortting !!"
            return
        }
    }
    $hostIpS = $Results.Values
    $services = $Results.Keys
    # $originalNames = $originalServiceNames.Values
    Write-Output "Starting monitor for the following services"
    # Write-Host $originalServiceNames.Values -ForegroundColor Green
    # Write-Host "Corresponding endpoints: "
    # Write-Host $hostIpS -ForegroundColor Green
    $combinedList = for ($i = 0; $i -lt $hostIpS.Count; $i++) {
        [PSCustomObject]@{
            Services = $services[$i]
            Endpoints = $Results[$services[$i]]
        }
    }
    $combinedList | Format-Table -AutoSize
    $restarting =@{}
    $failureCount = @{}
    while ($true) {
        foreach ($service in $services) {
            $hostIp = $Results[$service]
            if ($restarting.ContainsKey($service)) {
                Write-Output "$service is being restarted"
                if (Check-ServiceStatus -hostIp $hostIp -timeout $timeout) {
                    $date = Get-Date
                    Write-Output "[ $date ]Service on $service has restared"
                    # $failureCount[$service] = 0
                    $restarting.Remove($service)
                }
                continue
            }
            $cpuUsage = Check-ServiceCpu $service -ErrorAction Ignore
            if ((Check-ServiceStatus -hostIp $hostIp -timeout $timeout) -and ( $cpuUsage -le $thresholdCpu)) {
                $date = Get-Date
                Write-Output "[ $date ]$service is running, consuming $cpuUsage% cpu"
                $failureCount[$service] = 0
            } else {
                $failureCount[$service]++
                $oneServiceFaliurCount = $failureCount[$service]
                $date = Get-Date
                Write-Output "[ $date ]$service failed $oneServiceFaliurCount health checks consecutively"
                if ($oneServiceFaliurCount -ge $failureBeforeRestart){
                    $restarting[$service]=$hostIp
                    Stop-Service -Name $service
                    Start-Service -Name $service
                    $failureCount[$service] = 0
                    Write-Output "sending mail"
                    $subject = "Tisane admin alert [$hostName]"
                    $instanceName = $originalServiceNames[$service]
                    $date = Get-Date
                    $body = "[$instanceName] running on [$hostName ($machine)] was restarted on [$date] after health check failed"
                    try {
                        Send-Email -body $body -subject -$subject
                        Write-Output "[ $date ] Email Sent"
                    }catch{
                        Write-Output "[ $date ] Could not send email"
                    }
                    
                }
            }
        }
        # Sleep for 120 seconds
        Start-Sleep -Seconds $interval
    }
}
#EndRegion '.\Public\Monitor-Tisane.ps1' 139
#Region '.\Public\Reset-EmailSettings.ps1' 0
function Reset-EmailSettings {
    <#
.SYNOPSIS
Resets the email settings to their default values.
 
.DESCRIPTION
The Reset-EmailSettings function resets the email settings by clearing the current values and setting them to their default values.
 
.PARAMETER None
This function does not accept any parameters.
 
.EXAMPLE
Reset-EmailSettings
Resets the email settings to their default values.
 
.NOTES
#>

    Save-LampSetting -settingName 'emailTo' -settingValue $null
    Save-LampSetting -settingName 'smtpServer' -settingValue $null
    Save-LampSetting -settingName 'smtpPort' -settingValue '587'
    Save-LampSetting -settingName 'emailUsername' -settingValue $null
    Save-LampSetting -settingName 'emailPassword' -settingValue $null
}
#EndRegion '.\Public\Reset-EmailSettings.ps1' 24
#Region '.\Public\Send-Email.ps1' 0
function Send-Email() {
    <#
.SYNOPSIS
Sends an email using the configured SMTP settings.
 
.DESCRIPTION
The Send-Email function sends an email using the configured SMTP settings. It retrieves the necessary email parameters from the Lamp settings and sends the email using the Send-MailMessage cmdlet.
 
.PARAMETER Body
Specifies the body of the email. This parameter is optional.
 
.PARAMETER Subject
Specifies the subject of the email. This parameter is optional.
 
.EXAMPLE
Send-Email -Subject "Hello" -Body "This is the email body"
Sends an email with the specified subject and body.
 
.NOTES
 
Run Setup-Email to set the smtp settings
#>


    Param(
    
        [Parameter(Mandatory=$false, HelpMessage="Body: ")][String] $body,
        [Parameter(Mandatory=$false, HelpMessage="Subject: " )][String] $subject
    )
    $To = Get-LampSetting -settingName 'emailTo' -defaultValue ''
    $To = $To.Split(",")
    $smtpServer = Get-LampSetting -settingName 'smtpServer' -defaultValue 'mail.tisane.ai'
    $smtpPort = Get-LampSetting -settingName 'smtpPort' -defaultValue '587'
    $emailUsername = Get-LampSetting -settingName 'emailUsername' -defaultValue ''
    $encryptedEmailPassword = Get-LampSetting -settingName 'emailPassword' -defaultValue ''
    $secureEmailPassword = ConvertTo-SecureString -String $encryptedEmailPassword
    $emailCredential = New-Object System.Management.Automation.PSCredential($emailUsername, $secureEmailPassword)
    Send-MailMessage -To $To -From $emailUsername -Subject $subject -Body $body -Credential $emailCredential -SmtpServer $SmtpServer -Port $SmtpPort -WarningAction Ignore
    # Write-Host "Email sent"
}
#EndRegion '.\Public\Send-Email.ps1' 40
#Region '.\Public\Setup-Email.ps1' 0
function Setup-Email {
    <#
.SYNOPSIS
Configures email settings for sending alerts.
 
.DESCRIPTION
The Setup-Email function allows you to configure email settings for sending alerts. It prompts you to provide the necessary information, such as comma-delimited email addresses to send alerts to, SMTP server, SMTP port, and sender email account credentials. Once the settings are configured, it sends a test email to verify the configuration.
 
.PARAMETER None
This function does not accept any parameters.
 
.EXAMPLE
Setup-Email
Configures email settings for sending alerts. Prompts for email addresses, SMTP server, SMTP port, and sender email account credentials.
 
.NOTES
You must have the necessary permissions and access to the SMTP server to successfully send emails.
 
#>

    Write-Host "Comma-delimited email addresses to send alerts to:"
    $To = Read-Host
    $To = $To.Split(",")
    Save-LampSetting -settingName 'emailTo' -settingValue $To
    $smtpServer = Read-Host "SMTP server"
    $smtpPort = Read-Host "SMTP port (default: 587)"
    Save-LampSetting -settingName 'smtpServer' -settingValue $smtpServer
    Save-LampSetting -settingName 'smtpPort' -settingValue $smtpPort
    if ([string]::IsNullOrEmpty($smtpServer)) {
        $smtpServer = Get-LampSetting -settingName 'smtpServer' -defaultValue ''   
    }
    if ([string]::IsNullOrEmpty($smtpPort)) {
        $smtpPort = Get-LampSetting -settingName 'smtpPort' -defaultValue '587'
    }
    
    $emailUsername = Get-LampSetting -settingName 'emailUsername' -defaultValue ''
    $encryptedEmailPassword = Get-LampSetting -settingName 'emailPassword' -defaultValue ''
    if ([string]::IsNullOrEmpty($emailUsername)){
        # Write-Host "in if"
        # $credential = Get-Credential -Message "Sender email account credentials"
        $credential = $host.ui.PromptForCredential("Sender email account credentials", "Sender email account credentials", "", "")
        $emailUsername = $credential.UserName
        $secureEmailPassword = $credential.Password
        $encryptedEmailPassword = ConvertFrom-SecureString -SecureString $secureEmailPassword
        Save-LampSetting -settingName 'emailUsername' -settingValue $emailUsername
        Save-LampSetting -settingName 'emailPassword' -settingValue $encryptedEmailPassword
    }else{
        # Write-Host "in else"
        $secureEmailPassword = ConvertTo-SecureString -String $encryptedEmailPassword
    }
    # $emailPassword = [System.Net.NetworkCredential]::new("", $secureEmailPassword).Password
    $emailCredential = New-Object System.Management.Automation.PSCredential($emailUsername, $secureEmailPassword)
    $date = Get-Date
    $subject = "Tisane admin test email"
    $body = "This is a test email sent by Monitor-Tisane script [ $date ]"
    Write-Host "Email settings : "
    Write-Host "To: $To"
    Write-Host "From: $emailUsername"
    Write-Host "smtpServer: $smtpServer"
    Write-Host "smtpPort: $smtpPort"
    try {
        Send-MailMessage -To $To -From $emailUsername -Subject $subject -Body $body -Credential $emailCredential -SmtpServer $smtpServer -Port $smtpPort
    }
    catch {
        Write-Host "An error occurred while sending the email:" -ForegroundColor Red
        Write-Host "Error message: $($_.Exception.Message)" -ForegroundColor Red
        Write-Host "Please reset the settings using Reset-EmailSettings, and run the Setup-Email again." -ForegroundColor Red
        return
    }
    
    # Send-MailMessage -To $To -From $emailUsername -Subject $subject -Body $body -Credential $emailCredential -SmtpServer $smtpServer -Port $smtpPort
    Write-Host "Test Email sent" -ForegroundColor Green
    Write-Host "Please check your inbox, if you did not receive the email, please reset the settings using Reset-EmailSettings, and run the Setup-Email again."
}
#EndRegion '.\Public\Setup-Email.ps1' 74
#Region '.\Public\Update-TisaneFromFTP.ps1' 0
function Update-TisaneFromFTP{
  <#
.SYNOPSIS
Updates the Tisane software from an FTP server.
 
.DESCRIPTION
The Update-TisaneFromFTP function downloads the latest Tisane software files from an FTP server and updates the Tisane installation on the local machine. It can be used to update both the Tisane data files and the Tisane Web Service.
 
.PARAMETER user
The FTP user name required to connect to the FTP server.
 
.PARAMETER password
The FTP password required to authenticate the FTP user.
 
.PARAMETER ftp
The FTP host name or IP address.
 
.PARAMETER copyOnly
Specifies whether to copy the updated files without re-registering the Tisane service. By default, this parameter is set to $false.
 
.EXAMPLE
Update-TisaneFromFTP -user "ftpuser" -password "ftppassword" -ftp "ftp.example.com"
 
Downloads the latest Tisane software files from the FTP server "ftp.example.com" using the FTP user "ftpuser" and the specified password. The function updates the Tisane installation on the local machine.
 
#>

  [CmdletBinding()]
Param(
     [Parameter(Mandatory = $true, valueFromPipeline=$true, HelpMessage="FTP user: ")][String] $user,
     [Parameter(Mandatory = $true, HelpMessage="FTP password: ")][String] $password,
     [Parameter(Mandatory = $true, HelpMessage="FTP host: ")][String] $ftp,
     [Parameter(Mandatory = $false, HelpMessage="Copy only, no need to reregister the service: ")][Boolean] $copyOnly
   )

$DOWNLOAD_FOLDER = "C:/Users/Administrator/Downloads/Tisane"
$TISANE_ROOT = "C:/Tisane"

$webclient = New-Object System.Net.WebClient 

if(-not(Test-Path($DOWNLOAD_FOLDER))) {
    # file with path $path doesn't exist
  mkdir $DOWNLOAD_FOLDER
}


$webclient.Credentials = New-Object System.Net.NetworkCredential("$user@$ftp", $password)

$uri = New-Object System.Uri("ftp://ftp.$ftp/tisane_db.zip")
Write-Host "Downloading Tisane data to $DOWNLOAD_FOLDER..." -ForegroundColor Green
$webclient.DownloadFile($uri, "$DOWNLOAD_FOLDER/tisane_db.zip")
Stop-Service "Tisane Runtime *"
Write-Host "Downloading Tisane Web Service to $DOWNLOAD_FOLDER..." -ForegroundColor Green
$uri = New-Object System.Uri("ftp://ftp.$ftp/tisane_ws.zip")
$webclient.DownloadFile($uri, "$DOWNLOAD_FOLDER/tisane_ws.zip")

if(Test-Path($TISANE_ROOT)) {
  #mkdir $TISANE_ROOT
  #Remove-Item -Path "$TISANE_ROOT/" -Recurse
  # dir $TISANE_ROOT -Recurse | Where-Object { $_.FullName -imatch '^C:\\tisane\\([^\\]+)\\.+[.]sst' } | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
}

#Add-Type -AssemblyName System.IO.Compression.FileSystem
#Add-Type -Assembly "System.IO.Compression.Filesystem"


# WE HAVE TO USE Ionic Zip because the standard compression libraries crash with very large archives
[System.Reflection.Assembly]::LoadFrom("$DOWNLOAD_FOLDER/Ionic.Zip.dll")
$zipFile = New-Object Ionic.Zip.ZipFile("$DOWNLOAD_FOLDER/tisane_db.zip")

#[System.IO.Compression.ZipFile]::ExtractToDirectory("$DOWNLOAD_FOLDER/tisane_db.zip", $TISANE_ROOT, $true)
Write-Host "Extracting Tisane data to $TISANE_ROOT..." -ForegroundColor Green
#Expand-Archive -LiteralPath "$DOWNLOAD_FOLDER/tisane_db.zip" -DestinationPath $TISANE_ROOT -Force
(Get-Service "Tisane *").WaitForStatus('Stopped')
$zipFile.ExtractAll($TISANE_ROOT, 1)

# dir $TISANE_ROOT -Recurse | Where-Object { $_.FullName -imatch '^C:\\tisane\\([^\\]+)\\LOG[.]old.+' } | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue


$services = Get-Service "Tisane Runtime *"
$instances = $services.length


For ($i=1; $i -le $instances; $i++) {
  $formattedI = $i | % tostring 00
  Write-Host "Updating instance under $TISANE_ROOT/instance$formattedI" -ForegroundColor Green
  Expand-Archive -LiteralPath "$DOWNLOAD_FOLDER/tisane_ws.zip" -DestinationPath "$TISANE_ROOT/instance$formattedI" -Force
  if (-not $copyOnly) {
    Start-Process -FilePath "$TISANE_ROOT/instance$formattedI/Tisane.Runtime.Service.exe" -ArgumentList "-r" -Wait
  }
}

Start-Service "Tisane Runtime *"
}
#EndRegion '.\Public\Update-TisaneFromFTP.ps1' 94
#Region '.\Public\Update-TisaneFromLocalZips.ps1' 0
function Update-TisaneFromLocalZips{
Stop-Service "Tisane Runtime *"

$DOWNLOAD_FOLDER = "C:/Users/Administrator/Downloads/Tisane"
$TISANE_ROOT = "C:/Tisane"

#Add-Type -AssemblyName System.IO.Compression.FileSystem
#Add-Type -Assembly "System.IO.Compression.Filesystem"


# WE HAVE TO USE Ionic Zip because the standard compression libraries crash with very large archives
[System.Reflection.Assembly]::LoadFrom("$DOWNLOAD_FOLDER/Ionic.Zip.dll")
$zipFile = New-Object Ionic.Zip.ZipFile("$DOWNLOAD_FOLDER/tisane_db.zip")


#[System.IO.Compression.ZipFile]::ExtractToDirectory("$DOWNLOAD_FOLDER/tisane_db.zip", $TISANE_ROOT, $true)
Write-Host "Extracting Tisane data to $TISANE_ROOT..." -ForegroundColor Green
#Expand-Archive -LiteralPath "$DOWNLOAD_FOLDER/tisane_db.zip" -DestinationPath $TISANE_ROOT -Force
$zipFile.ExtractAll($TISANE_ROOT, 1)

# dir $TISANE_ROOT -Recurse | Where-Object { $_.FullName -imatch '^C:\\tisane\\([^\\]+)\\LOG[.]old.+' } | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue


$services = Get-Service "Tisane Runtime *"
$instances = $services.length


For ($i=1; $i -le $instances; $i++) {
  $formattedI = $i | % tostring 00
  Write-Host "Updating instance under $TISANE_ROOT/instance$formattedI" -ForegroundColor Green
  Expand-Archive -LiteralPath "$DOWNLOAD_FOLDER/tisane_ws.zip" -DestinationPath "$TISANE_ROOT/instance$formattedI" -Force
  Start-Process -FilePath "$TISANE_ROOT/instance$formattedI/Tisane.Runtime.Service.exe" -ArgumentList "-r" -Wait
}

Start-Service "Tisane Runtime *"
}
#EndRegion '.\Public\Update-TisaneFromLocalZips.ps1' 37