Timothy Leavitt · Mar 11, 2016 go to post

One solution would be to look at the audit database. It's not pretty, but there might not be any other way.

ClassMethod GetLoginService() As %String
{
    New $Namespace
    Zn "%SYS"
    
    Set tService = ""
    
    // Ensure that login events are being audited.
    Set tLoginEvent = ##class(Security.Events).Get("%System","%Login","Login",.tProps)
    If '$Get(tProps("Enabled")) {
        // Querying the audit DB probably won't do any good.
        Quit tService
    }
    
    // Warning: on systems with a lot of activity, this query might take a long time.
    // It might be worth filtering by recent UTCTimeStamp, assuming processes won't be that long-running.
    Set tRes = ##class(%SQL.Statement).%ExecDirect(,
        "select top 1 EventData from %SYS.Audit "_
        "where EventSource = '%System' and EventType = '%Login' and Event = 'Login' and PID = ? "_
        "order by UTCTimeStamp DESC, SystemID DESC, AuditIndex DESC",$Job)
    
    Set tHasResult = tRes.%Next()
    If (tHasResult) {
        Set tData = tRes.%Get("EventData")
        //NOTE: This makes assumptions about the format of EventData.
        //Particularly, that it looks something like:
        /*
        Service name:       %Service_Bindings
        Login roles:        %All
        $I:                 |TCP|1972|15396
        $P:                 |TCP|1972|15396
        */
        //
        Set tFirstLine = $Piece(tData,$c(13,10))
        
        //Presumably "Service name:" might be localized, but %Service_<something> would not be.
        Set:tFirstLine["%Service" tService = "%Service"_$Piece(tFirstLine,"%Service",2)
    }
    Quit tService
}

 

Note: if your application is using Caché security correctly, you'd probably need to define a privileged routine application to allow access to Security.Events and the audit database.

Timothy Leavitt · Mar 14, 2016 go to post

This should accomplish what you want:

var tablePane = zen('yourTablePaneId');

// Don't actually execute the query the next time the table is rendered.
tablePane.setProperty('initialExecute',false);

// Clear the snapshot (cached results of the query, used for quick pagination) before re-rendering.
// This is irrelevant if the tablePane isn't using snapshots.
tablePane.setProperty('clearSnapshot',true);

// Regenerate HTML for the tablePane - it'll be empty.
tablePane.refreshContents();

Or, a bit more simply:

var tablePane = zen('yourTablePaneId');

// Don't actually execute the query the next time the table is rendered.
tablePane.setProperty('initialExecute',false);

// Re-execute the query (this also clears the snapshot if snapshots are in use)
tablePane.executeQuery();

If refreshContents() / executeQuery() is called again, query results will be shown, because initialExecute is set to true each time the tablePane is drawn.

Timothy Leavitt · Mar 17, 2016 go to post

Actually - if you're going to do this after issuing the Get, you should use request.Get(,,0), then do this, then call Reset() on the request manually. Otherwise the parameters will get cleared out and the query string will always be blank.

Timothy Leavitt · Mar 17, 2016 go to post

Yeah, that would be nice to have. I looked a bit and didn't see one right away. (I'd actually been wondering the same thing last night, as it happens.)

Timothy Leavitt · Mar 21, 2016 go to post

A few side notes...

The correct/best way to create a %Object from a %RegisteredObject (or vice versa) is $compose, not $fromObject (which has been marked as Internal in more recent builds). This is first available in 2016.2.

SAMPLES>set person = ##class(Sample.Person).%OpenId(1)
SAMPLES>set obj = {}.$compose(person)
SAMPLES>w obj.$toJSON()
{"Age":88,"DOB":31520,"FavoriteColors":["Blue"],"Home":{"City":"Youngstown","State":"CO","Street":"1360 Oak Avenue","Zip":74578},"Name":"Tillem,Terry Y.","Office":{"City":"Gansevoort","State":"KY","Street":"4525 Main Court","Zip":93076},"SSN":"132-94-8739"}

Also, you can get %RegisteredObjects as JSON more directly:

SAMPLES>set person = ##class(Sample.Person).%OpenId(1) 
SAMPLES>w person.$toJSON()
{"Age":88,"DOB":31520,"FavoriteColors":["Blue"],"Home":{"City":"Youngstown","State":"CO","Street":"1360 Oak Avenue","Zip":74578},"Name":"Tillem,Terry Y.","Office":{"City":"Gansevoort","State":"KY","Street":"4525 Main Court","Zip":93076},"SSN":"132-94-8739"}
Timothy Leavitt · Apr 1, 2016 go to post

