0

I've written auto-updating software (python that was compiled to exe with pyinstaller). everything works just fine, but one problem I just can't solve - I can't close the cmd window after it finishes updating.

let's say the software exe name is software.exe(this software is single file exe). when started, it checks for updates, if found, it will download it as software.bundle, and will create a temporary script software-update.bat on the current directory. this script is registered to be called when software.exe is closed (using atexit.register).
this script will delete software.exe and rename software.bundle to sofware.exe, will launch software.exe and will delete atself software-update.bat.

so the update is working.

the thing is, in order to delete the .exe I need first to completely close it, this means that the command that executes software-update.bat need to run separately from the main running .exe software.exe.

i could get it to work only with

os.system(f'start {script_update_name} /b')

(again - this command is starting software-update.bat and then software.exe 'immediately' exits)

trying to use subprocess.run or any alternative just resulted with 'Access denied' when trying to delete sofware.exe from the update script(because software.exe was apparently still running).

so finally to my question, solving one of the above will solve my problem:

  1. is there an option to reliably exit cmd window that was stated because of start ... command? adding exit at the end of the script(or any other solution I could find) does not work.
  2. is there an alternative to 'start'? that knows to run bat file separately from the main exe, and yet to exit when finished?

if it helps, here's the update script software-update.bat:

with open(script_update_name, 'w') as f:
    s = dedent(f"""
          @echo off
          echo Updating...
          del {file.__str__()}
          rename {(file.parent / done_download_name).__str__()} {osPath.basename(file.__str__())}
          start {osPath.basename(file.__str__())}
          echo Done
          del {script_update_name}
          """).strip('\n')
    f.write(s)

I know this sounds simple, but honestly, I couldn't solve it by now.

any help would be appreciated!

Eliav Louski
  • 3,593
  • 2
  • 28
  • 52
  • There could be used `os.system(f'start "Updating Software" %SystemRoot%\System32\cmd.exe /D /C "{script_update_name}"')` whereby `{script_update_name}` should expand to a string with full path. `os.system()` results on Windows in starting in background `%SystemRoot%\System32\cmd.exe` with option `/c` and the string in Python code as command line to executed with one or more arguments. In this case the command line is passed to `cmd.exe` with multiple arguments and the criteria as explained by the help output on running `cmd /?` in a command prompt window must be taken into account. – Mofi Dec 06 '21 at 20:12
  • 1
    Python waits with further processing of Python code until the `cmd.exe` started by Python itself with `os.system()` terminated itself which happens on having started successfully one more `cmd.exe` instance with opening a new console window and running parallel to first `cmd.exe` and the Python coded executable. So the batch file needs additional code to check the task list and wait for the self-termination of the Python executable before it can replace it or it simply waits one second which is hopefully always enough time for self-closing of first `cmd.exe` and next of the Python executable. – Mofi Dec 06 '21 at 20:16
  • The batch file should be coded to delete itself with the last line which is possible because a batch file is processed by `cmd.exe` by opening it, reading next line and closing it, before the command line is processed and executed. So the batch file is not opened all the time and `cmd.exe` can delete the batch file with a command written in the batch file itself. See [How to make a batch file delete itself?](https://stackoverflow.com/questions/20329355/) – Mofi Dec 06 '21 at 20:20
  • BTW: [os.system](https://docs.python.org/3.9/library/os.html#os.system) is deprecated. There should be used the [subprocess module](https://docs.python.org/3.9/library/subprocess.html). I recommend to read first the Microsoft documentation of the Windows kernel library function [CreateProcess](https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw). It is used by `cmd.exe` to run any executable without or with using `start` and also by Python on using a function of the `subprocess` module and any other Windows executable starting another exe. – Mofi Dec 06 '21 at 20:26
  • After reading about `CreateProcess` and of course also the [STARTUPINFO](https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-startupinfow) structure, the documentation for the `subprocess` module should be read and used should be `subprocess.Popen` with `STARTUPINFO` to run just one `cmd.exe` with the arguments posted above as __DETACHED_PROCESS__ which means without Python waiting for self-closing of started `cmd.exe` (just one needed). `os.environ['SystemRoot']` can be used to get the path to Windows directory concatenated with `"\\System32\\cmd.exe"`. – Mofi Dec 06 '21 at 20:34

1 Answers1

0

I could solve at with the very helpful comments of @Mofi:

the problem was timing problem - I needed to the .exe process to fully terminated before starting the .bat script because the .bat script is actively trying to remove the .exe file. for this I used os.getpid() in my python code(that is run as .exe) to get the current process pid of the .exe, and the .bat file waited for this PID process to exit, the full script is:

with open(script_update_name, 'w') as f:
s = dedent(f"""
      @echo off
      
      REM wait for process to finish
      :loop
      tasklist | find " {os.getpid()} " >nul
      if not errorlevel 1 (
          timeout /t 1 /nobreak >nul 
          echo waiting for EasySchema to exit...
          goto :loop
      )
      
      echo Updating EasySchema...
      del {file.__str__()}
      rename {(file.parent / done_download_name).__str__()} {osPath.basename(file.__str__())}
      start {osPath.basename(file.__str__())}
      echo Done
      del {script_update_name}
      """).strip('\n')
f.write(s)

the part that running the script was changed from os.system() subprocess.Popen as @Mofi advice

def run_update_script():
  subprocess.Popen(['cmd', '/C', file.parent / script_update_name])

with this I could reliably update my .exe and close the terminal after ended.

Eliav Louski
  • 3,593
  • 2
  • 28
  • 52