Neerav, I'll just note that if you use Jenna's suggestion your code doesn't need to be a part of a business service -- it can be any normal ObjectScript code and can be called from anywhere (CSP, scheduled task, etc.). And your code can make as many calls as it needs to and can receive response messages.

It will appear in message traces as if a business service sent requests to the process or operation, but in reality it's just your code sending the requests.

Jenna's approach is a good one, and it's the standard way to achieve what you describe. Having said that, if you can provide more details on your use case and/or why this approach doesn't fit your need we can help you explore alternatives.

My understanding is that the Caché application is terminal based (accessed on the client PC through telnet).

The signature device connects to the client PC using USB.

So the question/challenge is for the terminal-based application to send a request to the signature device when a signature is needed and to receive the signature image.

Seems like you need a daemon running on the PC which uses the signature device's SDK to communicate with it. The daemon would then exchange messages with Caché to manage the signature process -- possibly via web services or web sockets.

You could use Apache's mod_rewrite to take all requests that fall under a certain sub-path and transform them on the fly to point to your CSP page. It could add the information about which specific page/resource was requested as a URL parameter that could be accessed by your CSP page.

For example, if a client makes a request for:
http://myserver/reporting/Dashboard1/resource2.js

mod_rewrite could change it to:
http://myserver/csp/dashboards/proxy.csp?targetResource=Dashboard1/resou...

After mod_rewrite changes the URL in the request, Apache continues processing it as usual using the new URL. Since the new URL refers to a CSP page Apache will pass it to CSP Gateway as we want.

Have a look at this documentation on Virtual Property Path syntax.

Essentially, if you specify an asterisk "*" inside the parentheses for a repeating segment it will return a count of how many of those segments exist in the document.

<assign value='source.{PID:3("*")}' property='pid3Count'/>

In this example, the number of repetitions that exist in PID:3 will be stored in the variable pid3Count.

You could then use source.{PID:3(pid3Count)} to refer to the last item in PID:3.

Here's a stored procedure that accepts a setting name and returns the setting value for all components that have it. It's not SQL, but can be executed from SQL :)

You can call it this way -- this example returns the port setting for all components that have it:

call Sample.Util_SettingsByName('Port')

Here's the source code as XML export format. Copy this into a file and then import it using Studio, terminal, or the System Management Portal.

<?xml version="1.0" encoding="UTF-8"?>
<Export generator="Cache" version="25" zv="Cache for Windows (x86-64) 2016.1.1 (Build 108U)" ts="2016-10-12 16:15:39">
<Class name="Sample.Util">
<Super>%RegisteredObject</Super>
<TimeChanged>64203,58380.929948</TimeChanged>
<TimeCreated>64202,44682.614801</TimeCreated>

<UDLText name="T">
<Content><![CDATA[
/*
         *****************************************************
         *                 ** N O T I C E **                 *
         *               - TEST/DEMO SOFTWARE -              *
         * This and related items are not supported by       *
         * InterSystems as part of any released product.     *
         * It is supplied by InterSystems as a demo/test     *
         * tool for a specific product and version.          *
         * The user or customer is fully responsible for     *
         * the maintenance of this software after delivery,  *
         * and InterSystems shall bear no responsibility nor *
         * liabilities for errors or misuse of this item.    *
         *                                                   *
         *****************************************************
*/
]]></Content>
</UDLText>

<Query name="SettingsByName">
<Type>%Query</Type>
<FormalSpec>SettingName:%String</FormalSpec>
<SqlProc>1</SqlProc>
<Parameter name="ROWSPEC" value="BusinessHost:%String,SettingName:%String,SettingValue:%String"/>
</Query>

