Rubens Silva · Aug 9, 2017 go to post

Thank you for your suggestion, however the error still happens.
I also didn't made it clear, but the payload doesn't have a class to be reflected from, it can be abstract, requiring it to be parsed with %DynamicObject or %DynamicArray.
Since there's no class with %Stream.* property, %ConvertJSONToObject cannot decide what to do with long strings.
The ideal would be to detect the $$$MaxStringSize size and fallback to an internal %Stream instance before assigning the value to the proxy.
Also, I'm trying to avoid using %ZEN.proxyObject due to issues with stacking many objects for a single process. So the class parameter should be something inheriting from %DynamicAbstractObject.

 

Here's is the step-by-step:

DEV>set f = ##class(%Stream.FileCharacter).%New()
 
DEV>do f.LinkToFile("/temp/lorem.json")
 
DEV>set sc = ##class(%ZEN.Auxiliary.jsonProvider).%ConvertJSONToObject(f,, .o)
 
DEV>if $System.Status.IsError(sc) do $System.OBJ.DisplayError(sc)
 
ERRO #5002: Erro Caché: <MAXSTRING>%ParseJSON+290^%ZEN.Auxiliary.jsonProvider.1

This won't happen if I have long strings enabled.

Rubens Silva · Aug 8, 2017 go to post

