Well, that's actually exactly what this plugin is.

<?xml version="1.0" encoding="UTF-8"?>
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
  <ShortName>InterSystems</ShortName>
  <Description>Latest InterSystems documentation search</Description>
  <InputEncoding>UTF-8</InputEncoding>
<Image width="16" height="16">data:image/png;base64,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
</Image>
<Url type="text/html" method="GET" template="http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.SearchPageZen.cls" rel="searchform">
  <Param name="KeyWord" value="{searchTerms}"/>
</Url>
</OpenSearchDescription>

localeopt parameter solved the problem. This Boolean parameter specifies either the user’s current locale definition or the ODBC locale definition as the source for defaults for the locale-specified parameters dformat, monthlist, yearopt, mindate and maxdate:

  • If localeopt=0, all of these parameters take the current locale definition defaults.
  • If localeopt=1, all of these parameters take the ODBC defaults.

For example:

write $zd($h,11,,,,,,,,1)
>Mon

Here's a sample BP that calls BO asynchronously. BO returns  current request state, upon which BP decides to wait some more, report an error or process an answer:

Class test.BP Extends Ens.BusinessProcess
{

/// Operation name
Property Operation As %String(MAXLEN = 250) [ Required ];

/// How long to wait for an answer. 0 - forever.
Property MaxProcessTime As %Integer(MINVAL = 0) [ InitialExpression = 3600 ];

Parameter SETTINGS = "MaxProcessTime:Basic,Operation:Basic:selector?context={Ens.ContextSearch/ProductionItems?targets=1&productionName=@productionId}";

/// Identifier for a first request
Parameter callCOMPLETIONKEY = "*call*";

/// Identifier for a state request
Parameter getStateCOMPLETIONKEY = "*getState*";

/// Alarm identifier
Parameter alarmCOMPLETIONKEY = "*alarm*";

Method OnRequest(pRequest As Ens.StringRequest, Output pResponse As Ens.StringResponse) As %Status
{
    #dim msg As Ens.StringRequest = pRequest.%ConstructClone(1)
    quit ..SendRequestAsync(..Operation, msg, $$$YES, ..#callCOMPLETIONKEY)
}

/// Process Async reply from Operation
Method OnResponse(request As Ens.StringRequest, ByRef response As Ens.StringResponse, callrequest As Ens.StringRequest, callresponse As Ens.StringResponse, pCompletionKey As %String) As %Status
{
    #dim sc As %Status = $$$OK
    
    // Got an error
    if $$$ISERR(callresponse.status)
    {
        set response = callresponse
        quit $$$OK
    }
    
    // Got primary answer
    if (pCompletionKey = ..#callCOMPLETIONKEY) || (pCompletionKey = ..#alarmCOMPLETIONKEY)
    {
        quit ..OnResponseFromCallOrAlarm(request, .response, callrequest, callresponse, pCompletionKey)
    }
    
    // Got getState
    if (pCompletionKey = ..#getStateCOMPLETIONKEY)
    {
        
        // Current processing state (1 - received; 2 - in work; 3 - done)
        #dim status As %String = callresponse.StringValue

        // If not 3, run getState again
        if (status '= "3")
        {        
            // Check how much time passed since we started
            set processTime = $system.SQL.DATEDIFF("s", ..%TimeCreated, $$$ts)
            
            if ((..MaxProcessTime=0) || (processTime<..MaxProcessTime)) {
                // Let's run getState again in 30 seconds
                #dim alarmMsg As Ens.AlarmRequest = ##class(Ens.AlarmRequest).%New()
                set alarmMsg.Duration = "PT30S"
                quit ..SendRequestAsync("Ens.Alarm", alarmMsg, $$$YES, ..#alarmCOMPLETIONKEY)
            } else {
                // Timeout
                set response = ##class(Ens.StringResponse).%New()
                set response.status = $$$ERROR($$$GeneralError, "Timeout")
                quit $$$OK    
            }
        }
        else
        {
            quit ..OnResponseFromGetState3(request, .response, callrequest, callresponse, pCompletionKey)
        }
    }
    
    // unrecognized pCompletionKey value
    quit $$$ERROR($$$InvalidArgument)
}

/// OnResponse for alarm or call - run get state
Method OnResponseFromCallOrAlarm(request As Ens.StringRequest, ByRef response As Ens.StringResponse, callrequest As Ens.StringRequest, callresponse As Ens.StringResponse, pCompletionKey As %String) As %Status [ Private ]
{
    #dim msg As Ens.StringRequest = pRequest.%ConstructClone(1)
    quit ..SendRequestAsync(..Operation, msg, $$$YES, ..#getStateCOMPLETIONKEY)
}

/// OnResponse for an answer
Method OnResponseFromGetState3(request As Ens.StringRequest, ByRef response Ens.StringResponse, callrequest As Ens.StringRequest, callresponse As Ens.StringResponse, pCompletionKey As %String) As %Status [ Private ]
{
    // Process complete response
    quit $$$OK
}

OnResponse would be an automatic observer, and can in turn notify anything else. You can use original request id or SessionID to identify what you need to notify.

From the class documentation on OSUserName:

Operating system username of process.
Username given to the process by the operating system when the process
is created. When displayed, it is truncated to 16 characters. Note that the real O/S
username is only returned when connecting to UNIX or VMS systems; For Windows, it
will return the O/S username for a console process, but for telnet it will return
the $USERNAME of the process. For client connections, it contains the O/S username
of the client. This field is truncated at 16 characters.

_Ensemble is a Caché user under which all Ensemble jobs are run.

If you need to understand the context under which the service runs execute in a BS:

do $zf(-1,"set > vars.txt")

to output all environment variables to a file.

There is no Cache function to do that.

But as PDF contains readable "postscript" parts you can use regexp to search for relevant information. Stack. Article. It's not guaranteed to be precise though.

Here's my article about using LibreOffice for work with documents. I've also used ghostscript and postscript to work with pdf from Caché and it's all fairly straightforward.

Also, here's the code I wrote (execute is defined here) to add footer to every page of a PDF file using ghostscript:

/// Use ghostscript (%1) to apply postscript script %3
/// Upon source pdf (%4) to get output pgf (%2)
/// Attempts at speed  -dProvideUnicode -dEmbedAllFonts=true  -dPDFSETTINGS=/prepress
Parameter STAMP = "%1  -dBATCH -dNOPAUSE -sDEVICE=pdfwrite -sOutputFile=%2 %3 -f %4";

ClassMethod stampPDF(pdf, psFile, pdfOut) As %Status
{
    set cmd = $$$FormatText(..#STAMP, ..getGS(), pdfOut, psFile, pdf)
    return ..execute(cmd)
}

ClassMethod createPS(psFile, text) As %Status
{
    set stream = ##class(%Stream.FileCharacter).%New()
        // For cyrillic text
    set stream.TranslateTable = "CP1251"
    set sc = stream.LinkToFile(psFile)
    quit:$$$ISERR(sc) sc
    
    do stream.WriteLine("<<")
    do stream.WriteLine("   /EndPage")
    do stream.WriteLine("   {")
    do stream.WriteLine("     2 eq { pop false }")
    do stream.WriteLine("     {")
    do stream.WriteLine("         gsave")
    do stream.WriteLine("         /MyFont 12 selectfont")
    do stream.WriteLine("         30 70 moveto (" _ text _ ") show")
    do stream.WriteLine("         grestore")
    do stream.WriteLine("         true")
    do stream.WriteLine("     } ifelse")
    do stream.WriteLine("   } bind")
    do stream.WriteLine(">> setpagedevice")
    
    quit stream.%Save()
}

/// Get gs binary
ClassMethod getGS()
{
    if $$$isWINDOWS {
        set gs = "gswin64c"
    } else {
        set gs = "gs"
    }
    return gs
}

Ghostscript can be used to get a number of pages in a PDF file. Here's how.