<Method name="SettingsByNameExecute">
<ClassMethod>1</ClassMethod>
<FormalSpec><![CDATA[&qHandle:%Binary,SettingNames:%String=""]]></FormalSpec>
<ReturnType>%Status</ReturnType>
<Implementation><![CDATA[
    s qHandle=##class(%ArrayOfObjects).%New()

    &sql(select %DLIST(id) into :tHostIDs from ENS_Config.Item order by Name desc)
    s tHostIDList=##class(%Library.ListOfDataTypes).%New()
    s tSC=tHostIDList.InsertList(tHostIDs)
    s tSC=qHandle.SetAt(tHostIDList,"HostIDs")

    s tSC=qHandle.SetAt(##class(%ArrayOfDataTypes).%New(),"Counters")
    s tSC=qHandle.GetAt("Counters").SetAt(0,"CurrHost")
    s tSC=qHandle.GetAt("Counters").SetAt(0,"CurrSetting")
    
    if ($L(SettingNames)>1) {
        s SettingNames=$ZCONVERT(SettingNames,"U")
        s tFilterList=##class(%Library.ListOfDataTypes).%New()
        s tSC=tFilterList.InsertList($LISTFROMSTRING(SettingNames))
         s tSC=qHandle.SetAt(tFilterList,"FilterList")
    }

    Quit $$$OK
]]></Implementation>
</Method>

<Method name="SettingsByNameClose">
<ClassMethod>1</ClassMethod>
<FormalSpec><![CDATA[&qHandle:%Binary]]></FormalSpec>
<PlaceAfter>SettingsByNameExecute</PlaceAfter>
<ReturnType>%Status</ReturnType>
<Implementation><![CDATA[    Quit $$$OK
]]></Implementation>
</Method>

<Method name="SettingsByNameFetch">
<ClassMethod>1</ClassMethod>
<FormalSpec><![CDATA[&qHandle:%Binary,&Row:%List,&AtEnd:%Integer=0]]></FormalSpec>
<PlaceAfter>SettingsByNameExecute</PlaceAfter>
<ReturnType>%Status</ReturnType>
<Implementation><![CDATA[
    s tCurrHost=qHandle.GetAt("Counters").GetAt("CurrHost")
    s tCurrSetting=qHandle.GetAt("Counters").GetAt("CurrSetting")
    s tHostIDs=qHandle.GetAt("HostIDs")
    s tFilterList=qHandle.GetAt("FilterList")
    s oHost=qHandle.GetAt("Host")

    do {
        if ('$IsObject(oHost)||(oHost.VirtualSettings.Count()<tCurrSetting)) {
            if (tCurrHost=tHostIDs.Count()) {
                s AtEnd=1
                q
            }

            s tCurrHost=tCurrHost+1
            s tCurrSetting=1
            
            s tHostID=tHostIDs.GetAt(tCurrHost)
            s oHost=##class(Ens.Config.Item).%OpenId(tHostID,0)
            
            s tSC=oHost.PopulateVirtualSettings()

            s tSC=qHandle.SetAt(oHost,"Host")                
            s tSC=qHandle.GetAt("Counters").SetAt(tCurrHost,"CurrHost")
        }

        s tSettings=oHost.VirtualSettings
        s tSetting=tSettings.GetAt(tCurrSetting)
        s tStngName=$LISTGET(tSetting,2)
        s tStngValue=$LISTGET(tSetting,3)
        
        s tCurrSetting=tCurrSetting+1
    } while ($IsObject(tFilterList)&&('tFilterList.Find($ZCONVERT(tStngName,"U"))))
        
    if ('AtEnd) {
        s Row=$LB(oHost.Name,tStngName,tStngValue)
    }
    
    s tSC=qHandle.GetAt("Counters").SetAt(tCurrSetting,"CurrSetting")
        
    Quit $$$OK
]]></Implementation>
</Method>
</Class>
</Export>

Ens.StreamContainer's %New() method expects a string as the first parameter rather than a stream object.

Something like this should work:

set tRequest=##class(Ens.StreamContainer).%New()
set tSC=tRequest.StreamSet(pInput)

Or if you're trying to send one Ens.StreamContainer for each line from the input file you could do this:

while 'pInput.AtEnd {
    set tReadLength=32000
    set tLine=pInput.ReadLine(.tReadLength,.tSC)
    set tRequest=##class(Ens.StreamContainer).%New(tLine)
    //... do other stuff
}

One other thing you should be aware of. The following will not work if TargetConfigNames has more than one target selected:

set tSC = ..SendRequestAsync(..TargetConfigNames,tRequest)

You should add a loop using $LENGTH and $PIECE and do a SendRequestAsync for each item in TargetConfigNames' comma-separated string.

The resulting XML files can be imported again using %System.OBJ.Load().

If you prefer GOF format you can use %Global.Export() instead, however it doesn't accept wildcards so you would need to first put together a list of which globals you want to export.

For automation you can execute these methods from your own custom class or routine. If you want to schedule it to run automatically, you can create your custom class as a %SYS.Task.Definition and schedule it to run using Task Manager.

You're correct -- you can't restore specific globals from an Online Backup (.cbk) file.

Have a look at %System.OBJ.Export():
https://irisdocs.intersystems.com/irisforhealth20192/csp/documatic/%25CSP.Documatic.cls

TESTING>s itm="*.GBL"
TESTING>s file="C:\Projects\export_mm.xml"
TESTING>d $System.OBJ.Export(.itm,.file,,.errors)

Having said that, since this is just for backups you might also consider just using the standard Online Backup utility which will backup all globals in the selected database(s) into a file:

https://irisdocs.intersystems.com/irisforhealth20192/csp/docbook/DocBook.UI.Page.cls?KEY=GCDI_backup#GCDI_backup_methods_online