Link to home
Start Free TrialLog in
Avatar of narmi2
narmi2

asked on

Capture commandline output

Hi

I am creating a program which runs a commandline in cmd.  When the command is running, it displays progress information in the cmd.exe window.  This progress is displayed as a %.  How do I get that percentage into my program in a label?

Please have a look at the image below:
http://img249.imageshack.us/img249/1656/imageak2.png

As you can see, when i click the button, a cmd window opens and the processing starts, the red circle shows the percentage the processing is complete.  I would like to hide the cmd window and display that percentage in my windows application instead.

How can I do this?
Avatar of gnoon
gnoon
Flag of Thailand image

Try to use an OutputDataReceived event of Process class by set output redirect to True

start_info.RedirectStandardOutput = True

then just before proc.Start(), add an event handler

AddHandler proc.OutputDataReceived, AddressOf OutputHandler

Next, define a sub to handle the event

Private Sub OutputHandler(sendingProcess As Object, outLine As DataReceivedEventArgs)
    '-- Here, you can analyse the output using outLine.Data
End Sub

Finally, after you call proc.Start(), just call

proc.BeginOutputReadLine()
Avatar of narmi2
narmi2

ASKER

I have tried implimenting that but i an not sure what I am doing... help

    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnEncode.Click
        Dim myfile As New StreamWriter(Application.StartupPath & "\encode.bat")
        myfile.WriteLine("flac -d --stdout """ & txtInput.Text & """ | lame " & txtSettings.Text & " - """ & txtOutput.Text & """")
        myfile.Close()
        myfile.Dispose()
 
        ' Set start information.
        Dim start_info As New ProcessStartInfo
        start_info.WorkingDirectory = Application.StartupPath
        start_info.FileName = "encode.bat"
        start_info.UseShellExecute = False
        start_info.CreateNoWindow = False
        start_info.RedirectStandardOutput = True
        start_info.RedirectStandardError = False
 
        ' Make the process and set its start information.
        Dim proc As New Process()
        proc.StartInfo = start_info
 
        ' Start the process.
        AddHandler proc.OutputDataReceived, AddressOf OutputHandler
        proc.Start()
        proc.BeginOutputReadLine()
        proc.WaitForExit()
        proc.Close()
 
        File.Delete(Application.StartupPath & "\encode.bat")
    End Sub
 
    Private Sub OutputHandler(ByVal sendingProcess As Object, ByVal outLine As DataReceivedEventArgs)
        '-- Here, you can analyse the output using outLine.Data
        Control.CheckForIllegalCrossThreadCalls = False
        TextBox1.Text = outLine.Data.ToString
    End Sub

Open in new window

After run, any output at TextBox1?

I've modified the code, is it work for you?
PS: I dont have VS2005 for run test ;-)
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnEncode.Click
	Dim myfile As New StreamWriter(Application.StartupPath & "\encode.bat")
	myfile.WriteLine("flac -d --stdout """ & txtInput.Text & """ | lame " & txtSettings.Text & " - """ & txtOutput.Text & """")
	myfile.Close()
	myfile.Dispose()
 
	' Set start information.
	Dim start_info As New ProcessStartInfo
	start_info.WorkingDirectory = Application.StartupPath
	start_info.FileName = "encode.bat"
	start_info.UseShellExecute = False
	start_info.CreateNoWindow = True   '<------------- (1) Hide the output
	start_info.RedirectStandardOutput = True
	start_info.RedirectStandardError = False
 
	' Make the process and set its start information.
	Dim proc As New Process()
	proc.StartInfo = start_info
 
	' Start the process.
	AddHandler proc.OutputDataReceived, AddressOf OutputHandler
	AddHandler proc.Exited, AddressOf Batch_Exited  '<------ (2) Delete file on batch finished
 
	proc.Start()
	proc.BeginOutputReadLine()  '<----(3) Begin asynchronous read the output
	'proc.WaitForExit()  '<----------(4) Don't need to wait
	'proc.Close()
End Sub
 
Private Sub OutputHandler(ByVal sendingProcess As Object, ByVal outLine As DataReceivedEventArgs)
	'-- Here, you can analyse the output using outLine.Data
	Control.CheckForIllegalCrossThreadCalls = False
	TextBox1.Text = GetPercent( outLine.Data.ToString )  '<---------(5) Send the output to get only percent number
End Sub
 
Private Sub Batch_Exited(ByVal sender As Object, ByVal e As System.EventArgs)
	File.Delete(Application.StartupPath & "\encode.bat") '<-------(6) Perform delete file on process finished
End Sub
 
Function GetPercent(ByVal output As String) As String  '<----(7) Function to parse for percent number using regular expression
	Dim rx As System.Text.RegularExpressions.Regex = _
		New System.Text.RegularExpressions.Regex("([0-9]{1,3})\s*%", _
		RegexOptions.IgnoreCase Or RegexOptions.Multiline)
	Dim founds As MatchCollection = rx.Matches(output)
	If founds.Count > 0 Then Return founds(0).Groups(1).Value
	Return Nothing
End Function

Open in new window

Avatar of narmi2

ASKER

Thank you gnoon.  I will try this when I get home.  Thanks again.
Avatar of narmi2

ASKER

I am getting an error on the following line

TextBox1.Text = GetPercent(outLine.Data.ToString)

The error message is

Object reference not set to an instance of an object

Any ideas?
The error will be happen if an variable has not been initialized, but its method/property is being access.

I think, two possible cases
- outLine.Data is Nothing, then we're trying to call outLine.Data.ToString
- GetPercent is not visible as a function in your class, but it's looked like an array

Resolution
- check outLine.Data before access its methods
- define the function in class, copy/paste from my code above

Also, try this
Private Sub OutputHandler(ByVal sendingProcess As Object, ByVal outLine As DataReceivedEventArgs)
	'-- Here, you can analyse the output using outLine.Data
	If Not String.IsNullOrEmpty(outLine.Data) Then
		Control.CheckForIllegalCrossThreadCalls = False
		TextBox1.Text = GetPercent( outLine.Data.ToString )  '<---------(5) Send the output to get only percent number
	End If
End Sub

Open in new window

Avatar of narmi2

ASKER

Ok i tried that but it shows nothing in the textbox1?  the processing part of it works fine, it is just that, it gives me no feedback as to how much is done
Sorry narmi2, I just installed VS2005 on my comp and did test the code above.
I see that it need a line of code to achieve asynchronous read

        proc.WaitForExit()

It works if this code exists, otherwise it ends the program before the next read is reached.
Sorry again that I made you confused.
Avatar of narmi2

ASKER

No problem at all.  I will try that out as soon as I can, which will probably be at the end of the day.  Thanks again for all the help. :)
Avatar of narmi2

ASKER

gnoon: Where do I add that line of code?
After the line of proc.BeginOutputReadLine().
Avatar of narmi2

ASKER

i did that and it still did not work.  So I did a little testing by changing the following line:

TextBox1.Text = GetPercent( outLine.Data.ToString )  '<---------(5) Send the output to get only percent number

to this:

TextBox1.Text = outLine.Data.ToString

And it only returned the following text:

C:\Document and Settings\narmi2\My Documents\Visual Studio 2000\Projects\Windows\Application1\WindowsApplication1\bin\Debug\flac -d --stdout "c:\audio.flac" |lame -V9 - "c:\audio.mp3"

That is it!

it did not return the rest of the output for some reason, which explains why it did not display the percentage complete in the textbox.

do you know why it is not returning the full text?  You can see the full text in the image in the first post i made.
It prints the full text because the @echo command is turned on. If you turn it off, the full text should not printed.

myfile.WriteLine("@echo off") '<------------ turn off echo command itself
myfile.WriteLine("flac -d --stdout """ & txtInput.Text & """ | lame " & txtSettings.Text & " - """ & txtOutput.Text & """")

This is code I used to test capture. It's Console Application. Also, you can test it :-)
Imports System.IO
Imports System.Text.RegularExpressions
 
Module Module1
 
    Dim BaseDir As String = "c:"
 
    Sub Main()
        RunBatch()
    End Sub
 
    Sub RunBatch()
        Dim myfile As New StreamWriter(BaseDir & "\encode.bat")
        myfile.WriteLine("@ECHO OFF")
        myfile.WriteLine("SET COUNT=0")
        myfile.WriteLine(":Loop")
        myfile.WriteLine("cls")
        myfile.WriteLine("echo Encoding as 24 KHz VBR(q=9) j-stereo MPEG-2 Layer III (ca. 16.5x) qval=3")
        myfile.WriteLine("echo audio.flac: %COUNT%%% complete")
        myfile.WriteLine("SET /A COUNT=COUNT+1")
        myfile.WriteLine("@ping 127.0.0.1 -n 2 -w 1000 > nul")
        myfile.WriteLine("@ping 127.0.0.1 -n %1% -w 1000> nul")
        myfile.WriteLine("goto Loop")
        myfile.Close()
        myfile.Dispose()
 
        ' Set start information.
        Dim start_info As New ProcessStartInfo
        start_info.WorkingDirectory = BaseDir
        start_info.FileName = BaseDir & "\encode.bat"
        start_info.UseShellExecute = False
        start_info.CreateNoWindow = True   '<------------- (1) Hide the output
        start_info.RedirectStandardOutput = True
        start_info.RedirectStandardError = False
 
        ' Make the process and set its start information.
        Dim proc As New Process()
        proc.StartInfo = start_info
 
        ' Start the process.
        AddHandler proc.OutputDataReceived, AddressOf OutputHandler
        AddHandler proc.Exited, AddressOf Batch_Exited  '<------ (2) Delete file on batch finished
 
        proc.Start()
        proc.BeginOutputReadLine()  '<----(3) Begin asynchronous read the output
        proc.WaitForExit()  '<----------(4) Don't need to wait
        'proc.Close()
 
    End Sub
 
    Private Sub OutputHandler(ByVal sendingProcess As Object, ByVal outLine As DataReceivedEventArgs)
        '-- Here, you can analyse the output using outLine.Data
        If Not String.IsNullOrEmpty(outLine.Data) Then
            Console.WriteLine(GetPercent(outLine.Data.ToString))  '<---------(5) Send the output to get only percent number
        Else
            Console.WriteLine("NULL!!!")
        End If
    End Sub
 
    Private Sub Batch_Exited(ByVal sender As Object, ByVal e As System.EventArgs)
        File.Delete(BaseDir & "\encode.bat") '<-------(6) Perform delete file on process finished
    End Sub
 
    Function GetPercent(ByVal output As String) As String  '<----(7) Function to parse for percent number using regular expression
        Dim rx As System.Text.RegularExpressions.Regex = _
         New System.Text.RegularExpressions.Regex("([0-9]{1,3})\s*%", _
         RegexOptions.IgnoreCase Or RegexOptions.Multiline)
        Dim founds As MatchCollection = rx.Matches(output)
        If founds.Count > 0 Then Return founds(0).Groups(1).Value
        Return Nothing
    End Function
 
End Module

Open in new window

Avatar of narmi2

ASKER

Thank you.  I will try this tonight.
Avatar of narmi2

ASKER

I tried that and it is not working, it produces no output at all in the text box.  Can you please have a look at the project, you might see something which I have not seen.  I have zipped the whole project: https://filedb.experts-exchange.com/incoming/ee-stuff/6028-WindowsApplication1.zip
OK, I will check it out this tonight when I got home.
Avatar of narmi2

ASKER

To get it to run, you will need lame.exe from this download: http://audio.ciara.us/rarewares/lame3.97.zip
and flac.exe from this download: http://switch.dl.sourceforge.net/sourceforge/flac/flac-1.2.1-win.zip

also, you will need a .flac audio file to convert.  you can easily make on of those with the flac encoder above if you have a wav file already.  if you need help with any of the above, please let me know.
Avatar of narmi2

ASKER

Hi

Looks like I need to capture the StandardError instead of the StandardOutput as the bit that I am interested in, the percentage complete, comes in the StandardError and not in the StandardOutput.  I will try this when I get home in the evening...

Please let me know if you have any luck with this.
I tested the code. Yes, as you said. Lame prints out the percent message on error stream.

There are issues here about the code
- better start the process in another thread than UI-thread to prevent screen frozen.
- you need to check for not empty output before set to textbox.
- proc.Exited will be invoked only if proc.EnableRaisingEvents = True
- since you use AddHandler (asynchronous reading) and it's Win Form App (event driven programming), proc.WaitForExit() is not needed ;-)

Here the code I modified
Imports System.IO
Imports System.Threading
Imports System.Text.RegularExpressions
 
Public Class Form1
 
    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnEncode.Click
        'move code here into a thread to prevent screen frozen
        Dim t As Thread = New Thread(AddressOf ProcessInThread)
        t.Name = "ConvertThread"
        t.IsBackground = True
        t.Start()
        'ProcessInThread()
    End Sub
 
    Private Sub ProcessInThread()
        Dim myfile As New StreamWriter(Application.StartupPath & "\encode.bat")
        myfile.WriteLine("@echo off")
        myfile.WriteLine("flac -d --stdout """ & txtInput.Text & """ | lame " & txtSettings.Text & " - """ & txtOutput.Text & """")
        myfile.Close()
        myfile.Dispose()
 
        ' Set start information.
        Dim start_info As New ProcessStartInfo
        start_info.WorkingDirectory = Application.StartupPath
        start_info.FileName = "encode.bat"
        start_info.UseShellExecute = False
        start_info.CreateNoWindow = True
        start_info.RedirectStandardOutput = True
        start_info.RedirectStandardError = True
 
        ' Make the process and set its start information.
        Dim proc As New Process()
        proc.StartInfo = start_info
        proc.EnableRaisingEvents = True
 
        ' Start the process.
        AddHandler proc.OutputDataReceived, AddressOf OutputHandler '<----- isn't need
        AddHandler proc.ErrorDataReceived, AddressOf ErrorHandler
        AddHandler proc.Exited, AddressOf Batch_Exited
 
        proc.Start()
        proc.BeginOutputReadLine()
        proc.BeginErrorReadLine()
        'proc.WaitForExit()
        'proc.Close()
    End Sub
 
    Private Sub OutputHandler(ByVal sendingProcess As Object, ByVal outLine As DataReceivedEventArgs)
        If Not String.IsNullOrEmpty(outLine.Data) Then
            Dim percent As String = GetPercent(outLine.Data.ToString)
            Console.WriteLine("output: " & outLine.Data & " [" & percent & "]")
            If Not String.IsNullOrEmpty(percent) Then
                Control.CheckForIllegalCrossThreadCalls = False
                TextBox1.Text = percent
                'Console.WriteLine(percent)
            End If
        End If
    End Sub
 
    Private Sub ErrorHandler(ByVal sendingProcess As Object, ByVal outLine As DataReceivedEventArgs)
        If Not String.IsNullOrEmpty(outLine.Data) Then
            Dim percent As String = GetPercent(outLine.Data.ToString)
            Console.WriteLine("error: " & outLine.Data & " [" & percent & "]")
            If Not String.IsNullOrEmpty(percent) Then
                Control.CheckForIllegalCrossThreadCalls = False
                TextBox1.Text = percent
                'Console.WriteLine(percent)
            End If
        End If
    End Sub
 
    Private Sub Batch_Exited(ByVal sender As Object, ByVal e As System.EventArgs)
        Console.WriteLine("exit")
        File.Delete(Application.StartupPath & "\encode.bat")
    End Sub
 
    Function GetPercent(ByVal output As String) As String
        Dim rx As System.Text.RegularExpressions.Regex = _
                New System.Text.RegularExpressions.Regex("([0-9]{1,3})\s*%", _
                                                         RegexOptions.IgnoreCase Or RegexOptions.Multiline)
        Dim founds As MatchCollection = rx.Matches(output)
        If founds.Count > 0 Then Return founds(0).Groups(1).Value
        Return Nothing
    End Function
 
    Private Sub btnChoose_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnChoose.Click
        OpenFileDialog1.Filter = "Flac files (*.flac)|*.flac"
        If OpenFileDialog1.ShowDialog = Windows.Forms.DialogResult.OK Then
            txtInput.Text = OpenFileDialog1.FileName
            txtOutput.Text = Replace(OpenFileDialog1.FileName, ".flac", ".mp3")
        End If
    End Sub
 
    Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
        SaveFileDialog1.Filter = "mp3 files (*.mp3)|*.mp3"
        If SaveFileDialog1.ShowDialog = Windows.Forms.DialogResult.OK Then
            txtOutput.Text = SaveFileDialog1.FileName
        End If
    End Sub
End Class

Open in new window

Avatar of narmi2

ASKER

Hi, thanks for that.  It is not doing what I was hoping for.  This code displays the percentage when the process is complete, I was hoping to display the percentage as the process was processing, so you would see the percentage incrementing in the textbox.
>I was hoping to display the percentage as the process was processing
Actually, it did that. I think, the process is too fast, so you can see only 100% progress. You may try with a larger file. It's also happen on my comp.
However, I will help to check it again.
Avatar of narmi2

ASKER

I tried a large 80Mb flac file and nothing came in the textbox at all.  It just displayed 100 when the processing finished.  The time it took to process that 80Mb was 1 or 2 minutes.
Avatar of narmi2

ASKER

Please click on the link below to see an swf file of what happens.
http://www40.brinkster.com/namiiii/encoding.swf.html

you might have to use internet explorer 7 to open that link.

anyway, as you can see, when it is processing, nothing happens in the textbox.  only after the processing is finished, does the percentage go into the textbox.

how do i make it more real time?
Hi, sorry for late response.

I tried to run only flac in the Process
  flac -d -f "c:\audio.flac"
, get the same result. It's lazy of print out.

Also, I searched over the net, and get nothing helpful.
I think it's something about cache of flac.exe or the difference between 32-bit GUI and 16-bit Console.
(This article is useful http://www.codeproject.com/KB/threads/redir.aspx)

However, I have an idea of solution. Lets called "C# pipeline".

Instead of redirecting flac's output to lamp directly, as input, just redirect it to C# then forward to lamp afterward.
This can be done by define two Process in code. One handles flac.exe and get its output at the ErrorHandler, then send the output (array of byte) to the second process of lame.exe. The second process produces MP3 file using the array of byte received. Count the bytes received as progress.

Just an idea. I will look for a free time to test it.
Avatar of narmi2

ASKER

That sounds interesting!

Do you have any links about how to get flac to decode directly into .net byte?
ASKER CERTIFIED SOLUTION
Avatar of gnoon
gnoon
Flag of Thailand image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Line 58 should be this

lame_proc.StandardInput.BaseStream.Write(buffData, 0, bytesRead)

not this

lame_proc.StandardInput.BaseStream.Write(buffData, 0, buffSize)

Sorry.
Avatar of narmi2

ASKER

Hi

Thanks

That works a lot better than before.  But just 1 question, why is there a delay between 99 and 100?
>why is there a delay between 99 and 100?
Just to make sure that user will see 100% only when the convert was already done.

We use the fixed-ratio = 0.72, but in real this number can be vary depend on option used by flac.
So, it's possible to get 100% (or more than 100) inside the while loop, but not long time it will break out.