A Debugging Toolkit for Lightroom 5Ð6 SDK

Version 1.2

 

Copyright 2010-2017, John R. Ellis. You may use this toolkit for any purpose, as long as you include this notice in any versions derived in whole or part from these files.

 

This Debugging Toolkit consists of:

 

-       strict.lua: The standard script from the Lua distribution that detects undeclared global variables (usually typos).

 

-       Require.lua: A replacement for the standard require that provides the ability to reload all files and to define a search path for loading .lua files from shared directories.

 

-       debuggingtoolkit.lrdevplugin: Provides quick, easy loading and reloading of scripts (whether or not they're part of a plugin) and executing them with the debugger.

 

-       Debug.lua: A standalone debugging module that provides an interactive debugger with stack traces, breakpoints, stepping, and evaluation of expressions; a "pretty printer" that nicely formats any Lua value (including nested and circular tables); some logging tools; and a rudimentary elapsed-time profiler for functions.

 

You should expect to take about an hour to prepare your plugin for debugging and get up to speed on how to use the debugger. But this investment will quickly pay offÑtrying to debug plugins with print statements is an exercise in total frustration, given how mysterious Lua errors can be and Lightroom's often silent treatment of errors.

 

Getting Started

Using strict.lua

Using the Debug Script Plugin

Using Require.lua

Using the Debug.lua Debugger

Preparing Your Code

Examining the Call Stack

Evaluating Expressions and Statements

Resuming Execution

Setting Breakpoints

The Log

Setting the Source Path

Other Debug facilities

Getting Started

 

1.     Unzip the toolkit and place debuggingtoolkit.lrdevplugin  in your plugins folder.

                                         

2.     Use the Lightroom Plug-in Manager to add debuggingtoolkit.lrdevplugin as a plugin.

 

3.     Copy Require.lua from the debuggingtoolkit.lrdevplugin  directory to the directory of the plugin you want to debug. 

 

4.     Place these lines at the top of each of your plugin's .lua files, including service definition scripts but not Info.lua or scripts defining tagsets and metadata:

 

local Require = require "Require".path ("../debuggingtoolkit.lrdevplugin").reload ()

local Debug = require "Debug".init ()

require "strict"

 

When your plugin executes from a directory ending in .lrdevplugin, the debugger will be enabled; when it executes from any other directory (e.g. a release directory ending in .lrplugin) it will be disabled and have no impact on execution.

 

5.     Wrap all functions passed to the SDK API and your main showDialog function with Debug.showErrors. For example:

 

viewFactory:edit_field {validate = Debug.showErrors (myValidate)}

   

LrTask.startAsyncTask (Debug.showErrors (function () ...))

 

This will cause any errors within the functions to be trapped and invoke the debugger. Failing to do this will often cause the errors to be silently ignored by Lightroom, with no indication anything has gone wrong. Be sure to wrap callbacks passed to LrView controls, main functions of tasks, functions return by export and publish-service definition scriptions, and any other functions passed to the SDK API.

 

6.     Wrap the main function of your File or Library menu plugin (often called showDialog) with Debug.showErrors.  For example:

 

Debug.callWithContext ("showDialog",

    Debug.showErrors (function (context)

        showDialog (context)

        end))

 

7.     Replace uses of the Lua pcall and LrTasks.pcall with Debug.pcall. Replace uses of LrFunctionContext.callWithContext with Debug.callWithContext. This allows the debugger to trace the call stack across calls to these functions and enables breaks and stepping within the calls.

 

8.     Place a call to Debug.pauseIfAsked() as the first line of the showDialog function (for menu plugins) or the startDialog function (for export and publish-service plugins).

 

9.     Add the dummy file _debug.txt to your plugin directory (you can copy it from debuggingtoolkit.lrdevplugin or make an empty file with a text editor)

 

10.  Run your plugin. The debugger will open a window when it reaches the call to Debug.pauseIfAsked(), letting you set breakpoints before resuming execution.

 

11.  When you're done debugging, rename _debug.txt to __debug.txt so the debugger won't be invoked (unless the plugin encounters an error). Whenever you want the debugger to be invoked at plugin startup to set breakpoints, rename the file back.

Using strict.lua

 

Perhaps the most common Lua programming mistake is to mistype the name of variableÑLua will silently assume the mistyped name is a global variable.  If you include the following at the top of each file:

 

require "strict.lua"

   

any attempt to access a global variable that hasn't been "declared" will raise an error.  You "declare" a global variable simply by assigning it a value (or defining it as a function) at the top level of a file.

 

Of everything in this toolkit, strict.lua has the biggest bang for the buck. Use it.

Using the Debug Script Command

 

The Debug Script plugin provides quick, easy loading and reloading of scripts (whether or not they're part of a plugin) and executing them with the debugger. You can use Debug Script to run code without using the Plug-in Manager or creating a separate plugin directory and Info.lua.

 

To use the Debug Script command:

 

1.     File > Plug-in Extras > Debug Script.

 

2.     Browse to the .lua file you want to run.

 

3.     Each time you click Run, Debug Script will reload the file and any files loaded by nested requireÕs, executing the files in a new global environment.  If an error occurs, the source file will be displayed in the configured text editor at the appropriate line.

 

Click Debug options to configure the text editor you want to use with Debug Script.  On Windows, by default it will use TextPad if it's installed, Notepad otherwise.  On Mac, by default it will use TextEdit. (Neither Notepad nor TextEdit know how to display a source file at a particular line number.)

 

The Show new globals option shows global variables that were defined as a result of loading the file.  If your programming style avoids global variables, this helps you enforce that convention. 

 

The Reload all required scripts option forces the reloading of all nested requireÕd files.  The only reason to uncheck this is to test out persistent state that may be maintained by some of the modules loaded by the main file.

 

The Clear LrPrefs.prefsForPlugin option deletes any preferences before executing the script.

Using Require.lua

 

Require.lua provides a compatible replacement for the standard require that provides the ability to for loading shared files from common directories and to automatically reload all files each time your plugin executes during development (but not when released).

 

First, place a copy of Require.lua in your plugin directory.  To load files from a shared directory common that's a sibling of your plugin directory, put the following at the top of your plugin's main file:

 

local Require = require "Require".path ("../common")

   

Now require will look in the common directory for any files that aren't found in the plugin directory. You can provide more than one directory in the call to path.

 

To deploy your plugin to customers, you could continue to use the same directory structure.  However, I recommend compiling all the plugin-specific and common files into a single release directory that gets shipped to your customers.  That simplifies the installation for them by not having to create the common directory.   And if you have two plugins sharing a common file, each gets its own copy (and version) of the file, letting a customer upgrade one plugin without upgrading the other.  (Since each plugin executes in its own environment, each will compile and load its own copy of a shared file, regardless of whether it is loaded from a common directory.)

 

To force reloading of requireÕd files each time you run the plugin without invoking the Plug-in Manager, use the .reload() option:

 

local Require = require "Require".path ("../common").reload()

   

Whenever the main file is executed from a directory ending in .lrdevplugin, any subsequent nested requireÕs will be reloaded, regardless if they had been previously loaded.    The .reload() option has no effect when executed from a directory ending in .lrplugin (a release directory) Ð your released plugin will load files just once, when it is first invoked.

 

For export and publish-service plugins, you'll have to do a little extra in the service-definition script in addition to using the .reload() option. Where formerly it might have contained:

 

local ExportDialogSections = require "ExportDialogSections"

return {

    É

    startDialog = ExportDialogSections.startDialog

 

change it to be:

 

local ExportDialogSections = require "ExportDialogSections"

return {

    É

    startDialog = Debug.showErrors (function (...)

        require "ExportDialogSections".startDialog (...)

        end),

 

This will force ExportDialogSections to be reloaded each time the export or publish is executed (but not when the plugin is deployed in a .lrplugin folder).

 

Unfortunately, you can't use the Plug-in Manager's option Reload plug-in on each exportÑit will trigger a Lightroom bug, and you'll see the error "attempt to yield across metamethod/C-call boundary" in the Plug-in Manager. 

Using the Debug.lua Debugger

 

Debug.lua provides a simple interactive debugger with error trapping, breakpoints, stepping, stack traces, and evaluation of variable and expressions. 

 

Make sure you've prepared your code as described in Getting Started.

 

Once youÕve prepared your code, the debugger window will appear whenever a runtime error occurs, when Debug.pauseIfAsked, Debug.pause, or Debug.pauseIf is called, or the plugin hits a breakpoint.

Examining the Call Stack

 

The scrollable call stack at the top of the debugger window shows all the Lua calls on the call stack. Clicking on a call will open the corresponding source file in the configured text editor. It will also show all the in-scope variables in the scrollable pane below.  Clicking on a variable will pop up a scrollable window showing the variable's value, pretty-printed and unelided.

 

Note that by the time the debugger traps an error, the call stack has been truncated back to the point where the error is trapped (typically a call to Debug.showErrors). Though Lua provides a way of trapping the error at the point where it occurs (xpcall), that is not available in Lightroom (at least as far as I can determine).

Evaluating Expressions and Statements

 

You can enter an expression and click Eval, and the result will be pretty-printed in the results pane.   The expression will be evaluated in the context of the currently selected call on the stack, with access to all the variables shown for the call. The expression is always evaluated in an asynchronous task (essential for some API calls).

 

To see the entire result value in a scrollable pop-up window, click View Result.

 

Click the _ and _ buttons to call up previously executed expressions.

 

Checking Evaluate automatically will evaluate the expression each time the debugger window opens. (Separate multiple expressions with commas.)

 

Note that you can always re-import a Lightroom namespace to access it in an expression, e.g.

 

import "LrApplication".activeCatalog ():getPath ()

 

To execute a statement rather than evaluate an expression, prefix it with a period:

 

.x [i] = myFunc(j)

Resuming Execution

 

Go resumes execution.

 

Step resumes execution until the next Lua source line.

 

Step Over resumes execution until the next Lua source line at the same call-stack level; useful for stepping over function calls.

 

Step Up resumes execution until the next Lua source line at a earlier call-stack level, typically the caller of this function.

 

Stop terminates execution by raising a distinguished error.

Setting Breakpoints

 

There are two ways of setting breakpoints:

 

1.     Click Breaks and enter one or more source files and line numbers.  Optionally enter a conditional expression that will be evaluated in the context of the breakpointÑif it evaluates to true, the break will be taken. Breaks are remembered in the plugin preferences.

 

2.     Insert a call to Debug.pause() or Debug.pauseIf (expr) in your code. The latter opens the debugger window only if expr evaluates to true.

Setting the Source Path

 

Debug looks for source files in the plugin's directory (_PLUGIN.path). If you use Require.path to load files from other directories, Debug will use the same search path to locate the sources.

 

If you have your own module-loading scheme, you can explicitly give Debug a search path:

 

Debug.path ("../common", "../base")

Other Debug facilities

 

See Debug.lua for complete documentation.

 

Debug.pp

 

Debug.pp is a pretty printer that formats any Lua value as a string suitable for logging or display.  Debug.pp shows the contents of tables properly indented, showing each nested table at most once, even circular tables.   The debugger uses Debug.pp for displaying values.

 

Debug.log

 

Debug.log is an LrLogger that writes to plugin-directory/debug.log. It only creates the file if the plugin actually outputs something.  To log one or more values on a single line, converted to strings and separated by spaces:

 

Debug.logn ("n", n, "i > n", i > n)

 

To log a formatted string:

 

Debug.log:tracef ("%d items are stale", n)

 

To log one or more values pretty-printed with Debug.pp:

 

Debug.lognpp (table1, array1)

   

Debug.profileFunc

 

You can measure the total number of calls and elapsed time per call of a function using the Debug profiler:

 

myFunc = Debug.profileFunc (myFunc, "myFunc")

...execute code that calls myFunc one or more times...

Debug.logn ("\n", Debug.profileResults())

   

The profiler properly handles recursive calls to functions.