0

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

  • How to debug the execution of a binary with execl?
lf_araujo
  • 1,991
  • 2
  • 16
  • 39

1 Answers1

1

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.

Community
  • 1
  • 1
AlThomas
  • 4,169
  • 12
  • 22
  • Thanks for the thorough answer! Absolutely clear. One doubt is regarding the begin part of convert.begin... never seen this syntax, what is it for? – lf_araujo Jun 25 '16 at 10:14
  • It is part of making a method asynchronous. There is `convert.begin()` and also `convert.end()`, that is what I mean by the function is split in two. Both `.begin()` and `.end()` are added in the background by the Vala compiler. In this example `convert.end()` is not used because it is a simpler example. – AlThomas Jun 25 '16 at 13:22