The CacheTemp global mentioned in the original post already has the IDs sorted by sortColumn - it's just a matter of $order-ing over it normally (with $order(global)) or in reverse (with $order(global,-1)) based on the value of sortOrder. This is what the %DrawTable method in %ZEN.Component.tablePane does.

Timothy Leavitt · Apr 19, 2016 go to post

Other than locks, there are a few other cases where cleanup may be needed whether or not something goes wrong:

  • Closing SQL cursors that have been opened
  • Ensuring that the right IO device is in use and/or returning to the previous IO redirection state.

There are probably more of these too.

Here's the convention we use for error handling, logging, and reporting in InSync (a large Caché-based application):

  • We have TSTART/TCOMMIT/TROLLBACK in a try/catch block at the highest level (typically a ClassMethod in a CSP/Zen page). There isn't much business logic in here; it'll call a method in a different package.
  • If anything goes wrong in the business logic, an exception is thrown. The classes with the business logic don't have their own try/catch blocks unless it's needed to close SQL cursors, etc. in event of an exception. After the cleanup is done, the exception is re-thrown. (Unfortunately, this means that cleanup code may be duplicated between the try and catch blocks, but there's typically not too much duplication.) The classes with business logic also don't have their own TSTART/TCOMMIT/TROLLBACK commands, unless the business logic is a batch process in which parts of the process may fail and be corrected later without impacting the whole thing; such a case may also call for a nested try/catch to do the TROLLBACK if something goes wrong in part of the batch. In this case the error is recorded rather than re-throwing the exception.
  • We have our own type of exception (extending %Exception.AbstractException), and macros to create exceptions of this type from:
    • Error %Status codes
    • Error SQLCODEs and messages
      • SQLCODE = 100 can be treated as an error, "alert", or nothing.
    • Other types of exceptions
  • Exceptions of our custom type can also be created to represent a general application error not related to one of those things, either a fatal error, or something the user can/should fix - e.g., invalid data or missing configuration.
  • The macros for throwing these exceptions also allow the developer to provide a localizable user-friendly message to explain what went wrong.
  • When an exception is caught in the top level try/catch (or perhaps in a nested try/catch in a batch process), we have a macro that logs the exception and turns it into a user-friendly error message. This might just be a general message, like "An internal error occurred (log ID _______)" - the user should never see <UNDEFINED>, SQLCODE -124: DETAILS ABOUT SOME TABLE, etc.
  • Our persistent classes may include an XDATA block with localizable error messages corresponding foreign and unique keys in the class and types of violations of those keys. For %Status codes and SQLCODEs corresponding to foreign/unique key violations, the user-friendly error message is determined based on this metadata.
  • Logging for these exceptions is configurable; for example, exceptions representing something the user can/should fix are not logged by default, because they're not an error in the application itself. Also, the log level is configurable - it might be all the gory detail from LOG^%ETN, or just the stack trace. Typically, verbose logging would only be enabled system-wide briefly for specific debugging tasks. For SQL errors, the SQL statement itself is logged if possible.

I thought this convention was too complicated when I first started working with it, but have come to see that it is very elegant. One possible downside is that it relies on a convention that any method in a particular package (InSyncCode, in our case) might throw an exception - if that isn't respected in the calling code, there's risk of a <THROW> error.

I mentioned the InSync approach previously on https://community.intersystems.com/post/message-error-csppage . Unfortunately, it's coupled with several parts of the application, so it'd be quite a bit of work to extract and publish the generally-applicable parts. I'd like to do that at some point though.

Timothy Leavitt · Apr 19, 2016 go to post

Happy to help. :-)

To clarify, I think it's a difference between ccontrol terminal and ccontrol runw - ccontrol terminal wouldn't accept spaces for me either.

Timothy Leavitt · Apr 20, 2016 go to post

For more advanced error analysis, such as conversion of error %Status-es into user-friendly messages (as I described in another comment), $System.Status.DecomposeStatus will provide the parameters of the error message as well. These are substituted in to the localizable string.

For example, here's a foreign key violation message from %DeleteId on a system running in Spanish:

