Safari JavaScript Console to the Rescue

I was trying to submit an online order for a Christmas Gift this morning. Everything went well, till I clicked on the button to place the Order. The screen would take me back to the Payment Section without any Error or Warning(s). I checked the card couple times, but everything was in order.

Out of curiosity, I right clicked on the page, Inspect Element (Safari). Clicked on the Consol Tab. Cleared the logs to reduce clutter and clicked the Submit Order button. and I noticed a line with an Error. One thing that caught my attention was the word City Error.

Went back to Edit the address and I saw a warning asking to me to type the City. The City was already populated as I have been using this card for many years. Provided the city again and surprisingly the city now changed to 13 characters (I live in a city with a long name). It looks like the Developers included USPS validation, but failed to provide the customer with the validation error.

Anyways, thanks to developer tools. Order submitted successfully.

Python Script to Query Jira (On-Prem) and send reminder email(s)

This script connects to an On-Prem Jira instance, executes a Query to retrieve QA-Ready Tickets and sends an email to the Assignee. There are 2 parts to this script. The first section has some plumbing code to wrap the results into a HTML type document that can be sent as an email. The subsequent code has the code to connect to Jira.

  • Code section below Queries Jira to get a list of defects (see condition type = “Defect”)
  • Some custom columns in the script below may prevent the code from executing as-is
  • Use print (json.dumps(data, indent = 4)) to understand the JSON response and modify the rest of the code as needed
  • Review and modify the Variables, SMTP Server details, Queries and columns to match your enviornment
  • Warning :: Modify the sAssignedToEmail to a test email account in the call to SendEmail(sFrom, sAssignedToEmail, subject, body) to avoid sending emails to your user base

Review Jira documentation here


#run the following command in a Terminal window to install jira
#pip install atlassian-python-api

from atlassian import Jira
import json
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

#Call this function to wrap the Columne Header value into a HTML Table Header
def GetHtmlColumnHeader(value):
    val = f"""\
          <th>{value}</th>"""
    return val

#Call this function to wrap the cell value into a HTML Table Cell
def GetHtmlCell(value):
    val = f"""\
         <td>{value}</td>"""
    
    return val

#use double curly braces to escape style sheets.
#Variable within single curly braces will Substituted
def GetHtmlEmail(Headers, Rows):
    val = f"""\
        <html>
           <head>
              <style type='text/css'>
                 @font-face
                 {{font-family:Calibri;
                 panose-1:2 15 5 2 2 2 4 3 2 4;}}
                 table
                 {{
                 border: 0px;
                 width: 98%;
                 }}
                 th
                 {{
                 vertical-align: top;
                 font-family:'Calibri',sans-serif;
                 border:solid #4472C4 1.0pt;
                 border-right:none;
                 background:#4472C4;
                 padding:5.4pt 5.4pt 5.4pt 5.4pt;
                 color:white;
                 }}
                 th {{width: 75px;}}
                 th+th {{width: 300px;}}
                 th+th+th {{width: 120px;}}
                 td
                 {{vertical-align: top;
                 font-family:'Calibri',sans-serif;
                 color:windowtext;
                 font-size:11.0pt;
                 border:solid #8EAADB 1.0pt;padding:0in 5.4pt 0in 5.4pt;
                 }}
                 p, li, div
                 {{margin:0in;
                 margin-bottom:.0001pt;
                 font-size:12.0pt;
                 font-family:'Calibri',sans-serif;}}
                 span
                 {{mso-style-type:personal-compose;
                 font-family:'Calibri',sans-serif;
                 color:windowtext;
                 font-size:11.0pt;}}
                 .MsoChpDefault
                 {{mso-style-type:export-only;
                 font-size:12.0pt;
                 font-family:'Calibri',sans-serif;}}
                 @page WordSection1
                 {{size:8.5in 11.0in;
                 margin:1.0in 1.0in 1.0in 1.0in;}}
                 div
                 {{page:WordSection1;}}
              </style>
           </head>
           <body>
              <div>
                 <p>
                    <span>
                    Hi,
                    </span>
                    </=
                    p>
                 <p>
                    <span>
                    <o:p> </o:p>
                    </span=
                    >
                 </p>
                 <p>
                    <span>
                       The following Tickets are in QA Ready Status. Please retest and close the Ticket, if the requested functionality is working or return it back to Development with details (description of the issue and attachments if any)
                       <o:p></o:p>
                    </span>
                 </p>
                 <p>
                    <span>
                       <o:p> </o:p>
                    </span>
                 </p>
                 <table>
                    <thead>
                       <tr>
                          {Headers}
                       </tr>
                    </thead>
                    <tbody>
                       {Rows}
                 </table>
                 <p>
                    <span>
                       <o:p> </o:p>
                    </span>
                 </p>
                 <p>
                    <span>
                       Thank you,
                       <o:p></o:p>
                    </span>
                 </p>
                 <p>
                    <span>
                       <o:p> </o:p>
                    </span>
                 </p>
                 <p>
                    <span>
                    Project Management Team
                    </span>
                 </p>
              </div>
           </body>
        </html>"""
    
    return val

