Maintenance Pop-up customization

There is a task called “Get Maintenance Consent From Logged In User” that will pop up a pop-up for the user to accept or decline a maintenance session to run on their computer. You can customize the title, message, timeout, and default answer to the maintenance. However, I’m not aware of a way to customize it beyond that. I would like to be able to change the icon to our company’s logo since the question mark and the system icon at the top both just make it look like a virus.

MicrosoftTeams-image (1)

Yeah, that’s just a standard msgbox, I don’t think you can modify the title bar icon. However, if you would like to get a little more invested in the pop up Windows, a few of us in the community have already done the work to convert it into a WPF windows. Here’s my code. You will need the create a function called Get-ConsentFromUser before the task script will function.

Get-ConsentFromUser

<#
.SYNOPSIS
    Displays a consent window on the users desktop.

.DESCRIPTION
    Displays a consent window and prompts the user to provide input to allow maintenance to continue.
    
.PARAMETER Title
    Specifies the title in the title bar. There is a default value of "System Maintenance".

.PARAMETER Timeout
    Specifies the timeout before the program runs the default choice.

.PARAMETER DefaultAnswer
    Specifies the default answer that is chosen if the program times out.

.EXAMPLE
    Get-ConsentFromUser -title "System Maintenance" -timeout 90 -DefaultAnswer "No"

.OUTPUTS
    [Boolean]

#>
[CmdletBinding()]
param(
    [string]$title = "System Maintenance",
    [int]$timeout = 60,
    [ValidateSet('Yes','No')]
    [string]$DefaultAnswer = "Yes"
)

$isUserLoggedIn = Invoke-ImmyCommand {
    return $true
} -Context User -ErrorAction SilentlyContinue
if(!$isUserLoggedIn) {
    return $DefaultAnswer
}

$answer = Invoke-ImmyCommand {
    Write-Host "Running command as logged in user, waiting up to $($Using:Timeout) second(s) for consent."
    $syncHash = [hashtable]::Synchronized(@{})
    $newRunspace =[runspacefactory]::CreateRunspace()
    $newRunspace.ApartmentState = "STA"
    $newRunspace.ThreadOptions = "ReuseThread"
    $newRunspace.Open()
    $newRunspace.SessionStateProxy.SetVariable("Title",$Using:Title)
    $newRunspace.SessionStateProxy.SetVariable("Timeout",$Using:Timeout)
    $newRunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)
    $psCmd = [PowerShell]::Create().AddScript({
    Add-Type -AssemblyName PresentationFramework,PresentationCore
    $Icon = "<Add URL to logo image here>"     # <---------------------------NOTE
    
    $Notice = @"
<TextBlock FontSize="16">
Your MSP is trying to run maintenance on your computer.<LineBreak/>
This may trigger several restarts. If you do not wish to proceed now, <LineBreak/>
maintenance will continue unprompted after business hours. <LineBreak/>
</TextBlock>
"@

    # Define XAML for the window
    [xml]$xaml = @"
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="$Title"
        SizeToContent="WidthAndHeight"
        WindowStartupLocation="CenterScreen"
        WindowState="Normal"
        ShowInTaskbar="False"
        Icon="$Icon"
        Topmost="True"
        ResizeMode="NoResize">
    <Window.Resources>
        <SolidColorBrush x:Key="BackgroundBrush" Color="#1E1E1E"/>
        <SolidColorBrush x:Key="ForegroundBrush" Color="#F1F1F1"/>
        <SolidColorBrush x:Key="BorderBrush" Color="#2E2E2E"/>
        <Style TargetType="Button">
            <Setter Property="Background" Value="{StaticResource BackgroundBrush}"/>
            <Setter Property="Foreground" Value="{StaticResource ForegroundBrush}"/>
            <Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
            <Setter Property="BorderThickness" Value="1"/>
            <Setter Property="Padding" Value="5"/>
            <Setter Property="Margin" Value="5"/>
        </Style>
        <Style TargetType="TextBlock">
            <Setter Property="Foreground" Value="{StaticResource ForegroundBrush}"/>
            <Setter Property="Margin" Value="5"/>
        </Style>
    </Window.Resources>
    <Grid Background="{StaticResource BackgroundBrush}">
        <StackPanel Margin="10">
            $Notice
            <TextBlock FontSize="14" Foreground="Yellow" Text="Do you wish to proceed at this time?"/>            
            <StackPanel Orientation="Horizontal" Margin="0,10,0,0" HorizontalAlignment="Right">
                <Button Name="Yes" Content="Yes" FontSize="18" Background="LightYellow" Foreground="Black" FontWeight="Bold" />
                <Button Name="No" Content="No" FontSize="18" Background="LightYellow" Foreground="Black" FontWeight="Bold" />
            </StackPanel>
        </StackPanel>
    </Grid>
</Window>
"@

    $reader = New-Object System.Xml.XmlNodeReader $xaml
    try{
        $window = [Windows.Markup.XamlReader]::Load($reader)
    }catch{
        $_.Exception.InnerException.Message
        throw
    }

    # Define the event handlers for the buttons
    $yesHandler = {
        $window.DialogResult = $true
    }
    $noHandler = {
        $window.DialogResult = $false
    }

    # Add event handlers to the buttons
    $window.FindName("Yes").Add_Click($yesHandler)
    $window.FindName("No").Add_Click($noHandler)

    # Show the window and wait for the user to make a choice
    $result = $window.ShowDialog()

    return $result
    })

    $null = $psCmd.Runspace = $newRunspace
    $RunSpace = $psCmd.BeginInvoke()
    while (-not $RunSpace.IsCompleted) {
    Start-Sleep -Milliseconds 100
    }

    $RunspaceResult = $psCmd.EndInvoke($RunSpace)
    return $RunspaceResult
} -Context User -Timeout ($Timeout + 10) #Extra 10 seconds to account for any delay in showing the popup

