genie

Execl error debuging


Aim

My objective is to create a simple frontend to pandoc. I have learned that execl is a good way of calling executables in the system.

Note in the following code the function btn_pressed, that calls pandoc using the mentioned method.

[indent=4]
uses
    Posix
    Gtk


class TestWindow:Window
    _file_chooser:FileChooserButton
    _entry:Gtk.Entry
    _button:Gtk.Button
    _file:File

    construct()
        title = "Pandoc GUI"
        window_position = WindowPosition.CENTER
        destroy.connect( Gtk.main_quit )
        var folder_chooser = new FileChooserButton("Choose a Folder",FileChooserAction.SELECT_FOLDER)
        folder_chooser.set_current_folder( Environment.get_home_dir() )

        //I used selection_changed directly as per the question in stack_exchange
        //http://stackoverflow.com/questions/34689763/the-signal-connect-syntax
        folder_chooser.selection_changed.connect( folder_changed )

        _file_chooser = new FileChooserButton("Choose a File",FileChooserAction.OPEN)
        _file_chooser.set_current_folder( Environment.get_home_dir() )

        _file_chooser.file_set.connect( file_changed )
        _entry = new Gtk.Entry()
        _entry.set_text("Here the file name")

        _button = new Button.with_label("Convert to pdf")
        _button.set_sensitive(false)
        _button.clicked.connect(btn_pressed)

        var box = new Box( Orientation.VERTICAL, 0 )
        box.pack_start( folder_chooser, true, true, 0 )
        box.pack_start( _file_chooser, true, true, 0 )
        box.pack_start( _entry, true, true, 0 )
        box.pack_start( _button, true, true, 0 )
        add( box )

    def folder_changed( folder_chooser_widget:FileChooser )
        folder:string = folder_chooser_widget.get_uri()
        _file_chooser.set_current_folder_uri( folder )

    def file_changed ( file_chooser_widget: FileChooser )
        _file = File.new_for_uri(file_chooser_widget.get_uri())

        try
            info:FileInfo = _file.query_info (FileAttribute.ACCESS_CAN_WRITE, FileQueryInfoFlags.NONE, null)
            writable: bool = info.get_attribute_boolean (FileAttribute.ACCESS_CAN_WRITE)
            if !writable
                _entry.set_sensitive (false)
            else
                _button.set_sensitive (true)
        except e: Error
            print e.message

        _entry.set_text(_file.get_basename())

    def btn_pressed ()
        var md_name=_entry.get_text()+".md -s -o "+_entry.get_text()+".pdf"
        execl("/usr/bin/pandoc", md_name)
        _button.set_sensitive (false)

init
    Gtk.init( ref args )
    var test = new TestWindow()
    test.show_all()
    Gtk.main()

Error

At execution I get no response at all from my code, without any pdf being rendering as well.

Question


Solution

  • I would use GLib.Subprocess to call external commands because it provides better control over the input to and output from the external command. Changing the example below to execl should be easy enough though.

    The first thing is to de-couple your external command from your window object. This makes it more testable. To do this a separate object is used - a wrapper around the Subprocess call. Save this code as ToPDF.gs:

    namespace FileConverters
    
        class ToPDF
    
            const _command:string = "pandoc"
    
            def async convert( source:string, output:string )
                try
                    var flags = SubprocessFlags.STDOUT_PIPE \
                                | SubprocessFlags.STDERR_PIPE
                    var subprocess = new Subprocess( flags, 
                                                     _command, 
                                                     source, 
                                                     output 
                                                    )
                    output_buffer:Bytes
                    yield subprocess.communicate_async( null, 
                                                        null,
                                                        out output_buffer, 
                                                        null 
                                                       )
                    if ( subprocess.get_exit_status() == 0 )
                        debug( "command successful: \n %s",
                               (string)output_buffer.get_data() 
                              )
                    else
                        debug( "command failed" )
                except err:Error
                    debug( err.message )
    

    The ToPDF class is now de-coupled from the rest of your application. This means it can be re-used. To illustrate this an integration test is shown below that uses the class.

    The ToPDF also uses asynchronous code. So I will explain that first. Making a method asynchronous means it will run concurrently with the main thread of the application. By have a call to an external program run concurrently it means the main thread doesn't lock up while it is waiting for the external program to finish. Using async means the function is split in two. The first part is called with convert.begin( source, output ) and will run up to the yield command. At that point the execution of the program splits in two. The main thread will return to the caller of convert.begin, but what has started in the background is the Subprocess. When the Subprocess finished it returns to convert and finishes the method call.

    Save the integration test as ToPDFTest.gs:

    uses FileConverters
    
    init
        var a = new ToPDF()
        a.convert.begin( "source_file.md", "output_file.pdf" )
    
        var loop = new MainLoop()
        var loop_quitter = new LoopQuitter( loop )
        Timeout.add_seconds( 2, loop_quitter.quit )
        loop.run()
    
    class LoopQuitter
        _loop:MainLoop
    
        construct( loop:MainLoop )
            _loop = loop
    
        def quit():bool
            _loop.quit()
            return false
    

    Compile with valac --pkg gio-2.0 ToPDF.gs ToPDFTest.gs Then run the test with G_MESSAGES_DEBUG=all ./ToPDFTest

    The test uses MainLoop, which is the base class of Gtk.Main. To simulate a long running program a two second timeout is set then MainLoop.quit() is called to end the test. Unfortunately MainLoop.quit() doesn't have the right function signature for a Timeout callback so a wrapper class, LoopQuitter is used.

    Integration tests like this are often kept and run before a software release to ensure the application is working with other software modules.

    To integrate ToPDF with your window you need to change

    execl("/usr/bin/pandoc", md_name)

    to something like

    var to_pdf = new Fileconverts.ToPDF()
    to_pdf.convert.begin( md_name, pdf_name )
    

    You may also want to wrap it in a command pattern similar to Avoiding global variables in Genie . You may also want to modify it to provide better feedback to the user.