def SendEmail(sFrom, sTo, subject, body):
    #port = 2525 
    smtp_server = "smtp.domain.com"
    message = MIMEMultipart("alternative")
    message["Subject"] = subject
    message["From"] = sFrom
    message["To"] = sTo
    
    shtml = MIMEText(body, "html")
    message.attach(shtml)
    # send your email
    with smtplib.SMTP(smtp_server) as server:
        server.sendmail(sFrom, sTo, message.as_string())
    print('Sent') 

sFrom = "sender@gmail.com"

jira = Jira(
    url='https://onprem.jira.com',
    username='jira.user.name',
    password='jira.password')
query = r'project = "Project Name" and type = "Defect" AND status in ("In Test", "QA Ready") ORDER BY assignee'
data = jira.jql(query)

#print (json.dumps(data, indent = 4))

sHeader = getHtmlHeader("Key", "Summary", "Assigned To", "Creator", "Priority", "Status", "Comments")
sRows = ""
sLastTo = ""

for issue in data["issues"]:
    if (issue["fields"]["issuetype"]["name"] == "Enhancement"):
    
        sKey = GetHtmlColumn(issue["key"])
        sCreator = issue["fields"]["creator"]["name"]
        print (issue["fields"]["creator"]["emailAddress"])
        print (issue["fields"]["summary"])
        print (issue["fields"]["description"])
        print (issue["fields"]["priority"]["name"])
        print (issue["fields"]["status"]["name"])
        print (issue["fields"]["created"])


    if (issue["fields"]["issuetype"]["name"] == "Defect"):
    
        sKey = issue["key"]
        sCreator = issue["fields"]["creator"]["name"]
        sAssignedToName = issue["fields"]["assignee"]["name"]
        sAssignedToEmail = issue["fields"]["assignee"]["emailAddress"]
        sSummary = issue["fields"]["summary"]
        sDescription = issue["fields"]["description"]
        sPriority = issue["fields"]["priority"]["name"]
        sStatus = issue["fields"]["status"]["name"]
        sCreationDate = issue["fields"]["created"]
        intCommentCount = issue["fields"]["comment"]["total"]
        sLastComment = ""                   
        if (intCommentCount > 0):
            #print (issue["fields"]["comment"]["comments"][intCommentCount - 1]["author"]["displayName"])
            #print (issue["fields"]["comment"]["comments"][intCommentCount - 1]["author"]["emailAddress"])
            sLastComment = issue["fields"]["comment"]["comments"][intCommentCount - 1]["body"]
            #print (issue["fields"]["comment"]["comments"][intCommentCount - 1]["updated"])
        
        print ("{},{}".format(sLastTo, sAssignedToEmail))
        if (sLastTo is ""):
            sLastTo = sAssignedToEmail
                
        if (sLastTo != sAssignedToEmail):
            if (sRows != ""):
                subject = "ACTION REQUIRED :: Project Name - QA Ready Jira Ticket(s)"
                body = GetHtmlEmail(sHeader, sRows)
                #print (body)

                SendEmail(sFrom, sLastTo, subject, body)
                sLastTo = sAssignedToEmail
                sRows = ""

        sRows = sRows + getHtmlRow(sKey, sSummary, sAssignedToName, sCreator, sPriority, sStatus, sLastComment)
                     
if (sRows is not ""):
    subject = "ACTION REQUIRED :: Project Name - QA Ready Jira Ticket(s)"
    body = GetHtmlEmail(sHeader, sRows)
    #print (body)
    
    SendEmail(sFrom, sAssignedToEmail, subject, body)
    
print ("Finished")

Combine multiple CSV file(s) into a single CSV file

This Python Script will combine all the files in a Sub Folder called “/files” into a single file. Script assumes that the folder only has CSV file. The rest of the code is self explanatory.

import csv
import os
from os import listdir
from os.path import isfile, join

#sHasHeader when true, retains the first line as a Header and is carried over once into the output file
def Combine(sPath, sOutputFile, sHasHeader):
    headerRow = ""
    
    #find all files within the sub-directory 
    files = [f for f in listdir(sPath) if isfile(join(sPath, f))]
    
    with open(sOutputFile, 'w', newline='') as oFile:
        writer = csv.writer(oFile, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL)

        for file in files:
            file = os.path.join(sPath, file)
            print (file)

            with open(file, newline='') as csvfile:
                reader = csv.reader(csvfile, delimiter=',', quotechar='"')
                if (sHasHeader == True):
                    for row in reader:
                        if (headerRow == ""):
                            headerRow = row
                            writer.writerow(row)
                        else:
                            writer.writerow(row)
                else:
                    for row in reader:
                        writer.writerow(row)