INSYNC>Set tSC = ##class(Icon.DB.CT.TipoDocumento).%DeleteId(50)                 
INSYNC>k tErrorInfo d $System.Status.DecomposeStatus(tSC,.tErrorInfo) zw tErrorInfo
tErrorInfo=1
tErrorInfo(1)="ERROR #5831: Error de Foreign Key Constraint (Icon.DB.CC.AllowedGuaranteeTypes) sobre DELETE de objeto en Icon.DB.CT.TipoDocumento: Al menos existe 1 objeto con referencia a la clave CTTIPODOCUMENTOPK"
tErrorInfo(1,"caller")="zFKTipoDocDelete+4^Icon.DB.CC.AllowedGuaranteeTypes.1"
tErrorInfo(1,"code")=5831
tErrorInfo(1,"dcode")=5831
tErrorInfo(1,"domain")="%ObjectErrors"
tErrorInfo(1,"namespace")="INSYNC"
tErrorInfo(1,"param")=4
tErrorInfo(1,"param",1)="Icon.DB.CC.AllowedGuaranteeTypes"
tErrorInfo(1,"param",2)="Icon.DB.CT.TipoDocumento"
tErrorInfo(1,"param",3)="DELETE"
tErrorInfo(1,"param",4)="CTTIPODOCUMENTOPK"
tErrorInfo(1,"stack")=...

The "param" array allows clean programmatic access to the details of the foreign key violation, independent of language.

Of course, these level of detail in these error messages may be subject to change across Caché versions, so this is a *great* thing to cover with unit tests if your application relies on it.

Timothy Leavitt · Apr 21, 2016 go to post

I think this was a caution for anyone changing their username, since it's shared across InterSystems' sites/applications.

IIRC you use CCR (Change Control Record). The username change may prevent you from using the version control integration in that application. It might be good to ensure that it's still working, or at least to make a note that if it doesn't work, you'll need to change the username back (and then probably log out and back in again for the change to take effect in CCR).

Others may not be impacted as much.

Timothy Leavitt · Apr 27, 2016 go to post

Sure - although it'd be a property, not a parameter. Looking at utcov.ClassLookup (glad to see it's not %utcov now, by the way), this should work fine for you. Here's a sample:

