As far as I know, %OnBeforeSave is used to prevent (abort) the save operation and to customize %Save status.
You can't change any properties on that phase as they won't be reflected, that is because the payload is already queued

to be saved.

%OnAddToSaveSet is executed before before the queueing phase, which allows you to overwrite property values.

Unless you want to do something that deals with complex business rules, you should indeed use InitialExpression as Fabian suggested, even use ReadOnly to prevent the property's value to be edited.

If you want something more regular you could pipe the output via OS:
This also outputs more than 32000 kb.

 set errorLogDir = ##class(%File).TempFilename()
 set outputLogDir = ##class(%File).TempFilename()

 set command = "dir /A-D /B /S ""%1"" 2> ""%2"" > ""%3"""
 quit $zf(-1, $$$FormatText(command, "C:\InterSystems\Cache", errorLogDir, outputLogDir))

Now simply open and read the logs.

Unless you're asking for maximum performance you could create a method that receives or creates a %SQL.Statement for the %File:FileSet query and detect if "Type" is "D" to call it recursively, passing that statement instance.

Here's a use case that I applied that pattern:

Method SearchExtraneousEntries(
statement As %SQL.Statement = "",
path As %String,
ByRef files As %List = "")
{
  
  if statement = "" {
    set statement = ##class(%SQL.Statement).%New()
    $$$QuitOnError(statement.%PrepareClassQuery("%File", "FileSet"))
  }
  
  set dir = ##class(%File).NormalizeDirectory(path)
  set row = statement.%Execute(dir)
  set sc = $$$OK   
  
  while row.%Next(.sc) {
    if $$$ISERR(sc) quit
    set type = row.%Get("Type")
    set fullPath = row.%Get("Name")
    
    if ..IsIgnored(fullPath) continue
            
    if type = "D" {      
      set sc = ..SearchExtraneousEntries(statement, fullPath, .files)
      if $$$ISERR(sc) quit
    
    
    if '..PathDependencies.IsDefined(fullPath) {
      set length = $case(files, "": 1, : $listlength(files)+1)
      set $list(files, length) = $listbuild(fullPath, type)
    }
  }
  quit sc
}

I see that as an integrity viewpoint.

What if the user customized the storage? If you don't export it with his definition, it would map to another global.

This is even more fearsome if you map a class to a global since they're usually based on user-made globals.
 

Removing Schema definition also means introducing a checkpoint to verify if the class is a %Persistent or not, since you can also have global mapped classes. That could also reduce the peformance.

And the fundamental question is:

Why you would versionate something different than what you actually intended?
 

This indeed made the communication much easier!
Now I only have to create a strategy for message batching.

It seems that I can't simply use WaitForComplete since it puts the caller process into sleep.

My next step is do an experiment with the IPQ variation.

Anyway, here's my experiment using the WorkMgr.

 

