LPChip
LPChip

Reputation: 884

Using VB.Net, Start a cmd.exe and then execute multiple commands and output natively in that window

Introduction

I'm trying to write a program that will create lots of .7z files. Over 1000. If I create one cmd window for every instance, either it creates 1000 windows at the same time, or one in a row for several hours. Neither situation is good.

I can't say: hide the window, because I actually need to show the progress of the 7z to the user as some of the files may be huge, and without a window, the process seems to have died, but it just takes a bit, and I don't know how to use 7z.dll and be able to get the status.

My solution to the problem

If I can spawn a cmd window that the user can interact with (doesn't have to be), and then send keypresses to that window, everything is consolidated.

My problem

I can't seem to get it to work. The best thing that I have so far is that it does spawn a window, but every attempt to have something printed on the cmd window, ends up appearing in my debug output window. Because the 7z archiving process has dynamic text update, capturing the output and putting it in a textfield doesn't work because it doesn't capture the archiving percentage for the file being created. While it displays some stuff about the archiving process, the essential part is missing.

My code:

1 form: Form1
1 button: bInteractWithCMD
Targetting .net 4.8.1 using Visual Studio 2022 (if the fix is going to be using a different version of .net, let me know which version.)

Public Class Form1

    Private Process As New Process
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        Me.Process.StartInfo.FileName = "cmd.exe"
        Me.Process.StartInfo.Arguments = "/k echo Hello World"
        Me.Process.StartInfo.UseShellExecute = False
        Me.Process.StartInfo.RedirectStandardOutput = False
        Me.Process.StartInfo.RedirectStandardInput = True
        Me.Process.Start()

    End Sub

    Private Sub bInteractWithCMD_Click(sender As Object, e As EventArgs) Handles bInteractWithCMD.Click
        Me.Process.StandardInput.WriteLine("echo This is a test.")
    End Sub
End Class

Here's what happens: enter image description here

Footnote

In this example, I'm doing a simple echo. If I replace that with the 7z.exe code, the behavior is still the same, but this example is far easier to troubleshoot.

If you have any other way of getting the 7z.exe progress without displaying it in the cmd window, I'll happily accept that answer. Also, I can't let my program create a .cmd script and execute that. While it would theoretically work, my program will do steps in between processing each file including informing the user, and I don't want that part to happen in the cmd window. The user should be able to minimize the cmd window, and go back to it to peek for the progress if necessary. The cmd window is more a way to keep track that the process hasn't stalled. My program will display the status in a more convenient way.

Upvotes: 0

Views: 140

Answers (2)

It all makes cents
It all makes cents

Reputation: 4999

The following shows how one can use System.Diagnostics.Process with 7-Zip to compress files in a directory. If one specifies -bsp1 in the command-line arguments, the progress will be displayed.

Create a class (name: HelperProcess.vb)

Imports System.IO
Imports Microsoft.Win32

Public Class HelperProcess

    Private Shared Function Get7ZipLocation(Optional regView As RegistryView = RegistryView.Registry64) As String
        Dim hive As RegistryHive = RegistryHive.LocalMachine
        Dim subkey As String = "Software\7-Zip"
        Dim valueName As String = "Path"

        Using rKey As RegistryKey = RegistryKey.OpenBaseKey(hive, regView)
            If rKey IsNot Nothing Then

                'open subkey
                Using sKey As RegistryKey = rKey.OpenSubKey(subkey, False)
                    If sKey IsNot Nothing Then
                        'read from registry
                        'Debug.WriteLine($"'{valueName}' Data Type: {sKey.GetValueKind(valueName)}")
                        Return sKey.GetValue(valueName)?.ToString()
                    Else
                        Throw New Exception($"Error (GetRegistryValue) - Could not open '{subkey}'")
                    End If
                End Using
            Else
                Throw New Exception($"Error (GetRegistryValue) - Could Not open '{hive.ToString()}' ")
            End If
        End Using
    End Function


    Public Shared Sub RunProcess7Zip(arguments As String, Optional filename As String = Nothing)

        If String.IsNullOrEmpty(filename) Then
            'if 7-Zip fully-qualified filename wasn't supplied, get path from registry
            filename = $"""{Path.Combine(Get7ZipLocation(), "7z.exe")}"""
        End If

        'create new instance
        Dim startInfo As ProcessStartInfo = New ProcessStartInfo(filename) With {.CreateNoWindow = True, .RedirectStandardError = True, .RedirectStandardOutput = True, .UseShellExecute = False, .WindowStyle = ProcessWindowStyle.Hidden}

        If Not String.IsNullOrEmpty(arguments) Then
            startInfo.Arguments = arguments
        End If

        Using p As Process = New Process() With {.EnableRaisingEvents = True, .StartInfo = startInfo}

            AddHandler p.ErrorDataReceived, Sub(sender As Object, e As DataReceivedEventArgs)
                                                If Not String.IsNullOrWhiteSpace(e.Data) Then
                                                    'ToDo: add desired code
                                                    Debug.WriteLine("error: " & e.Data)
                                                End If
                                            End Sub

            AddHandler p.OutputDataReceived, Sub(sender As Object, e As DataReceivedEventArgs)
                                                 If Not String.IsNullOrWhiteSpace(e.Data) Then
                                                     'ToDo: add desired code
                                                     Debug.WriteLine("output: " & e.Data)
                                                 End If
                                             End Sub

            p.Start()

            p.BeginErrorReadLine()
            p.BeginOutputReadLine()

            'wait for exit
            p.WaitForExit()

        End Using
    End Sub
End Class

Usage:

Dim targetFilename As String = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Test.7z")

'-bsp1: output progress to StandardOutput
'-mmt4: set number of CPU threads to 4
'-mx=5: set compression level to 5 (default)
Dim t As Thread = New Thread(Sub() HelperProcess.RunProcess7Zip($"a -bsp1 -mx=5 -mmt4 -ssp -t7z {targetFilename} {sourceFolder}\*.*"))
t.Start()

Update:

The following shows how one can use a StatusStrip to display the filename, progress, and percent done.

Add a StatusStrip to Form (name: StatusStrip1)

Open Form in Designer (ex: Form1)

  • In Visual Studio menu, select View
  • Select Solution Explorer
  • In Solution Explorer, right click Form1.vb
  • Select View Designer

Add StatusStrip to Form

  • In Visual Studio menu, select View
  • Select Toolbox
  • Expand All Windows Forms, drag StatusStrip to the form

Add two ToolStripStatusLabel, and one ToolStripProgressBar to the StatusStrip

  • In Visual Studio menu, select View

  • Select Properties Window

  • From the drop-down, select StatusStrip1

  • Select Items, then click the box with ... (on the right-side)

  • From the drop-down select StatusLabel, then click Add (name: ToolStripStatusLabel1)

    enter image description here

  • From the drop-down select ProgressBar, then click Add (name: ToolStripProgressBar1)

    enter image description here

  • From the drop-down select StatusLabel, then click Add (name: ToolStripStatusLabel2)

    enter image description here

  • Click OK

To avoid: System.InvalidOperationException: 'Cross-thread operation not valid: Control 'StatusStrip1' accessed from a thread other than the thread it was created on.'.

Add a Module (name: ControlExtensions.vb)

Imports System.Runtime.CompilerServices

Public Module ControlExtensions
    <Extension()>
    Public Sub Invoke(control As System.Windows.Forms.Control, action As System.Action)
        If control.InvokeRequired Then
            control.Invoke(New System.Windows.Forms.MethodInvoker(Sub()
                                                                      action()
                                                                  End Sub), Nothing)
        Else
            action.Invoke()
        End If
    End Sub
End Module

Usage

StatusStrip1.Invoke(Sub()
                        ToolStripStatusLabel1.Text = $"Processing '{currentFilename}'..."
                        ToolStripProgressBar1.Value = percentage
                        ToolStripStatusLabel2.Text = $"{percentage}%"
                    End Sub)

Alternatively, instead of using an extension method, one could use:

If StatusStrip1.InvokeRequired Then
    StatusStrip1.Invoke(New MethodInvoker(Sub()
                                              ToolStripStatusLabel1.Text = $"Processing 2 '{currentFilename}'..."
                                              ToolStripProgressBar1.Value = percentage
                                              ToolStripStatusLabel2.Text = $"{percentage}%"
                                          End Sub))
Else
    ToolStripProgressBar1.Value = percentage
    ToolStripProgressBar1.Value = percentage
    ToolStripStatusLabel2.Text = $"{percentage}%"
End If

Form1.vb

Note: In the code below you'll notice that I've used a different way of subscribing to the OutputDataReceived event for System.Diagnostics.Process.

Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
    ToolStripProgressBar1.Visible = False
    ToolStripStatusLabel2.Visible = False
End Sub

Private Sub RunProcess7ZipCompress(sourceFilename As String, targetFilename As String)

    Dim filename As String = "c:\Program Files\7-Zip\7z.exe"

    If Not File.Exists(filename) Then
        Throw New FileNotFoundException("7z.exe wasn't found.")
    End If

    'create new instance
    Dim startInfo As ProcessStartInfo = New ProcessStartInfo($"""{filename}""") With
        {
          .Arguments = $"a -bsp1 -mmt4 {targetFilename} {sourceFilename}",
          .CreateNoWindow = True,
          .RedirectStandardError = True,
          .RedirectStandardOutput = True,
          .UseShellExecute = False,
          .WindowStyle = ProcessWindowStyle.Hidden}

    Using p As Process = New Process() With {.EnableRaisingEvents = True, .StartInfo = startInfo}

        'subscribe to event
        AddHandler p.ErrorDataReceived, Sub(sender As Object, e As DataReceivedEventArgs)
                                            If Not String.IsNullOrWhiteSpace(e.Data) Then
                                                'ToDo: add desired code
                                                Debug.WriteLine("error: " & e.Data)

                                            End If
                                        End Sub

        'subscribe to event
        AddHandler p.OutputDataReceived, AddressOf Process_OutputDataReceived

        'update StatusLabels and ProgressBar
        StatusStrip1.Invoke(Sub()
                                ToolStripProgressBar1.Value = 0 're-initialize value
                                ToolStripProgressBar1.Visible = True 'show
                                ToolStripStatusLabel2.Visible = True 'show
                            End Sub)

        p.Start()

        p.BeginErrorReadLine()
        p.BeginOutputReadLine()

        'wait for exit
        p.WaitForExit()

        'update StatusLabels and ProgressBar
        StatusStrip1.Invoke(Sub()
                                ToolStripProgressBar1.Value = 100
                                ToolStripProgressBar1.Visible = False 'hide
                                ToolStripStatusLabel2.Visible = False 'hide
                                ToolStripStatusLabel1.Text = "Status: Complete."
                            End Sub)

    End Using
End Sub

Private Sub Process_OutputDataReceived(sender As Object, e As DataReceivedEventArgs)
    If Not String.IsNullOrWhiteSpace(e.Data) Then
        'ToDo: add desired code
        Debug.WriteLine("output: " & e.Data)

        If e.Data.Length >= 3 AndAlso e.Data.Substring(3, 1) = "%" Then
            Dim percentage As Integer = CInt(e.Data.Substring(0, 3))

            Dim currentFilename As String = String.Empty
            If e.Data.Contains("U") Then
                currentFilename = e.Data.Substring(e.Data.IndexOf("U") + 1).Trim()
            End If

            StatusStrip1.Invoke(Sub()
                                    ToolStripStatusLabel1.Text = $"Processing '{currentFilename}'..."
                                    ToolStripProgressBar1.Value = percentage
                                    ToolStripStatusLabel2.Text = $"{percentage}%"
                                    'StatusStrip1.Refresh()
                                End Sub)

            'If StatusStrip1.InvokeRequired Then
            'StatusStrip1.Invoke(New MethodInvoker(Sub()
            '                                              ToolStripStatusLabel1.Text = $"Processing 2 '{currentFilename}'..."
            '                                              ToolStripProgressBar1.Value = percentage
            '                                              ToolStripStatusLabel2.Text = $"{percentage}%"
            '                                          End Sub))
            'Else
            '   ToolStripProgressBar1.Value = percentage
            '   ToolStripProgressBar1.Value = percentage
            '   ToolStripStatusLabel2.Text = $"{percentage}%"
            'End If
        End If
    End If
End Sub

Usage

'this may take a while, start a new thread; alternatively, use a Task
Dim t As Thread = New Thread(Sub() RunProcess7ZipCompress("C:\Temp\Test\test123.txt", "C:\Temp\Test\Test.7z"))
t.Start()

Resources:

Upvotes: 1

LPChip
LPChip

Reputation: 884

Answering myself

I've decided to also write an answer to show what I ended up doing. I'm sure some people out there want this too, and my approach is a bit more simplified. I will of course keep the answer that allowed me to learn from my mistake to be the accepted answer. This one is just bonus. :)