switch($answer) {
     $true      {return 'Yes'}
     $false      {return 'No'}
}

And here’s the task combined script:

$ConsentDialogParams = @{}
if($DialogTitle) {
    $ConsentDialogParams.Add("Title",$DialogTitle)
}
if($DialogTimeout) {
    $ConsentDialogParams.Add("Timeout",$DialogTimeout)
}
if($DialogDefaultAnswer) {
    $ConsentDialogParams.Add("DefaultAnswer",$DialogDefaultAnswer)
}
$answer = Get-ConsentFromUser @ConsentDialogParams
if($answer -ne "Yes") {
    Stop-ImmySession -FailureMessage "Maintenance was aborted by the logged in user."
}

Do whatever you like with it. You could make a $message parameter, however for me I had trouble getting the text lined up right, so I just put my message directly in the XAML with line breaks. I also have a note in the function script to add your icon, but you could turn that into a parameter as well following the example of the other parameters.

1 Like

I would love to give this a try! I have never created my own task though. Do you think you could give more instructions or screenshots on how it is set up?

I can provide you with some screenshots of the same task to help a bit. Here’s how I set the parameters:

If you notice under the name that it says DialogTitle will be usable in your script as $DialogTitle, and the same occurs for the other parameters. In the task script I posted earlier, if $DialogTitle is present, then it becomes $Title and passed to the function. Again, it’s the same for the other parameters.

Next is the script section:

I blurred my script since it has my company name, but this is where you would add the task script. For most scripts, you would use the combined script which utilizes a switch statement with the automatic $method variable ($method will only ever be “test”, “get”, or “set”). In this case, the script only ever needs to be in “set” mode since it requires user interaction.

Hope that helps!

1 Like

I got it figured out. Thank you for the help on that! But how were you able to have the maintenance run at a later time if they say no?

The task itself doesn’t do that, your schedule does. This is based off of the msgbox option that the Immy team has in global. They both do the same thing in the case of a user saying “no”. They will run the Stop-ImmyMaintenanceSession function, which does what it says. The session itself is just stopped, it is not postponed. Your schedule will run the next maintenance session.

1 Like

Is there a way to have the new task you helped me make, “Get-ConsentFromUser”, to run after the detection? Something like:
Detection > Get consent > Pending for execution > Execution

That is how it was setup before with the original “Get Maintenance Consent From Logged In User” task. I kind of liked it setup that way, but now I can’t recreate it again. Any suggestions?

Not entirely sure what you are asking here. You should place it first on the ordering page, and that should make sure it’s the first thing that runs after detection.

That being said, it is worth noting (and mentioned at the top of the ordering page) that onboarding tasks always occur before regular maintenance items, regardless of the ordering. So you might need to make two cross tenant deployments for this task: one that is set to “onboarding only”, and another that uses the filterscript “Not During Onboarding”

What I meant is that I want it to ask for consent before the maintenance detection or immediately after finishing the detection.

