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.
Evaluating Expressions and Statements
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.
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.
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.
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.
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.
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).
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)
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.
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.
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")
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.