#declare all functions above this line
mypath = "./files/" 
outputfile = "combined.csv" 
Combine (mypath, outputfile, True)

Oracle Service Cloud :: Python, Data Export to CSV using a Report

I wrote this script to load all the Knowledge base Articles or Answers published in Oracle Service Cloud, parse and extract URL’s for further analysis (links to Migrated sites etc).

Use a recently cloned test site to run this script.

This sample script covers the following scenarios

  • Authentication
  • Executing an Oracle Service Cloud Report
  • Extracting Json formatted report output
  • Parsing the output
  • Loading a HTML document (Answer) from a URL
  • Extract URL’s  using BeautifulSoup
  • Create a CSV for Analysis

We need a list of Answers Id’s to work with. A report was created to expose all the Answer ID’s (Public). LastID is a custom filter for the answers.a_id (AnswerId) column. By Default this report return all answers id > 0. The report is sorted in Ascending Order by answers.a_id. Save this report with necessary execute permissions for the profile used by the Integration account used in the Script. Write down the Report ID, we will need this to modify the GetAnswerIDs function.


GetAnswerIDs(LastID) function
The Script connects to Oracle Service Cloud and executes a report. The report returns Json data. 0 is passed to LastID when this function is called the first time. The report will then return the first 1000 record (or as limited per call), returning answers that are greater than the last Answer Id. Subsequent calls to this function will pass the last Answer Id (see use of variable “gLastAnswerId”). This provides a mechanism to recursively call the report and load incremental data using the Answer Id as a Filter.

To get this script to work, you have to modify some of the variable.

Modify the “Url” variable and replace “your-domain” with the correct domain.

Update the “data” variable with the correct id (replace 110715 with the report id saved from the report that was created earlier)

Provide valid credentials using the OSCUserName and OSCPassword variables. This is case sensitive and the account should have necessary permissions to execute the Analytics Report API.

Continuing from while (gContinue):
Let’s continue with the rest of the Script.

Experiment with json.dumps(gData, indent=4)) to pretty print Json responses. Some comments are intentionally left to assist with understanding the script better.

Modify the variable “answerUrl” and replace “your-domain” with the correct Url to point to your KBA.

The Answer Id is then used to build a URL. The Url is then used to download the Answer from the public facing UR. We could have fetched actual field that stores the Answer or Summary. The Url based approach is better if the answer uses Answer Variables / substitution variables or dynamic content as in my case. The HTML is then parsed using BeautifulSoup library.

The KB section was embedded within a particular DIV tag with a Unique class identifier. You may have to change the for loop below to identify html tags by Id or Class depending on your site’s HTML content.

The for loop below finds these sections

for section in soup.find_all('div', {"class":"positioner"}):

The line below is to avoid any Relative URL’s from being picked up.

if (url.startswith('http')):
import requests
from bs4 import BeautifulSoup
from io import BytesIO
import base64
import json
from urllib.parse import urlparse
from urllib.parse import unquote
import csv

global gResponse, gData, writer, gContinue, gLastAnswerId

gContinue = True
gLastAnswerId = 0

def LoadResponse(Url):
    response = requests.get(Url)
    return response

def GetAnswerIDs(LastID):
    Url = r"https://your-domain/services/rest/connect/v1.4/analyticsReportResults/"
    data = {"id":110715,"filters":[{"name":"LastID","values":"{}".format(LastID)}]}
    OSCUserName = "USERNAME";
    OSCPassword = "PASSWORD";
    credentials = r"{}:{}".format(OSCUserName, OSCPassword);
    base64_bytes = base64.b64encode(credentials.encode('ascii'))
    Authorization = base64_bytes.decode("ascii");

    
    headers = {"Content-type":"application/json",
               "Authorization":"Basic {}".format(Authorization), 
               "OSvC-CREST-Application-Context":"App", 
               "OSvC-CREST-Suppress-Rules":"true"};
    response = requests.post (Url, json=data, headers=headers)
    return response

#declare all functions above this line

print ("Starting...")