Right now, I have it set to do the detection during the day, then run the maintenance after work hours. So ideally the user can consent to maintenance while working during the day for it to run in the afternoon. It would be nice to have the pop-up say something along the lines of, “Can we do maintenance on your device tonight at 9 p.m.?” That way, the user knows when to expect maintenance.

you could add it first to the ordering, enable execute serially and then add it to test, that way it runs during detection.

unsure how to get it to abort maintenance if the click no

Pretty sure there’s a built-in Stop-ImmySession function. So you would just do something like:

if ($no -eq $true) { 
   Stop-ImmySession
}

I use something like Dakota’s script, but modified the task and duplicated the scripts so that it tests for get/test/set. During the maintenance dection(test phase), it fires a dialog letting the user know we’re looking at what needs to be done, that if they’re the primary user they’ll get an email, and when the actual maintenance may occur with only an OK button. This part of the script always returns false(not compliant). When the execution time comes around, it fires the task again, execution(set phase). Another dialog comes up letting them know maintenance will be performed, restart may be necessary, etc, with a Continue and Cancel button. It times out to Continue.

Curious if anyone has any experience in the ImmyBot functions to allow for the ability to do the following with a user Prompt:

  1. Initial Warning Message: The script should start by sending a warning message about the scheduled maintenance. This message should include details about the maintenance, its expected duration, and its impact.
  2. Postpone Button: Include a button in the message that allows users to postpone the maintenance for one hour. You need to limit this to a maximum of four postponements.
  3. Tracking Postponements: Keep a record of how many times a user has postponed the maintenance. After the fourth postponement, the option to postpone further should be disabled.
  4. Schedule for Later Button: Include another button that allows users to schedule the maintenance for later in the day. This can bring up a simple interface for selecting a time.
  5. Backend Logic: The script should handle the logic of rescheduling the maintenance and tracking user interactions.

If someone figures this out…id implement in a heartbeat.

I am not sure what I am missing but I am getting the error below.

The Test script did not return any usable output.
Multiple ambiguous overloads found for “Load” and the argument count: “1”.
Please ensure you output at least one $true or $false value to indicate compliance

I made this before I started using the parameter blocks. Here is a copy of the parameter block for this task:

param(
[Parameter(Position=0,Mandatory=$False)]
[String]$DialogTitle='System Maintenance',
[Parameter(Position=1,Mandatory=$False)]
[Int32]$DialogTimeout=60,
[Parameter(Position=2,Mandatory=$False)]
[ValidateSet('Yes','No')]
[String]$DialogDefaultAnswer='Yes'
)

This might help, hopefully.

1 Like

You might also need to wrap it all in something like

if ($method -eq "set") {
    # paste entire code here, except the param block
}

You don’t want anything popping up during “test” because tests should not perform any action that modifies the state of the computer.

1 Like

I tried to make the adjustments you mentioned but now I am getting the error below.

Error: The Test script did not return any output. Please ensure you output at least one $true or $false value to indicate compliance

Additional Info.

I have the Function script “Get-ConsentFromUser” saved from your first comment.

Then I have a task called “Get Consent From User”

The task is set up like

The script “Get-ConsentFromUser Combined Script 2” is below.

param(
[Parameter(Position=0,Mandatory=$False)]
[String]$DialogTitle='System Maintenance',
[Parameter(Position=1,Mandatory=$False)]
[Int32]$DialogTimeout=60,
[Parameter(Position=2,Mandatory=$False)]
[ValidateSet('Yes','No')]
[String]$DialogDefaultAnswer='Yes'
)

if ($method -eq "set") {
$ConsentDialogParams = @{}
if($DialogTitle) {
    $ConsentDialogParams.Add("Title",$DialogTitle)
}
if($DialogTimeout) {
    $ConsentDialogParams.Add("Timeout",$DialogTimeout)
}
if($DialogDefaultAnswer) {
    $ConsentDialogParams.Add("DefaultAnswer",$DialogDefaultAnswer)
}
$answer = Get-ConsentFromUser @ConsentDialogParams
if($answer -ne "Yes") {
    Stop-ImmySession -FailureMessage "Maintenance was aborted by the logged in user."
}
}

Anything I am screwing up here?

Okay, so let’s try it without it being a combined script. Use it as a set script like this:

This also means you can remove if ($method -eq "set") { from around the actual script.

Since “Test” and “get” are disabled in this context, they shouldn’t throw any errors.