Hide CLI while grabbing its stdout?

Hello,

I use a wxPython GUI to run a CLI application. It’s just a listbox and a button to trigger the action.

Is it possible to hide the CLI console while still being able to grab its stdout output and display it in the GUI?

Thank you.

import wx, subprocess
 
CMD = fr'C:\blah.exe'
 
class ListBoxFrame(wx.Frame):
    def __init__(self, *args, **kwargs):  
        super().__init__(None, -1,title='Bulk Download')
         
        panel = wx.Panel(self, -1)
 
        sizer = wx.BoxSizer(wx.VERTICAL)
 
        self.lb1 = wx.ListBox(panel, -1, style=(wx.LB_SINGLE | wx.LB_ALWAYS_SB))
        sizer.Add(self.lb1,1, wx.ALL | wx.EXPAND ,5)
 
        self.dload_btn = wx.Button(panel, -1, "Download")
        sizer.Add(self.dload_btn,0, wx.EXPAND)
        self.dload_btn.Bind(wx.EVT_BUTTON, self.OnDloadButtonClick)
 
        panel.SetSizer(sizer)
 
    def OnDloadButtonClick(self,event):
        for i in range(self.lb1.GetCount()):
            URL = self.lb1.GetString(i)
            my_command = fr'{CMD} {URL}'
            p = subprocess.Popen(my_command, stdout=subprocess.PIPE, text=True)
            while (line := p.stdout.readline()) != "":
                self.statusbar.SetStatusText(line)
                wx.Yield()
        self.statusbar.SetStatusText("Done.")
 
app = wx.App()
ListBoxFrame().Show()
app.MainLoop()

This appears to be Windows specific as it doesn’t happen on linux.

I found several references to hiding the console on Windows when using Popen(). e.g. https://stackoverflow.com/questions/1813872/running-a-process-in-pythonw-with-popen-without-a-console

That thread was started back in the days of Python 2, so some of the answers may not work in Python 3. I can’t test that as I don’t use Windows. Also, beware of the security risks when using shell=True.

Side note: you omitted the call to self.CreateStatusBar() in your example.

Thanks. It seem impossible to run a CLI with no console and still get its output so it can be displayed in the GUI.

With the extension as .pyw, it still displays a console.

And with this code, I get no download:

import sys,os, wx, pyperclip, re
#import subprocess
from subprocess import Popen, PIPE, CREATE_NO_WINDOW

CMD = fr'C:\blah.exe"
class ListBoxFrame(wx.Frame):
	def __init__(self, *args, **kwargs):  
		super().__init__(None, -1,title='Bulk Youtube Download')

		self.statusbar = self.CreateStatusBar()

		panel = wx.Panel(self, -1)

		sizer = wx.BoxSizer(wx.VERTICAL)

		self.lb1 = wx.ListBox(panel, -1, style=(wx.LB_SINGLE | wx.LB_ALWAYS_SB))
		sizer.Add(self.lb1,1, wx.ALL | wx.EXPAND ,5)

		self.add_btn = wx.Button(panel, -1, "Add URL")
		sizer.Add(self.add_btn,0, wx.EXPAND)
		self.add_btn.Bind(wx.EVT_BUTTON, self.OnAddButtonClick)

		self.dload_btn = wx.Button(panel, -1, "Download")
		sizer.Add(self.dload_btn,0, wx.EXPAND)
		self.dload_btn.Bind(wx.EVT_BUTTON, self.OnDloadButtonClick)

		panel.SetSizer(sizer)

	def OnAddButtonClick(self,event):
		URL = pyperclip.paste()
		self.lb1.Append(URL)

	def OnDloadButtonClick(self,event):
		self.add_btn.Disable()
		self.dload_btn.Disable()
		wx.Yield()
		
		for i in range(self.lb1.GetCount()):
			URL = self.lb1.GetString(i)
			print(f"Starting {URL}") #not shown
			self.statusbar.SetStatusText(URL)
			my_command = fr'{CMD} {URL}'

			#OK p = subprocess.Popen(my_command, stdout=subprocess.PIPE, text=True)

			#BAD p = subprocess.Popen(my_command, stdout = subprocess.PIPE, creationflags = subprocess.CREATE_NO_WINDOW)
			#wx._core.wxAssertionError: C++ assertion ""(unsigned)number < m_panes.size()"" failed at ..\..\src\common\statbar.cpp(259) in wxStatusBarBase::SetStatusText(): invalid status bar field index"""
			
			#TRY from subprocess import Popen, PIPE, CREATE_NO_WINDOW
			#+
			p = subprocess.Popen(my_command)
			while (line := p.stdout.readline()) != "":
				self.statusbar.SetStatusText(line)
				wx.Yield()
			output = f"End of output.  Return code: {p.wait()}"
			print(output)
			self.statusbar.SetStatusText(output)
			wx.Yield()

		self.add_btn.Enable()
		self.dload_btn.Enable()
		self.statusbar.SetStatusText("Done.")

app = wx.App()
ListBoxFrame().Show()
app.MainLoop()

Didn’t try it with python, but there was an API call under windowses (I think shellExecute), where you could pass ‘HID’ as one of parameters, so while you’d still have a console window, it wouldn’t show.