while (gContinue):
    
    gContinue = False
    
    print ("Loading Answers > {}".format(gLastAnswerId))
    gResponse = GetAnswerIDs(gLastAnswerId)
    gData = gResponse.json()
    #print (json.dumps(gData, indent=4))
            
    with open("urls.csv", "w") as writer:
        writer.write("Answer ID, KB Link, Host Name, Url,Display Text,  Scheme, Path, Query String\n")

        #writer = csv.writer(file, delimiter=',')

        for r in gData["rows"]:
           
            gContinue = True; #if we find records, we may have to loop this once more... 
            answerID = str(r[0])

            print ("Processing Answer Id : {}".format(answerID))
            if int(answerID) > int(gLastAnswerId):
                gLastAnswerId = answerID
            
            answerUrl = r"https://your-domain.custhelp.com/app/answers/detail/a_id/{}".format(answerID)
            print (answerUrl)
            response = LoadResponse(answerUrl)
            
            soup = BeautifulSoup(response.text, "html.parser")

            for section in soup.find_all('div', {"class":"positioner"}): #KB content is within this element + Class. Helps to avoid the header and footer

                for a in soup.find_all('a', href=True):
                    url = a['href']
                    url = unquote(url)
                    if (url.startswith('http')):
                        #print (url)
                        o = urlparse(url)
                        writer.write("{},{},{},{},{},{},{}\n".format(answerID, answerUrl, o.hostname, url, a.string, o.scheme, o.path, o.query))
            
print ("Finished.")

Hope this was helpful and leave your comments below

Oracle Service Cloud :: Python, Data Export to CSV using Connect PHP

To get started download Anaconda Navigator, Launch Jupyter Notebook, Create a new Python Script file and paste the code below. Make appropriate changes and you are good to go. YouTube has many videos to help with Anaconda and first steps with Python.

This script is only a start and does not help download large tables, like the Incident or Messages table. It covers the basics of connecting to Oracle Service Cloud, Connect PHP Web Services using an Integration Account, download and save the results into a well formatted CSV File .

Modify the variable “Url” and replace the text “your-site” to point to the correct test / prod site. This part of the url will be the same as your Customer Portal or Agent Web url.

Modify the Variables OSCUserName and OSCPassword and provide the case-sensitive user-name and password.

The account used to make this connection must have necessary access to the object and to use Connect Web Services. Reach out to your OSvC Admin to have this setup prior to running this script.

A file will be created with the name specified in “fileName”. This file will be created in the same folder as the script. Change this as appropriate.

Connect Web Services only return 20K records per call (review documentation for other governor limits). This script may have to be modified to manage recursive call. Will cover it in another script or if you have working example, don’t forget to share your comments.

import requests
from io import BytesIO
import base64
import json
import csv

#Code needs to be enhanced with Logging, Exception Handling and Logic to manage large or recursive data sets / Queries

#GerResponse expects a Query and return a Json formatted Response.
#use Print statments with json.dumps to understand the Json Structure
#this method is called by GenerateCsvFile
def GetReponse(query):
    Url = r"https://your-site.custhelp.com/services/rest/connect/v1.4/queryResults/?query={}".format(query)
    OSCUserName = "USERNAME";
    OSCPassword = "PASSWORD";
    credentials = r"{}:{}".format(OSCUserName, OSCPassword);
    base64_bytes = base64.b64encode(credentials.encode('ascii'))
    Authorization = base64_bytes.decode("ascii");

    
    headers = {"Content-type":"application/json",
               "Authorization":"Basic {}".format(Authorization), 
               "OSvC-CREST-Application-Context":"App", 
               "OSvC-CREST-Suppress-Rules":"true"};
    #response = requests.post (Url, json=data, headers=headers)
    response = requests.get(Url, headers=headers)
    return response

#provide the Query and the output file name to GenerateCsvFile
def GenerateCsvFile(query, outputFileName):
    iRow = 0

    gResponse = GetReponse(query)
    gData = gResponse.json()
    #print (json.dumps(gData, indent=4))
    #print (gData["items"][0]["rows"])

    with open(outputFileName, 'w', newline='') as file:
        writer = csv.writer(file, delimiter=',', quotechar='"', quoting=csv.QUOTE_ALL)
        writer.writerow (gData["items"][0]["columnNames"]) #Column Headers

        for r in gData["items"][0]["rows"]:
            iRow = iRow + 1
            writer.writerow (r) #Rows

    return iRow

#declare all functions above this line

print ("Starting...")
query = "select * from accounts" #if we need custom columns to be returned, make this Query to return Explicit Columns
fileName = "accounts.csv" #this file will be saved in the same directory as the Script
rows = GenerateCsvFile(query, fileName)
print ("{} Row(s) Exported".format(rows))
print ("Success")

 

APPLICATION_FAULT_NULL_POINTER_READ_INVALID_POINTER_READ – IIS Worker Process stopped working and was closed –

Ran into another Weird issue today. An ASP.NET 4.0 website started throwing an Exception today. The weird part was that only one of the pages would throw a Network Error (tcp_error, not a 500 or 503) one time. The rest of the site seemed to work normally up until navigating to this particular page. After navigating to this page, the Web site become unavailable and start throwing a 503. The Application EventLog did provide clues to the failure. I just wasn’t looking hard enough. If you are looking for the solution, please skip the rest of the section and jump to the last paragraph.

