-
Type:
New Feature
-
Resolution: Unresolved
-
Priority:
Normal
-
None
-
Affects Version/s: 3.0.2
-
Component/s: Plugins API
-
None
Picard runs external programs in several places (fpcalc fingerprinting, the fpcalc version check, GNOME/KDE icon-theme detection), and each site reimplements the same plumbing independently: hiding the console window on Windows, capturing and decoding output, handling non-zero exit codes, and surfacing the program's own error message. This logic is currently scattered across QProcess usage in picard/acoustid and the fingerprinting options page, subprocess in picard/util, and os.popen in picard/util/icontheme.
Plugins have no supported way to run an external tool, so plugin authors must hand-roll subprocess/QProcess and get the threading and error handling right themselves. For example, the MusicBrainz ReplayGain 2.0 plugin shells out to rsgain and shipped a bug where a failing command's diagnostic output was discarded.
Proposed solution
Add a reusable external command facility (picard/util/externalcommand.py) with two layers:
- A dependency-light, Qt-free discovery/data layer (CommandResult, CommandError, find_executable(), synchronous run_command_sync()).
- A QProcess-based async layer (ExternalCommand and a bounded ExternalCommandRunner pool with per-key cancellation) that runs off the UI thread and reports back on the main thread.
Both layers capture stdout/stderr as UTF-8, hide the console on Windows, accept caller-declared acceptable non-zero exit codes (e.g. fpcalc's exit code 3), support an optional timeout, and preserve program output on failure.
Migrate Picard's own callers (fpcalc fingerprinting, the fpcalc -v version check, icon-theme detection) onto the facility, and expose it through the v3 plugin API (api.find_executable(), api.run_command(), api.cancel_commands(), api.set_max_concurrent_commands()), cancelling a plugin's running commands when it is disabled.