Run the 7zip process and capture its output to a textbox (for debug)

For the following code, Create a form with a command button and a textbox.

Public Class Form1

    Private Process As New Process
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load

    End Sub

    Private Sub bInteractWithCMD_Click(sender As Object, e As EventArgs) Handles bInteractWithCMD.Click
        Me.Process.StartInfo.FileName = """c:\Program Files\7-Zip\7z.exe"""
        Dim sFileTo As String = "c:\temp\test\test.7z"
        Dim sFileFrom As String = "c:\temp\test\test.vis"
        Me.Process.StartInfo.Arguments = $"a -bsp1 {sFileTo} {sFileFrom}"
        Me.Process.StartInfo.UseShellExecute = False
        Me.Process.StartInfo.CreateNoWindow = True
        Me.Process.StartInfo.RedirectStandardOutput = True
        Me.Process.Start()

        While Me.Process.HasExited = False
            Application.DoEvents()
            Dim sOutput As String = Me.Process.StandardOutput.ReadLine()
            Me.Process.WaitForExit(100)
            Me.TextBox1.Text = sOutput
        End While
    End Sub
End Class

Run the 7zip process and capture its output to a progressbar (end result)

For the following code, you need a command button and a progressbar. I kept the textbox reference in, you can uncomment it to see its result (for debug).