Faulting application name: w3wp.exe, version: 7.5.7601.17514, time stamp: 0x4ce7a5f8
Faulting module name: clr.dll, version: 4.7.2053.0, time stamp: 0x58fa6bb3
Exception code: 0xc0000005
Fault offset: 0x00192f7b
Faulting process id: 0x96c
Faulting application start time: 0x01d2fe12bb42ecc2
Faulting application path: C:\Windows\system32\inetsrv\w3wp.exe
Faulting module path: C:\Windows\Microsoft.NET\Framework\v4.0.30319\clr.dll

Attaching WinDbg to a running instance of w3wp, a stack trace, confirmed that the CLR crashed. Little bit of googling around showed that this might be a Heap corruption, but nothing concrete. A similar issue was reported to Microsoft, but seems to be open at this point.

0:032> !analyze -v

STACK_TEXT: 
12d6e400 7079005e 12d6e740 0000000a 5516af7c clr!BlobToAttributeSet+0x63
12d6e878 6f0f910b 12d6e894 00000001 07625170 clr!COMCustomAttribute::GetSecurityAttributes+0x265
12d6e8a4 6f0f8f56 12d6e8bc 00000001 6f225210 mscorlib_ni+0x3c910b
12d6e8cc 6f16d7ea 03624f34 6d1af3e3 00000000 mscorlib_ni+0x3c8f56
12d6e8fc 6d1af39e 00000000 03624f34 12d6e948 mscorlib_ni+0x43d7ea
12d6e90c 6d3b5145 12d6e8b4 0b31f594 70787cd8 System_Web_Extensions_ni+0x5f39e
12d6e948 6d3b4c0c 036183dc 12d6ecb8 12d6e970 System_Web_Extensions_ni+0x265145
12d6e958 10444234 0361d190 00000000 00000000 System_Web_Extensions_ni+0x264c0c
WARNING: Frame IP not in any known module. Following frames may be wrong.
12d6e970 104416a3 00000000 00000000 00000000 0x10444234
12d6ecc0 015ff870 00000000 00000000 00000000 0x104416a3
12d6ed04 015ff735 00000000 00000000 00000000 0x15ff870
12d6ed2c 6adfc921 036183dc 036183dc 00000000 0x15ff735
12d6ed5c 6adfc8a9 00000001 03472ad8 03472ad8 System_Web_ni+0x21c921
12d6ed94 6adfc857 036183dc 12d6edb4 6adfc83b System_Web_ni+0x21c8a9
12d6eda0 6adfc83b 00000000 0761bec8 034b5aa8 System_Web_ni+0x21c857
12d6edb4 015ff676 0761bec8 036183dc 12d6ee08 System_Web_ni+0x21c83b
12d6edc4 6adff96d 12d6ee08 6adf637b 034b243c 0x15ff676
12d6ee08 6adb9ec6 6adcc959 6f0e1af6 00000000 System_Web_ni+0x21f96d
12d6ee44 6adc813d 12d6eef0 00000000 00000000 System_Web_ni+0x1d9ec6
12d6ef28 6adba850 00000000 00000000 12d6ef80 System_Web_ni+0x1e813d
12d6ef3c 6adc6ccb 034a7e54 12d6ef74 12d6f02c System_Web_ni+0x1da850
12d6ef8c 6adbb6ef 0761bec8 47cb644c 706ff6e8 System_Web_ni+0x1e6ccb
12d6f084 6adbb39f 00000000 0000000c 00000002 System_Web_ni+0x1db6ef
12d6f0ac 00c1de6a 00000000 0000000c 6ae10878 System_Web_ni+0x1db39f
12d6f0dc 71acac76 0124106c 01288c08 0000000c 0xc1de6a
12d6f100 71acbc75 0128990c 712e3863 00000000 webengine4!W3_MGD_HANDLER::ProcessNotification+0x62
12d6f12c 71acac15 00000080 00000000 02feb79c webengine4!W3_MGD_HANDLER::DoWork+0x32a
12d6f174 71b29a88 00000080 00000000 02feb79c webengine4!RequestDoWork+0x39f
12d6f194 712e03b5 012883b0 01288408 012883b4 webengine4!CMgdEngHttpModule::OnExecuteRequestHandler+0x18
12d6f1a8 712e11c0 02feb79c 012883b0 00000000 iiscore!NOTIFICATION_CONTEXT::RequestDoWork+0x128
12d6f220 712cdf13 00000000 00000000 00000000 iiscore!NOTIFICATION_CONTEXT::CallModulesInternal+0x305
12d6f268 712d068b 00000000 00000000 00000000 iiscore!NOTIFICATION_CONTEXT::CallModules+0x28
12d6f28c 712d267b 00000000 00000000 00000000 iiscore!W3_CONTEXT::DoStateRequestExecuteHandler+0x36
12d6f4f4 712d62e4 00000000 00000000 00000001 iiscore!W3_CONTEXT::DoWork+0xd7
12d6f514 712d633d 00000000 00000000 712d0765 iiscore!W3_MAIN_CONTEXT::ContinueNotificationLoop+0x1f
12d6f528 712d07da 00000000 012883b0 12d6f54c iiscore!W3_MAIN_CONTEXT::ProcessIndicateCompletion+0x1f
12d6f538 71b25abd 00000000 12d6f65c 71b27e10 iiscore!W3_CONTEXT::IndicateCompletion+0x75
12d6f54c 71b27e32 00000000 0303cc58 12d6f59c webengine4!W3_MGD_HANDLER::IndicateCompletion+0x45
12d6f55c 6ae0e181 01288c08 12d6f65c 47cb644c webengine4!MgdIndicateCompletion+0x22
12d6f59c 6adbb892 47cb644c 706ff6e8 12d6f778 System_Web_ni+0x22e181
12d6f690 6adbb39f 00000004 0000000d 0303cc58 System_Web_ni+0x1db892
12d6f6b8 00c1de6a 00000004 0000000d 6ae10878 System_Web_ni+0x1db39f
12d6f6d8 70724861 12d6f898 00000010 00c1de48 0xc1de6a
12d6f738 70724792 12d6f82c 5516bf00 0fcb6236 clr!UM2MThunk_Wrapper+0x76
12d6f804 707246fa 00000002 707247f0 12d6f82c clr!Thread::DoADCallBack+0xbc
12d6f85c 00c1dec3 0fcb6220 00c1de48 12d6f898 clr!UM2MDoADCallBack+0x92
12d6f890 71acac76 00000000 01288c08 0000000d 0xc1dec3
12d6f8b4 71acacd3 70d6d368 03053068 01288c08 webengine4!W3_MGD_HANDLER::ProcessNotification+0x62
12d6f8c8 70715423 01288c08 5516bfdc 70d6d368 webengine4!ProcessNotificationCallback+0x33
12d6f914 70711d53 12d6f945 12d6f947 0303cc58 clr!UnManagedPerAppDomainTPCount::DispatchWorkItem+0x1d6
12d6f92c 707119f9 5516be3c 707118d0 00000000 clr!ThreadpoolMgr::ExecuteWorkRequest+0x4f
12d6f994 7089de11 00000000 ffffffff ffffffff clr!ThreadpoolMgr::WorkerThreadStart+0x3d3
12d6fcb4 75ff336a 030533c0 12d6fd00 775f9902 clr!Thread::intermediateThreadProc+0x55
12d6fcc0 775f9902 030533c0 65b1ffb7 00000000 kernel32!BaseThreadInitThunk+0xe
12d6fd00 775f98d5 7089ddc0 030533c0 ffffffff ntdll!__RtlUserThreadStart+0x70
12d6fd18 00000000 7089ddc0 030533c0 00000000 ntdll!_RtlUserThreadStart+0x1b

