Is there a way to detect the last written char?


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.

  • 0
  • 151
  • 12
  • 2


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 
  do output($char(s))
  do output($char(12))
  if firstLine = 0 set firstLine = 1
  else  set firstLine = -1
  do output($char(13,10))
  do output(s)
  do output($char(9))
  // 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
  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)


%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

   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

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 = "",
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 !

just a guess:
with do content.MoveTo(content.Size - 1)   you are probably just between CR and LF 

Could you try do content.MoveTo(content.Size - 3)  just to make sure you a e before CRLF

I'm not sure how EOL is triggered
eventually also check returned success code

%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.

I don't see a call for  wnl()   
but ancient coding practices  would suggest its:  writenewline just that
and that's what it does without any other content.
though the remark is rater direct if interpreting feedback := line

// Should be smart as well: routines ending with write ! will create an empty feedback!

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 you change wnl() ...
wnl()  quit
you just disable it to see if it plays a role at all    

to me this looks like your content had an extra $C(13,10) at the end.
It could help to  have the full content at hands. 
before  content.OutputToDevice()
set ck=content.Read()
set ^ck($i(^ck))=ck
do content.Rewind()

I expect  zwrite ^ck will show more than 3  lines
That would indicate that the source of trouble is on the input side. 
Some closing action ?   


  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.

indeed this is surprising.

but is this really:   do content.ReadLine(,,isEOL)

it should be a pass by reference to receive something back do content.ReadLine(,,.isEOL) 
with the <dot> in front of the variable 

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