Class Sample.ClassQueryProperty Extends %RegisteredObject
{

Property SubclassQuery As %SQL.Statement [ InitialExpression = {##class(%SQL.Statement).%New()}, Private, ReadOnly ];

Method %OnNew() As %Status [ Private, ServerOnly = 1 ]
{
	Quit ..SubclassQuery.%PrepareClassQuery("%Dictionary.ClassDefinition","SubclassOf")
}

Method Demo()
{
	Set tRes = ..SubclassQuery.%Execute("%UnitTest.TestCase")
	While tRes.%Next(.tSC) {
		$$$ThrowOnError(tSC)
		Write tRes.%Get("Name"),!
	}
	$$$ThrowOnError(tSC)
}

}

Then:

SAMPLES>d ##class(Sample.ClassQueryProperty).%New().Demo()
%UnitTest.IKnowRegression
%UnitTest.PMMLRegression
%UnitTest.SQLDataRegression
%UnitTest.SQLRegression
%UnitTest.SetBuilder
%UnitTest.TSQL
%UnitTest.TestCacheScript
%UnitTest.TestProduction
%UnitTest.TestScript
%UnitTest.TestSqlScript
Wasabi.Logic.Test.InventoryTest
Wasabi.Logic.Test.PricingTest
Timothy Leavitt · Apr 27, 2016 go to post

That's true.

Some developers like to use #dim to declare all the variables they expect to create. In Studio, Tools > Options..., Environment > Class, there's an "Option Explicit" option that will give you warnings if you use a variable that hasn't been #dim'd. (The "Track Variables" option is also very useful.)

Timothy Leavitt · Apr 30, 2016 go to post

I really would recommend creating a %All namespace (if there isn't already one), via %Installer or something that works similarly.

One projects on the intersystems-ru github, Caché Web Terminal, has the same requirement (use from any namespace); this class might be helpful for reference: https://github.com/intersystems-ru/webterminal/blob/master/export/WebTerminal/Installer.xml. It doesn't actually use %Installer, so configuration changes are implemented in COS instead of being generated based on XML, but it works similarly.

Particularly, see methods CreateAllNamespace and Map/UnMap. You should be able to adapt these without too much effort. If your code coverage project eventually has a UI, then the web application setup method will be useful too (for simple installation).

Timothy Leavitt · May 9, 2016 go to post

This is a great article!

One minor detail - MyPackage.Installer (or some other class) needs to declare the projection for the installer class to work as advertised.

For example, in MyPackage.Installer itself, you could add:

Projection InstallMe As MyPackage.Installer;

The examples you referenced on GitHub include this.

Timothy Leavitt · May 10, 2016 go to post

For what it's worth, I believe $Namespace is new'd before CreateProjection/RemoveProjection are called. At least, I was playing with this yesterday and there weren't any unexpected side effects from not including:

new $Namespace

in those methods. But it definitely is best practice to do so (in general).

One effect of this I noticed yesterday is that if you call $System.OBJ.Compile* for classes in a different namespace in CreateProjection, they're queued to compile in the original namespace rather than the current one. Kind of weird, but perhaps reasonable; you can always JOB the compilation in the different namespace. Maybe there's some other workaround I couldn't find.

Timothy Leavitt · May 12, 2016 go to post

Thank you for pointing out both of those things!

I'm not sure why I thought the end condition was evaluated each time. I'll update the post to avoid spreading misinformation.

Timothy Leavitt · May 12, 2016 go to post

Eduard, that actually does run faster for me than the $ListFromString/$ListNext approach (with the conversion included). Not including the conversion, $ListNext is still faster. (I've updated the Gist to include that example.)

Timothy Leavitt · May 13, 2016 go to post

I've seen the same. I got an email that had a bunch of my own answers in it, although I hadn't changed any of them. What threads did you see in that email?

Timothy Leavitt · May 13, 2016 go to post

Interesting. It might be related to the recipient's content then; I didn't see anything about that post, but I did get notified about content (answers) I'd added a while back that had not been changed.

Timothy Leavitt · May 16, 2016 go to post

They track execution time:

#define START(%msg) Write %msg Set start = $zh#define END Write ($zh - start)," seconds",!
Timothy Leavitt · May 25, 2016 go to post

Hi John,

Thank you for your feedback. We'll address these issues very soon.

I previously hadn't been aware of the ##www.intersystems.com:template_delimiter## syntax, but found it documented here (along with more details about Studio Templates). It looks like we do support this in Atelier for templates themselves (Tools > Templates), but not yet for templates launched from Studio extensions. This should change.

Thanks,

Tim

Timothy Leavitt · May 31, 2016 go to post

A few questions:

  • If you open the same URL without the CSPDEBUG URL parameter, what happens?
  • Does /namehere/package.home.cls work as a debug target?
  • Is the webserver port correct for IIS, or are you using a private webserver that isn't enabled? (The webserver port Studio uses can be changed in the Cube under Preferred Server > Add/Edit...)
  • Does the audit database show anything odd?

For troubleshooting CSP issues (although more often on InterSystems' side than in application code), ISCLOG can be helpful. For example, run the following line in terminal:

kill ^%ISCLOG set ^%ISCLOG = 3 read x set ^%ISCLOG = 0

Then make whatever HTTP request is causing an error, then hit enter in terminal (to stop logging), then:

zwrite ^%ISCLOG

There might be a lot of noise in there, but also possibly a smoking gun - a stack trace from a low-level CSP error, an error %Status from something security-related, etc.

Timothy Leavitt · May 31, 2016 go to post

The port isn't part of the debug target - that's configured as part of the remote/local server definition in the cube (in the system tray).

Since the page works with ?CSPDEBUG removed, it'd be interesting to see ^%ISCLOG or if there are any interesting events (e.g., <PROTECT>) recorded in the audit database.

Timothy Leavitt · Jun 1, 2016 go to post

Ah - yes, a number of things log to ^%ISCLOG. It's very important to set ^%ISCLOG = 0 at the end to keep it from continuing to record. The command I mentioned previously is an easy way to make sure that you're only logging for a brief period of time - paste the command into Terminal and hit enter to start logging, then load the page, then hit enter in Terminal to stop logging. Still, there could be lots of other stuff in there even from having it enabled briefly depending on how busy the server is.

It might make sense for you to contact InterSystems' outstanding Support department - someone could work through this with you directly and help you find a solution more quickly.