Next step was to load sos.dll using (Right Click, Launch WinDbg as an Administrator),

.load C:\Windows\Microsoft.NET\Framework\v4.0.30319\sos.dll

Reviewing the .NET Stack Trace using clrstack

0:034> !clrstack
OS Thread Id: 0xee0 (34)
Child SP IP Call Site
13a2e264 6eb52f7b [HelperMethodFrame_2OBJ: 13a2e264] System.Reflection.PseudoCustomAttribute._GetSecurityAttributes(System.Reflection.RuntimeModule, Int32, Boolean, System.Object[] ByRef)
13a2e6a0 6bac910b System.Reflection.PseudoCustomAttribute.GetCustomAttributes(System.Reflection.RuntimeAssembly, System.RuntimeType, Boolean, Int32 ByRef)
13a2e6cc 6bac8f56 System.Reflection.CustomAttribute.GetCustomAttributes(System.Reflection.RuntimeAssembly, System.RuntimeType)
13a2e6ec 6bb3d7ea System.Reflection.RuntimeAssembly.GetCustomAttributes(Boolean)
13a2e6f4 662cf3e3 System.Web.UI.AssemblyCache.SafeGetAjaxFrameworkAssemblyAttribute(System.Reflection.ICustomAttributeProvider)
13a2e71c 662cf39e System.Web.UI.AssemblyCache.GetAjaxFrameworkAssemblyAttribute(System.Reflection.Assembly)
13a2e72c 664d5145 System.Web.UI.ScriptManager.get_DefaultAjaxFrameworkAssembly()
13a2e768 664d4c0c System.Web.UI.ScriptManager..ctor()
13a2e778 01f2dfaa ASP.JethroGibbs_aspx.__BuildControlScriptManager1()
13a2e788 01f2afc6 ASP.JethroGibbs_aspx.__BuildControlJB()
13a2eb10 01f298af ASP.JethroGibbs_aspx.__BuildControlTree(ASP.JethroGibbs_aspx)
13a2eb24 01f2939e ASP.JethroGibbs_aspx.FrameworkInitialize()
13a2eb30 66cfc921 System.Web.UI.Page.ProcessRequest(Boolean, Boolean)
13a2eb64 66cfc8a9 System.Web.UI.Page.ProcessRequest()
13a2eb98 66cfc857 System.Web.UI.Page.ProcessRequestWithNoAssert(System.Web.HttpContext)
13a2eba4 66cfc83b System.Web.UI.Page.ProcessRequest(System.Web.HttpContext)
13a2ebb8 01f29335 ASP.JethroGibbs_aspx.ProcessRequest(System.Web.HttpContext)
13a2ebbc 66cff96d System.Web.HttpApplication+CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
13a2ec00 66cb9ec6 System.Web.HttpApplication.ExecuteStep(IExecutionStep, Boolean ByRef)
13a2ec40 66cc813d System.Web.HttpApplication+PipelineStepManager.ResumeSteps(System.Exception)
13a2ed20 66cba850 System.Web.HttpApplication.BeginProcessRequestNotification(System.Web.HttpContext, System.AsyncCallback)
13a2ed38 66cc6ccb System.Web.HttpRuntime.ProcessRequestNotificationPrivate(System.Web.Hosting.IIS7WorkerRequest, System.Web.HttpContext)
13a2ed88 66cbb6ef System.Web.Hosting.PipelineRuntime.ProcessRequestNotificationHelper(IntPtr, IntPtr, IntPtr, Int32)
13a2ed8c 66cbb39f [InlinedCallFrame: 13a2ed8c] 
13a2ee84 66cbb39f System.Web.Hosting.PipelineRuntime.ProcessRequestNotification(IntPtr, IntPtr, IntPtr, Int32)
13a2f360 0123de6a [InlinedCallFrame: 13a2f360] 
13a2f35c 66d0e181 DomainNeutralILStubClass.IL_STUB_PInvoke(IntPtr, System.Web.RequestNotificationStatus ByRef)
13a2f360 66cbb892 [InlinedCallFrame: 13a2f360] System.Web.Hosting.UnsafeIISMethods.MgdIndicateCompletion(IntPtr, System.Web.RequestNotificationStatus ByRef)
13a2f394 66cbb892 System.Web.Hosting.PipelineRuntime.ProcessRequestNotificationHelper(IntPtr, IntPtr, IntPtr, Int32)
13a2f398 66cbb39f [InlinedCallFrame: 13a2f398] 
13a2f490 66cbb39f System.Web.Hosting.PipelineRuntime.ProcessRequestNotification(IntPtr, IntPtr, IntPtr, Int32)
13a2f568 0123de6a [ContextTransitionFrame: 13a2f568]

