3

I need to wait until the user is done editing a text file in the default graphical application (Debian and derivates).

If I use xdg-open with subprocess.call (which usually waits) it will continue after opening the file in the editor. I assume because xdg-open itself starts the editor asynchronously.

I finally got a more or less working code by retrieving the launcher for the text/plain mime-type and use that with Gio.DesktopAppInfo.new to get the command for the editor. Provided that the editor is not already open in which case the process ends while the editor is still open.

I have added solutions checking the process.pid and polling for the process. Both end in an indefinite loop.

It seems such a overly complicated way to wait for the process to finish. So, is there a more robust way to do this?

#! /usr/bin/env python3

import subprocess
from gi.repository import Gio
import os
from time import sleep
import sys


def open_launcher(my_file):
    print('launcher open')
    app = subprocess.check_output(['xdg-mime', 'query', 'default', 'text/plain']).decode('utf-8').strip()
    print(app)
    launcher = Gio.DesktopAppInfo.new(app).get_commandline().split()[0]
    print(launcher)
    subprocess.call([launcher, my_file])
    print('launcher close')
    
def open_xdg(my_file):
    print('xdg open')
    subprocess.call(['xdg-open', my_file])
    print('xdg close')
    
def check_pid(pid):        
    """ Check For the existence of a unix pid. """
    try:
        os.kill(int(pid), 0)
    except OSError:
        return False
    else:
        return True
    
def open_pid(my_file):
    pid = subprocess.Popen(['xdg-open', my_file]).pid
    while check_pid(pid):
        print(pid)
        sleep(1)
        
def open_poll(my_file):
    proc = subprocess.Popen(['xdg-open', my_file])
    while not proc.poll():
        print(proc.poll())
        sleep(1)
        
def open_ps(my_file):
    subprocess.call(['xdg-open', my_file])
    pid = subprocess.check_output("ps -o pid,cmd -e | grep %s | head -n 1 | awk '{print $1}'" % my_file, shell=True).decode('utf-8')
    while check_pid(pid):
        print(pid)
        sleep(1)
        
def open_popen(my_file):
    print('popen open')
    process = subprocess.Popen(['xdg-open', my_file])
    process.wait()
    print(process.returncode)
    print('popen close')


# This will end the open_xdg function while the editor is open.
# However, if the editor is already open, open_launcher will finish while the editor is still open.
#open_launcher('test.txt')

# This solution opens the file but the process terminates before the editor is closed.
#open_xdg('test.txt')

# This will loop indefinately printing the pid even after closing the editor.
# If you check for the pid in another terminal you see the pid with: [xdg-open] <defunct>.
#open_pid('test.txt')

# This will print None once after which 0 is printed indefinately: the subprocess ends immediately.
#open_poll('test.txt')

# This seems to work, even when the editor is already open.
# However, I had to use head -n 1 to prevent returning multiple pids.
#open_ps('test.txt')

# Like open_xdg, this opens the file but the process terminates before the editor is closed.
open_popen('test.txt')
  • Are you asking specifically for a solution that uses `xdg-open`? Or are you looking for a solution that avoids `xdg-open`? It also might be worth explicitly stating what operating systems you want the solution to work on. – Shane Bishop Jan 04 '21 at 23:36
  • `xdg-open` is probably overkill; can you simply call `os.environ.get('VISUAL', os.envrion.get('EDITOR', 'vi'))`? – tripleee Jan 05 '21 at 07:47
  • I need to wait until the user is done editing a text file in the default graphical application. I've added the OS (Debian and derivates) to the OP. xdg-open is not a pre-requisite, however it is the usual way on these systems to open files in the default editor. The application is a graphical application, not a terminal application. So, explicitly calling the default editor with os.eviron is not what I need. I have updated the OP to reflect that. – Arjen Balfoort Jan 05 '21 at 07:56
  • Which text editor are you testing with? This would be valuable to know for anyone attempting to answer, so they can test if their solution works. – Shane Bishop Jan 05 '21 at 15:45
  • In my case: kate (plasma5) and mousepad (xfce4). – Arjen Balfoort Jan 07 '21 at 12:09

1 Answers1

1

Instead of trying to poll a PID, you can simply wait for the child process to terminate, using subprocess.Popen.wait():