Public Class Form1

    Private Process As New Process
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load

    End Sub

    Private Sub bInteractWithCMD_Click(sender As Object, e As EventArgs) Handles bInteractWithCMD.Click
        Me.Process.StartInfo.FileName = """c:\Program Files\7-Zip\7z.exe"""
        Dim sFileTo As String = "c:\temp\test\test.7z"
        Dim sFileFrom As String = "c:\temp\test\test.vis"
        Me.Process.StartInfo.Arguments = $"a -bsp1 {sFileTo} {sFileFrom}"
        Me.Process.StartInfo.UseShellExecute = False
        Me.Process.StartInfo.CreateNoWindow = True
        Me.Process.StartInfo.RedirectStandardOutput = True
        Me.Process.Start()

        While Me.Process.HasExited = False
            Application.DoEvents()
            Dim sOutput As String = Me.Process.StandardOutput.ReadLine()
            Me.Process.WaitForExit(100)
            If sOutput.Length < 4 Then Continue While
            If sOutput.Substring(0, 4) = "    " Then Continue While
            If sOutput.Substring(3, 1) <> "%" Then Continue While
            Dim percentage As Integer = CInt(sOutput.Substring(0, 3))
            Me.ProgressBar1.Value = percentage
            'Me.TextBox1.Text = sOutput & vbNewLine
        End While
    End Sub
End Class

Upvotes: 0

Related Questions