Looking into the Script Manager didn’t yield much, since the application in itself hasn’t changed in a while. Going back to the CLR and System_Web_ni.dll, I did notice the files were updated few hours ago (I should have noticed the CLR version in the EventLog, my bad).

Using the .NET Framework Setup Verification Tool I noticed .NET Framework 4.7 installed. We had only certified the app to run on 4.6.1. .NET Framework 4.7 wasn’t listed as part of  “View Installed updates”, but as a standalone component. The only drawback is that the AppPool’s .NET Framework Version will be reset to 2.0 after an uninstall. It would be worth taking a screenshot of your IIS AppPool Framework versions prior to  uninstalling. Downloaded the Standalone .NET Framework 4.6 installer, uninstalled 4.7 and installed 4.6.1. After a reboot and reconfiguration of the AppPool and an IISReset returned the Application back to a working state.

 

Memory Leak – Microsoft.Reporting.WinForms.LocalReport

Several weeks ago I was asked to assist with a sluggish ASP.NET C# MVC application. This application is an OLTP system and processes several thousand orders a day. It was hosted on a Windows 2012 R2 64bit 16 Core machine. After discussing with the developer, I was told that the application has acceptable levels of performance for a while and then becomes sluggish (this behavior suggests some kind of  a leak). At times, it becomes extremely slow and rebooting the machine solves the problem. The problem exhibits itself more frequently during days when there were above average volumes.

Downloading Process Explorer from SysInternals, I found the w3wp.exe (there were several, but 2 of them stood out). Process Explorer does not display handle information by default. I will probably blog about this separately. There are many articles out there that explain how to look for Handles using Process Explorer. This should get you started. Here is another excellent blog explaining the theoretical limit of handles. It’s a theoretical limit because there are many types of thresholds that an application can reach which causes severe performance degradation.

