Iris Classon
Iris Classon - In Love with Code

Using PowerShell to execute JavaScript on a page and outputting the result

Earlier this week I was putting together a PowerShell background service to generate some reports. The reports where based on some queries run against our databases in the different environments, as well as schema comparisons that created upgrade scripts and sent warnings if there could be data loss based on the changes. I’ll get to how the above was done I a different blog post early next week.

Every report needs a chart. Otherwise it just doesn’t look like s report. And I had no idea how to dynamically generate charts in PowerShell. I do however know how to use different JavaScript libraries and I could create a chart like that. The problem would however be that I can’t embed JavaScript in an email so I would need to generate the chart and then grab it as an image or similar.

And that’s how I got the terrible idea to use PowerShell to call a JavaScript function on a webpage that had my chart and return the canvas as a base64. Then I could embed that instead in the email.

Brilliant. Or hacky. Here is how.

Although this example uses a local file you can of course use remotely hosted files. And, for simplicity, I have simply used one a chart from Chart.js and added an ‘oncomplete’ function to get the base64 once the graph has finished rendering. And picked some pretty colors.

 
<!-- saved from url=(0014)about:internet -->
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<h1 style="color:#36B1BF; font-family: Arial, Helvetica, sans-serif;">CPU usage by service</h1>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.3.0/Chart.js"></script>
<img id="out" img>
<canvas id="cpuChart" width="400" height="400"></canvas>
<script>
var ctx = document.getElementById("cpuChart");
 
var myChart = new Chart(ctx, {
    type: 'pie',
    data: {
        labels: ["Aggregation engine", "Calculation engine", "Hangfire", "SignalR service", "Authentication server"],
        datasets: [
            {
                label: 'CPU usage by Service',
                data: [40, 30, 15, 10, 5],
                backgroundColor: ['#F2385A','#35203B','#911146','#CF4A30','#ED8C2B']
            }
        ]
    },
    options: {
        animation: {
            onComplete: function(animation) {
                document.getElementById("out").src = document.getElementById("cpuChart").toDataURL();
            }
        },
        legend:{display: true,labels:{fontSize:30}}
    }
});
 
</script>
</body>
</html>
To run the page you need to use the ie process, and you can do so hidden without the need for interaction. Simply set the visibility property to false.

 
$title = @"
 <div style=";padding:40px;"> <p style="color:#36B1BF;font-size:70px; font-family: Arial, Helvetica, sans-serif;">CPU by Service</p>
"@
 
$url = "file:///C:/Users/msn/Documents/Visual%20Studio%202015/Projects/Petition/index.html"
 
$ie = New-Object -comobject InternetExplorer.Application
 
$ie.Navigate2($url)
 
$ie.Visible = $false
 
while($ie.ReadyState -ne 4) {start-sleep -m 100} 
 
$img = $ie.document.body.getElementsByTagName('img') | Where-Object {$_.id -eq "out"}
 
"$title $($img.outerHTML)</div>" > image.html
 
You will have some surprises with ie, such as being unable to run the JavaScript (if it’s a local file) unless you confirm it’s okay to do so. Even with me disabling that under options the prompt wouldn’t go away, and when run visible=false there is no prompt, and no action. I read somewhere though that adding

 
    
<!-- saved from url=(0014)about:internet -->
    
To the top of the document would make the page considered safe, and it turns out it actually does work.Unless you wait for the page to load you will struggle to get anything returned, and the most common way it to check in iterations the readystate on the ie object. Some query the document for the desired object but I prefer the first method. Traversing the DOM is going to require a mix of JavaScript syntax and PowerShell. I know I only have one image element, but I double check for the id I set on it before extracting the outer html and outputting it to a separate file (or embedding it in an email).I hope you find this helpful, and do share any ideas or projects you have :)

Comments

Leave a comment below, or by email.
Daniel Keller
10/23/2016 7:28:42 PM
So I'm not sure if this is your main kind of thing or if you're willing to look at another language, but there are some awesomely nice things that the open source F# community have done for this kind of things.

Easiest is FsLab (fslab.org) which links together several open source offerings for mucking about data & charting.  Demo: Channel 9 demo

If you go all the way, there is Fabel which transpiles F# to Javascript.

I normally don't advertise stuff like this, web scraping, database, csv & xml parsing and merging a bunch of data sources together. All type safe, all with property names generated from datasource. 
Jacob Huizenga
10/31/2016 12:08:00 PM
Why don't you just use the Google Charts API? You can request the charts with a simple web System.Net.Webclient or Invoke-Webrequest without much hassle. (A simple example can be found here: http://mspowershell.blogspot.nl/2008/01/google-chart-api-powershell.html ) 


Last modified on 2016-10-23

comments powered by Disqus