Class Log.Test2 [ Abstract ]
{

ClassMethod Log()
{
  set buf = ""
  set msg = "This is a test #"
  
  for i=1:1:1046000 {
    set composedMessage = msg_i_$c(13, 10)
    set expectedSize = $length(buf) + $length(composedMessage)
    if expectedSize > $$$MaxStringLength {
      write buf
      set buf = ""
    else {
      set buf = buf_composedMessage
    
  }
  
  if buf '= "" write buf
  quit $$$OK
}

ClassMethod Start()
{
  write "This is from "_$job, !!
  set queue = $System.WorkMgr.Initialize("d", .sc, .sc)
  set sc = queue.Queue("##class(Log.Test2).Log")
  if $$$ISERR(sc) do $System.OBJ.DisplayError(sc) quit sc
  set sc = queue.WaitForComplete()
  if $$$ISERR(sc) do $System.OBJ.DisplayError(scquit sc
  quit $$$OK
}

}

Just like a CSP file.

You have two ways of working with that:

 

1 - If you modify the file using the Studio, that file is exported automatically when you save it.

 

2 - If you modify the file outside the Studio you can import the file again to the project and it'll be overwritten with the new version (as long as it's newer).

Anything inside the web folder is mirrored inside the csp application. Which means:

If you have a file with the path 
C:\CacheProjects\yourproject\web\css\index.css 

It's name will be resolved to:
csp/namespace/index.css

And will be mirrored like this:
C:\InterSystems\Cache\CSP\namespace\css\index.css

If you remove that file from the project, save it and export, then this file will be eliminated from the repository since it doesn't belong to the project anymore.

The same rule is applied for each type. Not only the folder "web".

If you want a proof-of-concept or a showcase, then this link shows how the class package hierarchy is followed.

You can disable the use of .txt using two ways: 

1 - By running ##class(Port.SourceControl.Config).SetSourceExtension("")

2 - By running the wizard with ##class(Port.SourceControl.Wizard).Start() and navigating to the relevant option.

Note that this will ONLY AFFECT the appended extension, not the type itself. Example:

##class(Port.SourceControl.Config).SetSourceExtension("")  = cls/Port/SourceControl/Hooks.cls

##class(port.SourceControl.Config).SetSourceExtension("txt")  = cls/Port/SourceControl/Hooks.cls.txt

All the same for following formats: INC, INT, MAC, DFI, MVB, MVI, BAS. Except if the file is inside the web (CSP) path.

I could do a small screencast, but the usage is pretty straightforward. Anything more advanced than that is covered by the Wizard.

How to use:

1 - First time only:  import the port.xml.

2 - Run ##class(Port.SourceControl.Installer).Install().

3 - Restart the Studio.

4 - Done! Now whenever you save a file related to the project, it's structure is generated and the file is exported everytime you save it.

5 - If you want to export all items regarding the current project you can use Source Control->Export Current Project.

EDIT: Oh I forgot mentioning about tests.

Unit testing with Port:

As long as you have classes prefixed with the package "UnitTest" (which is also configurable). You can export these classes to XML and run their tests atomically.

1 - SourceControl->Export Test Suites to XML.

2 - Context SourceControl menu->Run Tests Associated with this item.

 

By default when you install Port using the installer, it sets a parameter to run unit tests as you keep compiling the classes.
You can disable that as well.

If you notice any bugs, please fill a issue.
If you still need a demonstration, please ask again.

Wow, it worked! But I don't want it to send the data back to the other process for each iteration, so I tried buffering it using a variable. Now, If you run this code you will notice the following error:

 [CRLF] class '%Studio.General', method 'Execute': <WRITE> 41 zExecute+26^%Studio.General.1

The routine might according to the principal device, of course.

There're two points I noticed here:

1 - The caller process must wait for the job for it's first write. Or a <READ> error will happen.
2 - The caller process must know when stop iterating if the job is finished.
3 - Sometime the line break works, sometimes it doesn't.

That's why I tried introducing the Event API, however it doesn't seems to be working, since the Signal is being ignored by that WaitMsg. Is there anything I'm missing here?

Class Log.Test [ Abstract ]
{

ClassMethod Write(
device As %String,
start As %Integer = 1) As %Boolean
{
  set startEventTriggered = 0
  open device:("127.0.0.1":33568:"M"):3
  quit:'$test
  
  use device
  
  set buf = ""
  set msg = "This is a test #"
  
  for i=start:1:10460 {
  set composedMessage = msg_i_$c(13, 10)_" [CRLF] "
  set expectedSize = $length(buf) + $length(composedMessage)
  if expectedSize > $$$MaxStringLength {
  //set offset = expectedSize - $$$MaxStringLength
  //set subbuff = $e
  if 'startEventTriggered {
  set startEventTriggered = 1
  use 0
  do $System.Event.Signal($zparent, "1")
  use device
  }
   write buf
   set buf = ""
  else {
   set buf = buf_composedMessage
  
  }
  
  if buf '= "" write buf
  write $c(0)
  
  close device
}

ClassMethod Start()
{
   set server = "|TCP|1"
   set client = "|TCP|2"    
   
   job ..Write(server)::3
   
   do $System.Event.WaitMsg("", 3)
   
   open client:(:33568:"M"):3 // Moved open outside the loop
   quit:'$test 
   
   while 1 {
     use "|TCP|2"
     read x:3
     
     if '= "" {
       use 0
       write x
     }     
     quit:x=$c(0)
   }   
   close client
}

}
 

Both plus globals.

NoSQL:

set obj = ##class(Sample.Person).%New()
set obj.Name = "John"
set obj.Spouse = #class(Sample.Person).%OpenId(1) // Let's assume it's a woman.

set sc = obj.%Save() // Use sc to know if any error happened.


SQL:

set s = ##class(%SQL.Statement).%New()
set sc = s.%Prepare("INSERT INTO Package.PersistentClass (property) VALUES (propertyValue)")
set sc = s.%Execute() // If everything is OK sc is 1, otherwise you might want to check s.%Message and s.%SQLCODE as well.


You can also use embedded SQL, search for "&sql" on the link I provided.

Globals

WARNING: I don't recommend any manipulation using globals directly if you 're working with classes.

Normally class globals are stored following the pattern below:

^Package.ClassNameI for index
^Package.ClassNameD for values
^Package.ClassNameS for streams

Check the class's Storage XML discover it's global structure.

I tried making a simple example, no Event or IJC for now, just to understand how devices works. I understand the concept, but there are many types of devices and types of configuration that always lead me to a dead end.

I'll escalate the example (using Events and IJC) as I keep understanding the concepts.

So, what am I doing wrong here? read x:3 always times out, regardless of my doings.
 

Note: Just to make myself clear, I'm planning on using it along with this project. Well, use it with big projects and you'll understand.

Maybe I could even create an utility that makes it easier to print a job's outputs as long as it's possible.

Class Log.Test [ Abstract ]
{

ClassMethod Write(
device As %String,
start As %Integer = 1) As %Boolean
{
  open device:(:33568::"\n"):3
  quit:'$test
  
  use device
  
  for i=start:1:1000000000 {
    write "This is a test #"_i_$c(13,10)
    if i#1000 = 0 {
      write "\n"
    }
  }
}

ClassMethod Start()
{
   set device = "|TCP|1"
   
   job ..Write(device)::3
   
   while 1 {
     open "|TCP|2":(:33568):3
     quit:'$test
     
     use "|TCP|2"
     read x:3
     
     close "|TCP|2"
     
     if '= "" {
       use 0
       write x
     }
   }
}

}
 

Oh, sorry for my misleading words.

I said "parent process" or "main process" due to the $zparent, $zchild and $job. But I didn't mean something close to process forks, since as you said, they haven't a parent-child relationship, but more liking siblings as they can run independently.

Which situtation would be interesting to change the principal device? This method's description is quite 'dry' to understand what is it's usage.

I noticed that devices are my weak spot, maybe my questions are looking dumb for some people here. :P

Thanks for your help so far.

Hmm, about I'll have to do something more elaborated then. I had done a quick draft before.

Calling $ZUTIL(132) with no extra arguments makes the current device the principal device.

And how does this affects the parent process? (if it does affect).

Often more useful is the former $ZUTIL(82,12,bool), now ##CLASS(%Device).ReDirectIO(bool) which lets you redirect I/O through routines that can filter, redirect, record, though routines.

I remember reading about that here and here