Don’t know how would that translate into the .Popen() call, and does it use the same call in the first place.

Thanks. I’ll let it on the backburner since no solution worked so far.

#script doesn't run: stdout can't be read if terminal hidden?
p = subprocess.Popen(my_command, stdout=subprocess.PIPE, text=True, creationflags = CREATE_NO_WINDOW)
while (line := p.stdout.readline()) != "":
	self.statusbar.SetStatusText(line)
output = f"End of output.  Return code: {p.wait()}"

I didn’t try using your example. I just threw something together, but this seems to work for me:

import subprocess
import wx

class MyForm(wx.Frame):

    def __init__(self):
        super().__init__(None,
                         title="wxPython Redirect Tutorial")


        panel = wx.Panel(self, wx.ID_ANY)
        style = wx.TE_MULTILINE|wx.TE_READONLY|wx.HSCROLL
        self.log = wx.TextCtrl(panel, wx.ID_ANY, size=(300,100),
                          style=style)
        btn = wx.Button(panel, wx.ID_ANY, 'Push me!')
        self.Bind(wx.EVT_BUTTON, self.on_button, btn)

        # Add widgets to a sizer
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.log, 1, wx.ALL|wx.EXPAND, 5)
        sizer.Add(btn, 0, wx.ALL|wx.CENTER, 5)
        panel.SetSizer(sizer)


    def on_button(self, event):
        process = subprocess.Popen("ping www.gooogle.com", stdout=subprocess.PIPE)
        for line in iter(process.stdout.readline, b""):
            self.log.AppendText(line)

# Run the program
if __name__ == "__main__":
    app = wx.App(False)
    frame = MyForm().Show()
    app.MainLoop()

Actually I now remembered that I’m already doing it as well, in my mrz (mp3 read as cyrillic) player, when I chop albums:

			loProc = subprocess.run(["ffmpeg", "-i", tcAlbum, "-ss", s1, "-to", s2, "-acodec", "copy", lcIzlFajl],
										capture_output=True, text=True)
			print(loProc.stdout, loProc.stderr)

and it does exactly as required in this case.

Thanks for the two suggestions, but neither works (on Windows at least).

import sys,os, wx
import subprocess

class ListBoxFrame(wx.Frame):
	def __init__(self, *args, **kwargs):  
		super().__init__(None, -1,title='Bulk Youtube Download')
		
		self.Centre()

		self.statusbar = self.CreateStatusBar()

		self.dload_btn = wx.Button(self, -1, "Test")
		self.dload_btn.Bind(wx.EVT_BUTTON, self.OnTestButtonClick)

	def OnTestButtonClick(self,event):
		CMD="ping -n 10 www.gooogle.com"

		"""
		#.py OK, but .pyw NOK: Still shows console
		process = subprocess.Popen(CMD, stdout=subprocess.PIPE)
		for line in iter(process.stdout.readline, b""):
			self.statusbar.SetStatusText(line)
			wx.Yield()
		"""
		
		#Doesn't show progress
		loProc = subprocess.run(CMD, capture_output=True, text=True)
		print(self.statusbar.SetStatusText(loProc.stdout))

		self.statusbar.SetStatusText("Done.")

app = wx.App()
ListBoxFrame().Show()
app.MainLoop()

My app, RIDE, is started on Windows with pythonw.exe, so no CLI is started, but when running a test case execution, it creates the stdout/stderr, which makes a momentary opening of a CLI window. This way the output of the command robot is captured.
Maybe you can get the idea by exploring RIDE and its source. In particular, the code at contrib/testrunner. Note, I did not created that code, it was a nice contribution by a user.

1 Like

As it looks impossible 1) on Windows to 2) show live progress after 3) running an external CLI application 4) with no terminal window (“cmd”, “DOS box” in Windows-speak)… someone suggested a work-around: 1) Launch the app with no window (CREATE_NO_WINDOW)) and 2) Use a timer to read stdout:

from subprocess import Popen, PIPE, CREATE_NO_WINDOW

CMD = "ping -n 10 www.google.com"

class ListBoxFrame(wx.Frame):
	def __init__(self, *args, **kwargs):  
		...
		self.statusbar = self.CreateStatusBar()

		self.dload_btn = wx.Button(panel, -1, "Download")
		self.dload_btn.Bind(wx.EVT_BUTTON, self.OnDloadButtonClick)

		self.update_timer = wx.Timer(self, 1)
		self.Bind(wx.EVT_TIMER, self.update_status)

	def OnDloadButtonClick(self,event):
		self.process = Popen(CMD,stdout=PIPE,universal_newlines=True,creationflags=CREATE_NO_WINDOW)
		self.update_timer.Start(100)
			
	def update_status(self, event):
		wx.Yield() #to keep the UI alive
		
		if self.process.poll() is None:
			# Process is still active.
			line = self.process.stdout.readline()
			if line:
				self.statusbar.SetStatusText(line)
		else:
			# Process finished.  Fetch remaining stdout.
			for line in self.process.stdout.readlines():
				self.statusbar.SetStatusText(line)
			self.update_timer.Stop()
			self.statusbar.SetStatusText("Done.")