Show in live process output via print

Hi everyone. I have a question. When my package manager compile a task (basically a crystal script), I would like to print the compilation progress output via a print. But when I try to do it, the print show nothing. How should I proceed ?

def buildTasksFile
            processOutput = IO::Memory.new
            processResult = IO::Memory.new

            process = Process.run(  "crystal build --release --progress #{ISM::Default::Filename::Task}.cr -o #{@settings.rootPath}#{ISM::Default::Path::RuntimeDataDirectory}#{ISM::Default::Filename::Task} -f json",
                                    output: processOutput,
                                    error: processResult,
                                    shell: true,
                                    chdir: "#{@settings.rootPath}#{ISM::Default::Path::RuntimeDataDirectory}")

            print processOutput.to_s

            processResult.rewind

            if processResult.to_s != ""
                taskError = Array(ISM::TaskBuildingProcessError).from_json(processResult.to_s.gsub("\"size\":null","\"size\":0"))[-1]
                showTaskBuildingProcessErrorMessage(taskError, "#{@settings.rootPath}#{ISM::Default::Path::RuntimeDataDirectory}#{ISM::Default::Filename::Task}.cr")
                exitProgram
            end
        end

Something like this (sorry, untested code but it’s based on working code):

    Process.run(
      command: "yourcommand",
      args: args,
    ) do |process|
      loop do
        data = process.output.gets(chomp: false)
        STDOUT << data
        STDOUT.flush
        Fiber.yield # Without this the process never ends
        break if process.terminated?
      end
    end

Then you can check how the process finished by checking $?.exit_code

So it’s strange because I don’t have any error but my code print nothing.

This is my function:

        def buildTasksFile
            processResult = IO::Memory.new

            Process.run("crystal build --release --progress #{ISM::Default::Filename::Task}.cr -o #{@settings.rootPath}#{ISM::Default::Path::RuntimeDataDirectory}#{ISM::Default::Filename::Task} -f json",
                        error: processResult,
                        shell: true,
                        chdir: "#{@settings.rootPath}#{ISM::Default::Path::RuntimeDataDirectory}") do |process|
                loop do
                    data = process.output.gets(chomp: false)

                    print data

                    Fiber.yield
                    break if process.terminated?
                end
            end

            processResult.rewind

            if processResult.to_s != ""
                taskError = Array(ISM::TaskBuildingProcessError).from_json(processResult.to_s.gsub("\"size\":null","\"size\":0"))[-1]
                showTaskBuildingProcessErrorMessage(taskError, "#{@settings.rootPath}#{ISM::Default::Path::RuntimeDataDirectory}#{ISM::Default::Filename::Task}.cr")
                exitProgram
            end
        end

I reduced it to as simple as I could and this works:

image

So I did a small test you can run. If I run this command, I have no output:

Process.run("crystal build --release --progress Test.cr -o Test -f json", shell: true) do |process|
    loop do
        data = process.output.gets(chomp: false)

        STDOUT << data
        STDOUT.flush

        Fiber.yield
        break if process.terminated?
    end
end

Try add --stats

Thank you but it’s not what I want to get, I need the progress output :sweat_smile:

The issue is specifically with the --progress flag which rewrites the output on each stage update. Because a newline (\n) character is never written during this process, gets fails which is why there’s no output (or I think it just returns nil but it doesn’t matter).

As @zw963 suggested, --stats may be a better option as that prints lines. Do note however that the output can also affect lines already printed, the compiler will usually do this for stages that have longer names so that they are formatted nicely.

If you specifically want the output from --progress, this code should work:

Process.run("crystal build --release --progress test.cr -o test -f json", shell: true) do |process|
  data = String.build do |io|
    process.output.each_char do |char|
      if char == '\r'
        io << '\n'
      else
        io << char
      end
    end
  end

  puts data
end

So I tried your code, but it show the progress only when the process is finish, not in live :sweat_smile:

By the way, I noticed crystal have a bug during the compilation output:

[1/13] Parse                             
[1/13] Parse                             
[2/13] Semantic (top level)              
[2/13] Semantic (top level)              
[3/13] Semantic (new)                    
[3/13] Semantic (new)                    
[4/13] Semantic (type declarations)      
[4/13] Semantic (type declarations)      
[5/13] Semantic (abstract def check)     
[5/13] Semantic (abstract def check)     
[6/13] Semantic (restrictions augmenter) 
[6/13] Semantic (restrictions augmenter) 
[7/13] Semantic (ivars initializers)     
[7/13] Semantic (ivars initializers)     
[8/13] Semantic (cvars initializers)     
[8/13] Semantic (cvars initializers)     
[9/13] Semantic (main)                   
[9/13] Semantic (main)                   
[10/13] Semantic (cleanup)                
[10/13] Semantic (cleanup)                
[11/13] Semantic (recursive struct check) 
[11/13] Semantic (recursive struct check) 
[12/13] Codegen (crystal)                 
[12/13] Codegen (crystal)                 
[13/13] Codegen (bc+obj)                  
[13/13] [0/1] Codegen (bc+obj)                  
[13/13] [0/1] Codegen (bc+obj)                  
[14/13] Codegen (linking)                 
[14/13] Codegen (linking)

It show 14/13 :smiling_face_with_tear:

Try this?

Process.run("crystal build --progress test.cr", shell: true) do |process|
  output = process.output.each_char

  loop do
    char = output.next

    if char == '\r'
      STDOUT << '\n'
      STDOUT.flush
      STDOUT << ">>> "
    else
      STDOUT << char
    end

    Fiber.yield
    break if process.terminated?
  end
end

Okay, thank you zw963. So now I have 2 other questions.

From that, how can I redirect this output to a IO::Memory ? Because if I try to use a IO::Memory, when I run the program, it raise an error that Process#output cannot be nil.

Basically, I would like to get this output … But not print it directly from the process. (Like the process print but silently)

Another question as well. When I use this code, it print an extra text at the end, it print a class name and it address:

#<Iterator::Stop::0x.....>

Or maybe there is just another way to show the progression ? I just would like basically to show to the user how many times/steps left before the end of the compilation.

Because stats is not really for that

When output.next reach the end, a special Iterator::Stop exception raised, which cause loop exit.

it raise an error that Process#output cannot be nil.

Please show your code.