Yes, like... %DynamicObject and %DynamicArray.
I still don't quite get why to use %objlasterror instead of returning the error.
Method %ToJSON(outstrm As %Stream.Object) As %String
{
if $D(outstrm) {
if $isobject(outstrm) {
if '(outstrm.%IsA("%Stream.Object")) {
throw ##class(%Exception.General).%New("%ToJSON - argument passed is not an instance of %Stream.Object")
}
try {
set ans = $zu(210,27,outstrm)
catch {
do  $$$APPERROR1($$$LASTERROR)  <- This appends the error to %objlasterror and that's it.
}

Rubens Silva · Aug 8, 2017 go to post

I see, we have been using an in-house tool with features similar to yours as well, at that time files were still being exported as XML instead of UDL. But now we're moving our development to local and enforcing project usage using this tool.

 

We also had our share of pain with editing live production code and I have to say, it's not the greatest feeling.

Rubens Silva · Aug 8, 2017 go to post

a homemade tool

Wow, that's unexpected. Care to share some details about that tool?

Rubens Silva · Aug 8, 2017 go to post

I don't know what you're extending your class from, but try calling ##super() before you set the timeout.
EDIT: Hold on, try using %response.Timeout instead. Maybe your problem is not the session itself, but the response timeout instead.

/// Can be set in the OnPreHTTP() method of a page in which case this changes the amount of time the
/// CSP gateway will wait for a response from the server in seconds before it reports the 'Server is not
/// responding' error message. This is useful if you know that this page is doing an expensive SQL
/// query that will take a couple of minutes and you want to set the server response timeout on
/// the CSP gateway to a minute and yet wait three minutes for this page to respond. It will just
/// change the server response timeout for this page only. If not set the the CSP gateway uses its
/// default timeout value specified in the CSP gateway configuration.
Property Timeout As %Integer;
Rubens Silva · Aug 8, 2017 go to post

NOTE:
Expert programmers try to keep away from using GOTO because it can break greatly the code workflow consistency. This is a basic concept for using structured programming.
So, before you think about using it, try to render the same effect using subroutines and methods instead.

Rubens Silva · Aug 8, 2017 go to post

Since ZEN runs under CSP, maybe %session.AppTimeout = 900 // 15 mins.
Just remember to change it inside OnPreHTTP.

Rubens Silva · Aug 8, 2017 go to post

That's quite a topic for complex discussions.

  • Do you use an issue tracking / collaboration system? If so which one. Any you would recommend or immediately dismiss based on personal experience?

I use Github plus repository issues.

  • How do you keep track of large code bases? Thousdands of folders named backup1, backups2, ..., SVN, git?

Git.

  • Do you have a development server to which you commit and test features there, or do you rather run a local copy of caché and implement features locally first, then push to the server?

Locally implemented and tested. tested and implemented.

  • Bonus question: How do you handle legacy code (and I mean the using lots of $ZUs kind of legacy) code? Leave it untouched and try to implement new features elesewhere? Rewrite the entire thing?

It depends, the more complex the code is, more I consider creating modern API wrappers instead of re-writting it.

Rubens Silva · Aug 7, 2017 go to post

Since booleans can be only true or false, you usually don't need to know about anything else, so let the caller handle what it should do.
 

ClassMethod AssertClassExists(ClassName As %String) As %Status
{
   if '..ClassExists(ClassName) return $$$ERRROR($$$ClassDoesNotExist, ClassName)
   return $$$OK
}

ClassMethod ClassExists(ClassName as %String) as %Boolean {

return ##class(%Dictionary.CompiledClass).%ExistsId(ClassName)

}
Rubens Silva · Aug 7, 2017 go to post

I only return the result when I'm absolutely sure that the method cannot throw any error. Otherwise I follow the rule:
1 - Obligatory arguments first.

2 - Result by second.

2 - Parameter with initial values third.

3 - Rest parameters for last.
(obligatoryParamA,  obligatoryParamB, obligatoryParamC, result,  optionalA, optionalB,  rest...)

Rubens Silva · Aug 7, 2017 go to post

%Next does receives a %Status as output. It's not exactly what you suggested, but close enough.
while result.%Next(.sc) {

$$$ThrowOnError(sc)

}

Rubens Silva · Aug 7, 2017 go to post

Not sure if I understood what you meant, but I'll try to answer:
When using class methods you cannot reference your THIS instance. Have you every used Java, C# or whatever programming language that supports object oriented-programming paradigms? If so then class methods are the same as static methods.
Common (non-static) methods have the advantage of providing you the instance context. Which means that all properties are accessible inside this kind of method, even the private ones.

Class Sample.Person Extends %Persistent
{

Property Name As %String;

ClassMethod GetPersonName(instance As Sample.Person)
{
   return instance.Name // This works fine, GetPersonName is receiving an external Sample.Person.
}

// 'My' is used to empathize that this method refers to it's own instance name.
Method GetMyName() As %String 
{
   return ..Name // This also works fine.  Notice that this method doesn't receives the instance, it's because you can only call it from a instance already.
}

ClassMethod GetMyInstanceName() As %String
{
  return ...Name // This won't work. Since there's no context (instance). The compiler will even warn about it.
}
}
Rubens Silva · Aug 4, 2017 go to post

No, there isn't.
What he meant is that Caché supports binding for other languages. But there isn't a native COS implementation for PGP.
The easiest way of achieving what you want is to use gpg using $zf (CallOut). Where you can emit a comand directly to the host OS.
Remember that by default Windows haven't a gpg command. But there's a version for it as well.

You can also use method ##class(%Net.Remote.Utility).RunCommandViaZF(cmd,,.output,,) for brevity. Where cmd should be your gpg command.

Rubens Silva · Aug 4, 2017 go to post

%request.Content will provide you the raw string (or stream) contained on your request payload.

Rubens Silva · Aug 3, 2017 go to post

The hard-coded offset does not come from a variable. A lack of comments in the code makes it very difficult to understand and debug.

No wonder...  Maybe you could use Studio or Atelier debugger to verify their values. Unless you need that log file of course.

Maybe you could also use break instead, just before the routine ends. And zwrite the context variables manually within the terminal.
I still don't know exactly what you want to achieve neither what are your limitations, I only know that you want to see the content from LINE, so I might be speaking something unnecessary.

Rubens Silva · Aug 3, 2017 go to post

Since you said it's arbitrary, where did you get that +3? Is that a constant or variable value?
Maybe if you update your line to:
ZBREAK *LINE:"T"::"ZWRITE LINE(I+C)"

Where C should be your other arbitrary data (but provided as variable as well).

Rubens Silva · Aug 3, 2017 go to post

Yeah, I had fixed it but didn't posted the update and then started working on output, sorry.

Rubens Silva · Aug 3, 2017 go to post

I finally got it to work as I desired. Here's the source code, take a look on output routine:
This will:
Limit usage of ! to one per write.

Prevent initial write ! as I don't want to skip any line on the beginning.

Display compiler messages correctly.

Prevent from writing new lines for empty buffers.
Include portutils
Class Port.SourceControl.ExtendedHooks [ Abstract ]
{
ClassMethod Call(
sourceControl As %Studio.Extension.Base,
hookName As %String = "",
parameters... As %String) As %Status [ Internal, ProcedureBlock = 0 ]
{
  new sc, content, implementer, alreadyRedirected, currentMnemonic, childSC, expectingContent, firstLine
  
  set sc = $$$OK
  set childSC = $$$OK
  set implementer = ##class(Port.Configuration).GetExtendedHooksImplementer()
  set alreadyRedirected = ##class(%Device).ReDirectIO()
  set expectingContent = 0
  set firstLine = 0
  
  if '##class(%Dictionary.CompiledMethod).%ExistsId(implementer_"||"_hookName) return sc 
  set content = ##class(%Stream.GlobalBinary).%New()
  
  if implementer '= "" {
    write !, "Port: "_$$$FormatMsg("Port Log Messages", $$$RunningCustomHook, hookName, implementer)
    
    try {
      set currentMnemonic = "^"_##class(%Device).GetMnemonicRoutine()
      use $io::("^"_$zname)
      do ##class(%Device).ReDirectIO(1)
      set sc = $classmethod(implementer, hookName, sourceControl, parameters...)
    catch ex {
      set content = "" 
      set sc = ex.AsStatus()
    }
  }
  
  if alreadyRedirected 
    do ##class(%Device).ReDirectIO(1)
    use $io::(currentMnemonic)
  }
  
  if $isobject(content)
    do content.OutputToDevice()
  }
  
  write !
  
  if $$$ISOK(sc) {    
    write "Port: "_$$$FormatMsg("Port Log Messages", $$$HookReturnedOK, hookName)
  else {
    set errorText = $System.Status.GetOneStatusText(sc)
    write "Port: "_$$$FormatMsg("Port Log Messages", $$$HookReturnedError, hookName, errorText)
    set childSC = sc
    set sc = $$$PERROR($$$FailedWhileRunningExtendedHook, hookName)
    set sc = $$$EMBEDSC(sc, childSC)
  }   
  return sc 
  
rchr(c)
  quit
rstr(sz,to)
  quit
wchr(s)
  do output($char(s))
  quit 
wff()
  do output($char(12))
  quit
wnl()
  if firstLine = 0 set firstLine = 1
  else  set firstLine = -1
  do output($char(13,10))
  quit
wstr(s)
  do output(s)
  quit
wtab(s)
  do output($char(9))
  quit
output(s)
  // Skips writing the first !, we leave it to our write.
  if firstLine = 1 quit
  // Remaining writes ! are always a standalone buffer so we can check it's equality.
  if = $c(13,10) {
    // However we can only write if the the next buffer has indeed some content.
    // So we defer it to the next call where we can actually assert it.
    set expectingContent = 1
    // This catches writes with embedded CRLF (like the compiler ones).
  elseif $extract(s, 1, 2) = $c(13,10) {
    set expectingContent = 1
    do output($replace(s, $c(13,10), ""))
    set expectingContent = 0
    quit
  elseif $length(s) > 0 {
    // After deferring it, we can finally write a CRLF and the content, as long as it's not empty.
    if expectingContent = 1 {
      set expectingContent = 0
      do content.WriteLine()
      do content.Write($$$FormatText("Port (%1): ", hookName))
    }
    // Writes without ! must be written on the same line.
    do content.Write(s)
  }
   
  quit
}
}
 

Rubens Silva · Aug 2, 2017 go to post

  if $isobject(content) {
    do content.Rewind()
    while 'content.AtEnd set ^ck($i(i)) = content.ReadLine() }
    do content.OutputToDevice()
    do content.Rewind()
    do content.MoveTo(content.Size -3)
    do content.ReadLine(,,isEOL)
  }
^ck(1)="" <-- This is because write !, "First line"
^ck(2)="Port (OnAfterSave): First lineSecond line" <-- Though First line is actually pushed down and merged with "Second line"
^ck(3)="Port (OnAfterSave): Third line" <-- Third works fine, because second finishes with !
^ck(4)="Port (OnAfterSave): " <-- write "Third line", ! creates a blank line though my prefix is displaying.
I'm starting to think there's no way to prevent all situations.

Rubens Silva · Aug 2, 2017 go to post

Indeed, those redirection routines aren't called anywhere. But they're redefined extensively around the Caché Class API.
As you said it's cryptic and powerful enough to dictate the buffer workflow even though you don't use it explicitly.
From my experiments, what I notice is:
wstr handles every write

wnl handles every !

wchr handles every single char

wtab handles tab usage
wff  handles every form feed
As used here.
 

Rubens Silva · Aug 2, 2017 go to post

%Status always returned $$$OK and putting -3 instead -1 provided me the same feedback. The last line is still empty.
Maybe it's something regarding the redirection routines. I can't really see that:
wnl() do output($char(13,10)_$$$FormatText("Port (%1): ", hookName)) quit
As correct, since wnl doesn't even take a parameter  where I could decide what to do.

Rubens Silva · Aug 2, 2017 go to post

Updated to use %Stream.GlobalCharacter instead, thank you.
Using eol also didn't resolved I guess. Here's the feedback:

 

Port: Executing hook extension OnAfterSave from the class Port.HookTest ...
Port (OnAfterSave):
Port (OnAfterSave): First lineSecond line
Port (OnAfterSave): Third line
Port (OnAfterSave):
Port: OnAfterSave returned without errors.
ClassMethod OnAfterSave(
sourceControl As %Studio.Extension.Base = "",
InternalName,
Location,
Object) As %Status
{
  // ! skips the first line leaving it empty, actual message is pushed down to the second line.
  // should not even print it.
  write !, "First line"
  
  // First line is here, printed along with the second.
  write "Second line", !
  
  // Third is still third line. But ! writes another empty line.
  write "Third line", !
  return $$$OK
}
As you suggested, I changed it to use eol:
 if $isobject(content) {
    do content.OutputToDevice()
    do content.MoveTo(content.Size - 1)
    do content.ReadLine(, sc, .isEOL)
  }
     
  if 'isEOL write !

Rubens Silva · Aug 2, 2017 go to post

It seems they have changed it to an image for newer releases. (Even Caché is now an image instead of pure CSS).
Well, I do agree with crediting IS, however it should be something like:
"Powered by InterSystems ENSEMBLE" like a single message, since the focus is to demonstrate your work, not theirs.

Rubens Silva · Aug 1, 2017 go to post

Yeah, you can't upload photos directly to this forum and it is somewhat bad, even though there's a lot of image related configs that most won't even think about using...

 

The only way is hosting in the cloud.

Rubens Silva · Jul 31, 2017 go to post

Such a roundtrip for knowing something that should be configurable.
I don't think you should depend on INetInfo since normally you know where you're hosting your service. Make the address configurable and this will prevent headaches, after that you just need to concatenate with your CSP file address.

Rubens Silva · Jul 28, 2017 go to post

set number = "1234"
write number?.N // 1, so it's integer.
set number = "44.2d3"
write number?.N // 0, so it's float, double or whatever data type with precison.
You can read more about Pattern Matching here.

Rubens Silva · Jul 28, 2017 go to post

Since no one answered that yet, maybe there isn't a known way of doing it.
If that's really the case, I suggest you to create your procedure.
This could work as the following:
 

1 - Routine B watches a global generated from Routine A using a scheduled task.

2 - Process A triggers an exception.

3 - Exception on routine A executes subroutine or method to that uses $stack to capture execution data.

4 - Routine A stores data into a global and quits abnormally.

5 - Routine B watches for new entries in the global, marks it as used/processed.