Question
· Aug 2, 2017

Is there a way to detect the last written char?

Hello.

I'm using device redirection to intercept incoming writes and need to figure a smart way to detect when to CLRF in order to prevent misleading outputs.

So basically, if the intercepted write ends with !, I need to know that and prevent my routine from writing ! as well.

Here's the source code for it:
 

ClassMethod Call(
  sourceControl As %Studio.Extension.Base,
  hookName As %String = "",
  parameters... As %String) As %Status [ Internal, ProcedureBlock = 0 ]
{
  new sc, content, implementer, alreadyRedirected, isNewLineTerminator, currentMnemonic
  
  set sc = $$$OK
  set implementer = ##class(Port.Configuration).GetExtendedHooksImplementer() 
  set alreadyRedirected = ##class(%Device).ReDirectIO()
  set isNewLineTerminator = 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)
      write ! // Should be smart!
      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()
    do content.MoveTo(content.Size)
    set isNewLineTerminator = (content.Read(1) = $char(10)) 
  }
     
  if 'isNewLineTerminator write ! 
  
  if $$$ISOK(sc) {  
    write "Port: "_$$$FormatMsg("Port Log Messages", $$$HookReturnedOK, hookName)
  } else {
    write "Port: "_$$$FormatMsg("Port Log Messages", $$$HookReturnedError, hookName, $System.Status.GetOneStatusText(sc))
  }  
  return $$$OK
  
rchr(c) quit
rstr(sz,to) quit
wchr(s) do output($char(s)) quit 
wff() do output($char(12)) quit
wnl() do output($char(13,10)_$$$FormatText("Port (%1): ", hookName)) quit // Should be smart as well: routines ending with write ! will create an empty feedback!
wstr(s) do output(s) quit
wtab(s) do output($char(9)) quit
output(s) do content.Write(s) quit
}

I bolded the part of what I tried to do to resolve this situation but didn't worked.

Thanks in advance.

Discussion (13)0
Log in or sign up to continue

1)  
%Stream.GlobalBinary has a warning:

Note that on a unicode Cache although you can write unicode data into this class and read it back out this is not a supported use of this class and the Size property would be incorrect in this case as it would be the size in characters rather than the size in bytes

2)
   do content.OutputToDevice()
now your content is out and you are positioned at end

    do content.MoveTo(content.Size)
now you re-read content until LastCharacter-1, whatever total size might be
    set isNewLineTerminator = (content.Read(1) = $char(10)) 
reading the assumed last character

3)
you might be better off to use  
content.ReadLine(,.sc,.eol) and check eol for the termination status

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 !

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.
 

  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.

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
}

}