Wait for child process to terminate. Set and return returncode attribute.

Additionally, getting the first part of get_commandline() is not guaranteed to be the launcher. The string returned by get_commandline() will match the Exec key spec, meaning the %u, %U, %f, and %F field codes in the returned string should be replaced with the correct values.

Here is some example code, based on your xdg-mime approach:

#!/usr/bin/env python3
import subprocess
import shlex
from gi.repository import Gio

my_file = 'test.txt'

# Get default application
app = subprocess.check_output(['xdg-mime', 'query', 'default', 'text/plain']).decode('utf-8').strip()

# Get command to run
command = Gio.DesktopAppInfo.new(app).get_commandline()

# Handle file paths with spaces by quoting the file path
my_file_quoted = "'" + my_file + "'"

# Replace field codes with the file path
# Also handle special case of the atom editor
command = command.replace('%u', my_file_quoted)\
    .replace('%U', my_file_quoted)\
    .replace('%f', my_file_quoted)\
    .replace('%F', my_file_quoted if app != 'atom.desktop' else '--wait ' + my_file_quoted)

# Run the default application, and wait for it to terminate
process = subprocess.Popen(
    shlex.split(command), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
process.wait()

# Now the exit code of the text editor process is available as process.returncode

I have a few remarks on my sample code.

Remark 1: Handling spaces in file paths

It is important the file path to be opened is wrapped in quotes, otherwise shlex.split(command) will split the filename on spaces.

Remark 2: Escaped % characters

The Exec key spec states

Literal percentage characters must be escaped as %%.

My use of replace() then could potentially replace % characters that were escaped. For simplicity, I chose to ignore this edge case.

Remark 3: atom

I assumed the desired behaviour is to always wait until the graphical editor has closed. In the case of the atom text editor, it will terminate immediately on launching the window unless the --wait option is provided. For this reason, I conditionally add the --wait option if the default editor is atom.

Remark 4: subprocess.DEVNULL

subprocess.DEVNULL is new in python 3.3. For older python versions, the following can be used instead:

with open(os.devnull, 'w') as DEVNULL:
    process = subprocess.Popen(
        shlex.split(command), stdout=DEVNULL, stderr=DEVNULL)

Testing

I tested my example code above on Ubuntu with the GNOME desktop environment. I tested with the following graphical text editors: gedit, mousepad, and atom.

Shane Bishop
  • 3,905
  • 4
  • 17
  • 47
  • Thanks, but unfortunately, this also opens the editor and ends the process immediately. The open and close prints are printed while the file is still edited in the default editor. I have added this solution to the OP. – Arjen Balfoort Jan 05 '21 at 07:45
  • @ArjenBalfoort I updated my answer to work with mousepad. Can you please take another look? – Shane Bishop Jan 07 '21 at 17:18
  • Works perfectly, also with Kate! I'll except this as the answer. I'd also like your opinion on my workaround using "ps -o pid,cmd -e". Do you see any draw backs compared to your solution? – Arjen Balfoort Jan 08 '21 at 10:02
  • @ArjenBalfoort Yes, there are drawbacks to using `ps`. Running a child process is always expensive, in any language. Your `ps` approach also is a polling approach, which is not ideal. If it was necessary to poll, then it would be better to use the [`poll()`](https://docs.python.org/3/library/subprocess.html) function to check if a child process is still running, rather than spinning up a full separate `ps` process. – Shane Bishop Jan 08 '21 at 14:50
  • I don't think I understand. Why is calling `ps` a polling action, isn't it called just once? In that case, isn't the `xdg-mime` call also a polling action? Thanks again for your help. – Arjen Balfoort Jan 09 '21 at 12:18
  • 1
    Sorry, I didn't explain very well. You are right, you only execute `ps` once, but then you have a `while` loop where you continuously call `check_pid()`, and you only exit the loop if the return value of `check_pid()` is [falsy](https://stackoverflow.com/q/39983695/8593689). Continuously checking a condition is known as ["polling"](https://en.wikipedia.org/wiki/Polling_(computer_science)). – Shane Bishop Jan 09 '21 at 15:29
  • Thanks for the clarification. I haven't heard of falsy/truly before and the meaning of polling. Never to old to learn. – Arjen Balfoort Jan 10 '21 at 10:53