I knew that we had a potential handle leak. There were also other issues within the application, that were not related to the handle leak. The challenge was to isolate the handle leak versus defects in the program logic, 3rd party hardware behaviors (like non genuine printer cartridges) and various other user errors.  Debugging a live system with several 100 users connected wasn’t happening. We were able to get a 4.5Gb memory dump.

When I moved the memory dump to a machine where I could analyze it, I found that the Production server was running an older version of the .NET Framework. The MS Symbol server wasn’t much help. I ended up copying the complete framework64 folder to the local machine (a different folder, not the Microsoft.NET\Framework folder) and loaded SOS using the .load command.

01-load sos
Using the time command, the overall CPU utilization by the Process compared to the System uptime showed that it wasn’t CPU contention.

02-time

The dumpheap command displays the managed heap. The output contains the Method Table, Object Instance Count, Memory Allocated and the Type from left to right. !dumpheap -stat reported about 164k lines of output and 9.7M object references. Many of these objects pointed to dynamically created Report Expressions.

01-dumpheap -stat

Typically the next logical step is to use !dumpheap -mt <<method table address>> to display all the individual object instances and then reviewing the individual objects to understand them better.

02-dumpheap -stat

Looking for loaded modules using the !eeheap -loader command showed that the worker process had 2622 App Domains (with a heapsize of 537mb). Dumping one of the modules using !DumpModule command pointed to a Dynamically loaded Expression Library associated with SQL Server Reporting Services.

05-eeheap -loader06-eeheap -loader

At this point, I knew that the issue may be related to the Dynamically Loaded Expression Assembly and its associated AppDomain that failed to unload. What was keeping the AppDomain from unloading? The dynamic expression library is used by the LocalReport object, which is part of the Microsoft.Reporting.Winforms namespace.

Further research showed that creating a new AppDomain is per design. The developers @ Microsoft had identified an issue with the dynamic expression library being loaded into the Default AppDomain and using up more memory each time the LocalReport object was created. An assembly once loaded into an AppDomain can never be unloaded. Hence the dynamic assembly was loaded in a new AppDomain and if all goes well, the AppDomain can be safely unloaded. As per the documentation calling LocalReport.ReleaseSandBoxAppDomain should unload the AppDomain. Unfortunately, the unload never happens because there is a problem with the internal implementation of the Dispose Method.

Using .NET Memory Profiler (JetBrains dotPeek), I was able to identify the Event Delegate

Screen Shot 2017-04-11 at 11.13.42 AM

Looking at the implementation, the handlers are not removed in the Dispose method. This in turn keeps the AppDomain from unloading. Hope this gets resolved in a future release. For now, we are recycling the Application Pool more frequently to work around the memory leak and the developers are looking at an alternate solution to print the labels.

08

One of the questions that I was asked while presenting my findings with the project manager, is why did this suddenly become a problem, since the application had been in Production for over 2 years. The issue had been there from the very beginning. The OOTB Application Pool Recycle was  causing the host process to shutdown after a period of inactivity (this app was used only between 8am-5pm). So it had enough time to shutdown after hours and the problem would never surface until the transaction volume increased, at which point the application started hitting thresholds.

It always helps to set baselines. Keep tabs on System configuration, Transaction Volume and major system change events. This will greatly shorten the time it takes to troubleshoot performance issues.

 

 

WinDbg – Welcome to Logging

Performing dump analysis is often times challenging due to the nature of the beast, the UI or the lack thereof :). Some commands are extremely time consuming or like me, just hate scrolling. Also there is only so much that you can view by scrolling.

Logging can be enabled before running commands that produce tons of output. We can also create seperate log files for each command by setting up separate log files before executing each command. From here, we can open the log files in an External Editor like Notepad++ and work with WinDbg on another monitor.

To enable Logging, goto the Edit Menu -> Open/Close Log File…

01

provide the Log file name and click ok. You are all done. Check the Append checkbox to append to an existing file or else the file gets overwritten.

02

After I am done capturing the logs that I need, I come back and Close Open Log File button to close the open file. This disables logging, while I take the file offline for Analysis.

The output can also be copied to excel and formatted to quickly sift though.

.foreach WinDbg

Recursively executing a command in WinDbg is one of the coolest features. In the example below, I used one method table from the output of a !FinalizeQueue command. The FinalizeQueue similar to DumpHeap provides the Method Table, Object Count, Size allocated and the Type.

  • Here 6dcf1230 is one of the Method Tables that I was interested in. I wanted to see if any objects in this table were rooted.
  • myvar is a variable that holds the output of the !DumpHeap command. –short returns only the address. Maybe in another article I will cover how to process the tokens
  • the .echo command needs no introduction, in this content it is used twice to display the address and to separate the output.
.foreach (myvar {!DumpHeap /d -mt 6dcf1230 -short}) {.echo myvar;!gcroot -all myvar;.echo **************;}