vb.netcmdprocess7zip

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.


Solution

  • 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()
    

    Resources: