From 26c7146b6b8cad7d1420ae810ca3773dca73c4ba Mon Sep 17 00:00:00 2001 From: rcassani Date: Tue, 10 Mar 2026 11:37:45 -0400 Subject: [PATCH 01/48] Add "Container engine" as option --- toolbox/core/bst_get.m | 10 ++++++++++ toolbox/core/bst_set.m | 2 +- toolbox/gui/panel_options.m | 19 +++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/toolbox/core/bst_get.m b/toolbox/core/bst_get.m index e229b4eaf9..4a4112aaea 100644 --- a/toolbox/core/bst_get.m +++ b/toolbox/core/bst_get.m @@ -2994,6 +2994,16 @@ end end + case 'ContainerEngine' + containerEngines = {'auto-detect', 'docker', 'podman'}; + % Get saved value + if isfield(GlobalData, 'Preferences') && isfield(GlobalData.Preferences, 'ContainerEngine') && ~isempty(GlobalData.Preferences.ContainerEngine) + argout1 = GlobalData.Preferences.ContainerEngine; + else + argout1 = containerEngines{1}; + end + argout2 = containerEngines; + case 'ElectrodeConfig' % Get modality Modality = varargin{2}; diff --git a/toolbox/core/bst_set.m b/toolbox/core/bst_set.m index 49c76f2387..afdea60dd4 100644 --- a/toolbox/core/bst_set.m +++ b/toolbox/core/bst_set.m @@ -277,7 +277,7 @@ function bst_set( varargin ) 'StatThreshOptions', 'ContactSheetOptions', 'ProcessOptions', 'BugReportOptions', 'DefaultSurfaceDisplay', ... 'MagneticExtrapOptions', 'MriOptions', 'ConnectGraphOptions', 'NodelistOptions', 'IgnoreMemoryWarnings', 'SystemCopy', ... 'TimefreqOptions_morlet', 'TimefreqOptions_hilbert', 'TimefreqOptions_fft', 'TimefreqOptions_psd', 'TimefreqOptions_stft', 'TimefreqOptions_plv', ... - 'OpenMEEGOptions', 'DuneuroOptions','NIRSTORMOptions', 'DigitizeOptions', 'PcaOptions', 'CustomColormaps', 'PluginCustomPath', 'BrainSuiteDir', 'PythonExe', ... + 'OpenMEEGOptions', 'DuneuroOptions','NIRSTORMOptions', 'DigitizeOptions', 'PcaOptions', 'CustomColormaps', 'PluginCustomPath', 'BrainSuiteDir', 'PythonExe', 'ContainerEngine', ... 'GridOptions_headmodel', 'GridOptions_dipfit', 'LastPsdDisplayFunction', 'LastTfDisplayFunction', 'KlustersExecutable', 'ExportBidsOptions', 'ShowHiddenFiles'} GlobalData.Preferences.(contextName) = contextValue; diff --git a/toolbox/gui/panel_options.m b/toolbox/gui/panel_options.m index dad8613300..78bd67d571 100644 --- a/toolbox/gui/panel_options.m +++ b/toolbox/gui/panel_options.m @@ -147,6 +147,16 @@ jBlockSize.setToolTipText(blockSizeTooltip); jPanelRight.add('br hfill', jPanelProc); + % ===== RIGHT: CONTAINER ENGINE ===== + [~, tmp] = bst_get('ContainerEngine'); + jPanelContainers = gui_river([5 5], [0 15 15 15], 'Container engine'); + jContainerLabel = gui_component('Label', jPanelContainers, [], 'Container engine for container-based plugins: ', [], [], []); + jContainerCombo = gui_component('Combobox', jPanelContainers, 'tab', [], {tmp}, [], [], []); + containerTooltip = 'Default: "auto-detect"'; + jContainerLabel.setToolTipText(containerTooltip); + jContainerCombo.setToolTipText(containerTooltip); + jPanelRight.add('br hfill', jPanelContainers); + % ===== RIGHT: RESET ===== if (GlobalData.Program.GuiLevel == 1) jPanelReset = gui_river([5 5], [0 15 15 15], 'Reset Brainstorm'); @@ -241,6 +251,10 @@ function LoadOptions() jCheckUseSigProc.setSelected(bst_get('UseSigProcToolbox')); processOptions = bst_get('ProcessOptions'); jBlockSize.setText(num2str(processOptions.MaxBlockSize * 8 / 1024 / 1024)); + % Container engine + [containerEngine, containerEngines] = bst_get('ContainerEngine'); + iSel = find(strcmpi(containerEngine, containerEngines), 1, 'first') - 1; + jContainerCombo.setSelectedIndex(iSel); end @@ -387,6 +401,11 @@ function SaveOptions() processOptions.MaxBlockSize = blockSize * 1024 * 1024 / 8; % Mb to bytes bst_set('ProcessOptions', processOptions); end + + % ===== CONTAINER ENGINE ===== + bst_set('ContainerEngine', char(jContainerCombo.getSelectedItem())); + + % Stop applying preferences bst_progress('stop'); % If the scaling was changed: Restart brainstorm From b658e248086eec61e6709203b9f841377d5c2150 Mon Sep 17 00:00:00 2001 From: rcassani Date: Tue, 10 Mar 2026 11:38:28 -0400 Subject: [PATCH 02/48] Add `ImageReference` field to `PlugDes` for container-based plugins --- toolbox/db/db_template.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/toolbox/db/db_template.m b/toolbox/db/db_template.m index d5ac6a20e7..df2e661e92 100644 --- a/toolbox/db/db_template.m +++ b/toolbox/db/db_template.m @@ -1226,7 +1226,8 @@ 'Path', [], ... % Set at runtime: Installation path for this plugin 'Processes', [], ... % List of process functions to be added to the pipeline manager 'isLoaded', 0, ... % Set at runtime: 0=Not loaded, 1=Loaded (folder and specific subfolders added to Matlab path) - 'isManaged', 0); % Set at runtime: 0=Installed by the user, 1=Installed automatically by Brainstorm + 'isManaged', 0, ... % Set at runtime: 0=Installed by the user, 1=Installed automatically by Brainstorm + 'ImageReference',''); % String to that uniquely identifies a container (to be used by plugin) image from a registry template.LoadFolders = {}; template.UnloadPlugs = {}; template.RequiredPlugs = {}; From 9f6081bd7a4d74bc037d5aef0cc50a0768636d4b Mon Sep 17 00:00:00 2001 From: rcassani Date: Thu, 12 Mar 2026 16:44:43 -0400 Subject: [PATCH 03/48] Initial Brainstorm API for container-based plugins --- toolbox/core/bst_containers.m | 287 ++++++++++++++++++++++++++++++++++ 1 file changed, 287 insertions(+) create mode 100644 toolbox/core/bst_containers.m diff --git a/toolbox/core/bst_containers.m b/toolbox/core/bst_containers.m new file mode 100644 index 0000000000..516e588ccf --- /dev/null +++ b/toolbox/core/bst_containers.m @@ -0,0 +1,287 @@ +function [varargout] = bst_containers(varargin) +% BST_CONTAINERS: Manages containers for container-based plugins in Brainstorm +% +% USAGE: [isOk, eName, eStatus] = bst_containers('GetEngine') +% [isOk, imageSha] = bst_containers('PullImage', imageReference) +% [isOk, containerName] = bst_containers('RunContainer', containerName, imageReference, [volumes], [isDaemon]) +% [isOk, cmdout] = bst_containers('ExecInContainer', containerName, cmdStr) +% [isOk, cmdout] = bst_containers('StatusContainer', containerName) + +% @============================================================================= +% This function is part of the Brainstorm software: +% https://neuroimage.usc.edu/brainstorm +% +% Copyright (c) University of Southern California & McGill University +% This software is distributed under the terms of the GNU General Public License +% as published by the Free Software Foundation. Further details on the GPLv3 +% license can be found at http://www.gnu.org/copyleft/gpl.html. +% +% FOR RESEARCH PURPOSES ONLY. THE SOFTWARE IS PROVIDED "AS IS," AND THE +% UNIVERSITY OF SOUTHERN CALIFORNIA AND ITS COLLABORATORS DO NOT MAKE ANY +% WARRANTY, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF +% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, NOR DO THEY ASSUME ANY +% LIABILITY OR RESPONSIBILITY FOR THE USE OF THIS SOFTWARE. +% +% For more information type "brainstorm license" at command prompt. +% =============================================================================@ +% +% Authors: Raymundo Cassani, 2026 +% Takfarinas Medani, 2026 + + +eval(macro_method); +end + + +%% ===== GET CONTAINER ENGINE ===== +function [isFound, engineName, errMsg] = GetEngine(engineName) +% USAGE: [isFound, engineName, errMsg] = bst_containers('GetEngine') % Find, test and set a supported container engine +% [isFound, engineName, errMsg] = bst_containers('GetEngine', engineName) % Test the requested container engine + isFound = 0; + errMsg = ''; + + % Get and test all the supported container engines + if nargin < 1 || isempty(engineName) || strcmpi(engineName, 'auto-detect') + [~, engineNames] = bst_get('ContainerEngine'); + % Remove 'auto-detect', first element in engineNames + engineNames(1) = []; + engineName = ''; + isSetDefault = 1; + % Test only the requested container engine + else + engineNames = {engineName}; + isSetDefault = 0; + end + + % Tests container engines + for iEngine = 1 : length(engineNames) + if ispc + + else + [status, cmdout] = system(['which ' engineNames{iEngine}]); + % Container engine was found + if status == 0 + isFound = 1; + enginePath = strtrim(cmdout); + end + end + if isFound + engineName = engineNames{iEngine}; + fprintf('Container engine "%s" found in "%s"\r', engineName, enginePath); + break + end + end + % Return if not found + if ~isFound + if isempty(engineName) + errMsg = 'No valid container engine was found'; + else + errMsg = ['Container engine ' engineName ' was not found']; + end + return + end + + % Set as default the container engine found + if isSetDefault + bst_set('ContainerEngine', engineName); + end + + % Check the container engine status + switch(engineName) + case 'docker' + [status, cmdout] = system([engineName ' info']); + cmdout = strtrim(cmdout); + if status == 1 || ~isempty(strfind(lower(cmdout), 'failed')) || ~isempty(strfind(lower(cmdout), 'ERROR')) + errMsg = cmdout; + return + end + + case 'podman' + [status, cmdout] = system([engineName ' info', '-echo']); + if status == 1 + errMsg = cmdout; + return + end + + otherwise + + end +end + + +%% ===== PULL IMAGE REFERENCE AND ITS SHA ===== +function [isOk, imageSha] = PullImage(imageReference) +% USAGE: [isOk, imageSha] = bst_containers('PullImage', imageReference) + isOk = 0; + imageSha = ''; + + % Default container engine + engineName = bst_get('ContainerEngine'); + % Check status of container engine + [isFound, engineName, errMsg] = GetEngine(engineName); + if ~isFound || ~isempty(errMsg) + disp(errMsg) + return + end + % Pull image + switch engineName + case 'docker' + [status, cmdout] = system(['docker pull ' imageReference]); + + end + isOk = status == 0; + if ~isOk + return + end + % Get image SHA + switch engineName + case 'docker' + [status, cmdout] = system(['docker inspect ' imageReference ' --format "{{.Id}}"']); + imageSha = strtrim(cmdout); + end + isOk = status == 0; + if ~isOk + imageSha = ''; + return + end +end + + +%% ===== RUN CONTAINER AS DAEMON ===== +function [isOk, containerName] = RunContainer(containerName, imageReference, volumes, isDaemon) +% USAGE: [isOk, imageSha] = bst_containers('RunDaemonContainer', imageReference, volumes) + isOk = 0; + + % Validate inputs + if nargin < 4 || isempty(isDaemon) + isDaemon = 0; + end + if nargin < 3 || ~iscell(volumes) || size(volumes,2) ~=2 + volumes = []; + end + + % Default container engine + engineName = bst_get('ContainerEngine'); + % Check status of container engine + [isFound, engineName, errMsg] = GetEngine(engineName); + if ~isFound || ~isempty(errMsg) + disp(errMsg) + return + end + + % Create volumes pairs + volumesStr = ''; + if ~isempty(volumes) + nPairs = size(volumes, 1); + pairs = cell(nPairs, 1); + for iPair = 1 : nPairs + pairs{iPair} = ['-v' volumes{1} ':' volumes{2}]; + end + volumesStr = strjoin(pairs, ' '); + end + + % Run container + switch engineName + case 'docker' + cmdStr = ['docker run -d --name ' containerName]; + if ~isDaemon + cmdStr = sprintf('docker run --rm --name %s %s %s', containerName, volumesStr, imageReference); + else + % Replace ENTRYPOINT (if any) with `sleep infinity` + cmdStr = sprintf('docker run -d --name %s %s --entrypoint sleep %s infinity', containerName, volumesStr, imageReference); + end + [status, cmdout] = system(cmdStr); + end +end + + +%% ===== EXECUTE COMMAND IN CONTAINER ===== +function [isOk, cmdout] = ExecInContainer(containerName, cmdStr) + isOk = 0; + cmdout = ''; + + % Default container engine + engineName = bst_get('ContainerEngine'); + % Check status of container engine + [isFound, engineName, errMsg] = GetEngine(engineName); + if ~isFound || ~isempty(errMsg) + disp(errMsg) + return + end + + % Check container status + [isExist, isRunning] = StatusContainer(containerName); + if ~isExist || ~isRunning + return + end + + % Run command + switch engineName + case 'docker' + [status, cmdout] = system(['docker exec ' containerName ' sh -c ' '''' cmdStr '''']); + isOk = status == 0; + cmdout = strtrim(cmdout); + end +end + + +%% ===== CHECK CONTAINER STATUS ===== +function [isExist, isRunning] = StatusContainer(containerName) + isExist = 0; + isRunning = 0; + + % Default container engine + engineName = bst_get('ContainerEngine'); + % Check status of container engine + [isFound, engineName, errMsg] = GetEngine(engineName); + if ~isFound || ~isempty(errMsg) + disp(errMsg) + return + end + + % Search for existent container with the same name and image reference + switch engineName + case 'docker' + % Find containers with same name + [status, cmdout] = system(['docker inspect ' containerName ' --format "{{.Name}}"']); + if status == 0 + isExist = 1; + [status, cmdout] = system(['docker inspect ' containerName ' --format "{{.State.Status}}"']); + isRunning = strcmpi('running', strtrim(cmdout)); + end + end +end + + +%% ===== STOP CONTAINER ===== +function isOk = StopContainer(containerName, isForce) + isOk = 0; + + % Validate inputs + if nargin < 2 || isempty(isForce) + isForce = 0; + end + + % Default container engine + engineName = bst_get('ContainerEngine'); + % Check status of container engine + [isFound, engineName, errMsg] = GetEngine(engineName); + if ~isFound || ~isempty(errMsg) + disp(errMsg) + return + end + + % Stop container + switch engineName + case 'docker' + if ~isForce + % Stop and remove + [status, cmdout] = system(['docker stop ' containerName ' && docker rm ' containerName]); + else + % Kill + [status, cmdout] = system(['docker rm -f ' containerName]); + end + isOk = status == 0; + end + +end \ No newline at end of file From 0290f3c925c29007060bd6c4e50e3c12b0567515 Mon Sep 17 00:00:00 2001 From: rcassani Date: Fri, 20 Mar 2026 14:11:46 -0400 Subject: [PATCH 04/48] Revert "Add `ImageReference` field to `PlugDes` for container-based plugins" This reverts commit b658e248086eec61e6709203b9f841377d5c2150. --- toolbox/db/db_template.m | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/toolbox/db/db_template.m b/toolbox/db/db_template.m index df2e661e92..d5ac6a20e7 100644 --- a/toolbox/db/db_template.m +++ b/toolbox/db/db_template.m @@ -1226,8 +1226,7 @@ 'Path', [], ... % Set at runtime: Installation path for this plugin 'Processes', [], ... % List of process functions to be added to the pipeline manager 'isLoaded', 0, ... % Set at runtime: 0=Not loaded, 1=Loaded (folder and specific subfolders added to Matlab path) - 'isManaged', 0, ... % Set at runtime: 0=Installed by the user, 1=Installed automatically by Brainstorm - 'ImageReference',''); % String to that uniquely identifies a container (to be used by plugin) image from a registry + 'isManaged', 0); % Set at runtime: 0=Installed by the user, 1=Installed automatically by Brainstorm template.LoadFolders = {}; template.UnloadPlugs = {}; template.RequiredPlugs = {}; From 73c5b6f53a01c2188257c95159029c7cb3c269f8 Mon Sep 17 00:00:00 2001 From: rcassani Date: Fri, 20 Mar 2026 17:15:02 -0400 Subject: [PATCH 05/48] Add `ImageSource` and `ImageSha` fields for container `PlugDesc` --- toolbox/db/db_template.m | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/toolbox/db/db_template.m b/toolbox/db/db_template.m index d5ac6a20e7..ab9d933f78 100644 --- a/toolbox/db/db_template.m +++ b/toolbox/db/db_template.m @@ -1220,13 +1220,15 @@ 'UnloadedFcn', [], ... % String to eval or function handle to call after unloading the plugin 'DeleteFiles', [], ... % Cell-array of files to delete after unzipping the plugin package (path relative to the plugin folder) 'DeleteFilesBin',[], ... % Cell-array of files to delete before compiling Brainstorm, to avoid including them in the binary distribution (path relative to the plugin folder) + 'ImageSource', '', ... % String with location of container image: registry reference, path or download URL ... % Set when installing or loading the plugin 'InstallDate', '', ... % Installation date 'SubFolder', '', ... % If all the code is in a subfolder: detect this at installation time 'Path', [], ... % Set at runtime: Installation path for this plugin 'Processes', [], ... % List of process functions to be added to the pipeline manager 'isLoaded', 0, ... % Set at runtime: 0=Not loaded, 1=Loaded (folder and specific subfolders added to Matlab path) - 'isManaged', 0); % Set at runtime: 0=Installed by the user, 1=Installed automatically by Brainstorm + 'isManaged', 0, ... % Set at runtime: 0=Installed by the user, 1=Installed automatically by Brainstorm + 'ImageSha', ''); % String with SHA for container ImageSource template.LoadFolders = {}; template.UnloadPlugs = {}; template.RequiredPlugs = {}; From 7086f7ebf29e0c3e46a64c27e68c0d34aece9056 Mon Sep 17 00:00:00 2001 From: rcassani Date: Fri, 20 Mar 2026 17:32:23 -0400 Subject: [PATCH 06/48] Get image SHA when importing image; and use it to run container --- toolbox/core/bst_containers.m | 53 ++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/toolbox/core/bst_containers.m b/toolbox/core/bst_containers.m index 516e588ccf..8e262bb506 100644 --- a/toolbox/core/bst_containers.m +++ b/toolbox/core/bst_containers.m @@ -2,7 +2,7 @@ % BST_CONTAINERS: Manages containers for container-based plugins in Brainstorm % % USAGE: [isOk, eName, eStatus] = bst_containers('GetEngine') -% [isOk, imageSha] = bst_containers('PullImage', imageReference) +% [isOk, imageSha] = bst_containers('ImportImage', imageSource) % [isOk, containerName] = bst_containers('RunContainer', containerName, imageReference, [volumes], [isDaemon]) % [isOk, cmdout] = bst_containers('ExecInContainer', containerName, cmdStr) % [isOk, cmdout] = bst_containers('StatusContainer', containerName) @@ -109,9 +109,10 @@ end -%% ===== PULL IMAGE REFERENCE AND ITS SHA ===== -function [isOk, imageSha] = PullImage(imageReference) -% USAGE: [isOk, imageSha] = bst_containers('PullImage', imageReference) +%% ===== IMPORT IMAGE ===== +function [isOk, imageSha] = ImportImage(imageSource) +% Load container image into container engine +% USAGE: [isOk, imageSha] = bst_containers('ImportImage', imageSource) isOk = 0; imageSha = ''; @@ -123,33 +124,41 @@ disp(errMsg) return end - % Pull image - switch engineName - case 'docker' - [status, cmdout] = system(['docker pull ' imageReference]); + % [TODO] Check imageSource is: reference, local file or download URL + imageType = 'reference'; + + % [TODO] Get image from download link + if strcmpi(imageType, 'url') + % Download file in tmp + % Update imageSource + % Change type to file end - isOk = status == 0; - if ~isOk - return - end - % Get image SHA + + % Import image switch engineName case 'docker' - [status, cmdout] = system(['docker inspect ' imageReference ' --format "{{.Id}}"']); - imageSha = strtrim(cmdout); + switch imageType + case 'reference' + [status, cmdout] = system(['docker pull ' imageSource]); + case 'file' + [status, cmdout] = system(['docker load --input ' imageSource]); + end + if status == 0 + tokens = regexp(cmdout, 'sha256:[a-f0-9]+', 'match'); + imageSha = strtrim(tokens{1}); + end end isOk = status == 0; if ~isOk - imageSha = ''; return end end %% ===== RUN CONTAINER AS DAEMON ===== -function [isOk, containerName] = RunContainer(containerName, imageReference, volumes, isDaemon) -% USAGE: [isOk, imageSha] = bst_containers('RunDaemonContainer', imageReference, volumes) +function [isOk, containerName] = RunContainer(containerName, imageSha, volumes, isDaemon) +% USAGE: [isOk, imageSha] = bst_containers('RunDaemonContainer', imageSha, volumes) isOk = 0; % Validate inputs @@ -185,13 +194,17 @@ case 'docker' cmdStr = ['docker run -d --name ' containerName]; if ~isDaemon - cmdStr = sprintf('docker run --rm --name %s %s %s', containerName, volumesStr, imageReference); + cmdStr = sprintf('docker run --rm --name %s %s %s', containerName, volumesStr, imageSha); else % Replace ENTRYPOINT (if any) with `sleep infinity` - cmdStr = sprintf('docker run -d --name %s %s --entrypoint sleep %s infinity', containerName, volumesStr, imageReference); + cmdStr = sprintf('docker run -d --name %s %s --entrypoint sleep %s infinity', containerName, volumesStr, imageSha); end [status, cmdout] = system(cmdStr); end + isOk = status == 0; + if ~isOk + return + end end From 8fe96f448a608d3182e307e17b05dada2b2350d1 Mon Sep 17 00:00:00 2001 From: rcassani Date: Fri, 20 Mar 2026 21:32:01 -0400 Subject: [PATCH 07/48] Plugin: Allow install of container plugins --- toolbox/core/bst_plugin.m | 124 +++++++++++++++++++++++--------------- 1 file changed, 76 insertions(+), 48 deletions(-) diff --git a/toolbox/core/bst_plugin.m b/toolbox/core/bst_plugin.m index 68184f8345..812ff3b5e2 100644 --- a/toolbox/core/bst_plugin.m +++ b/toolbox/core/bst_plugin.m @@ -1684,8 +1684,10 @@ function Configure(PlugDesc) PlugDesc = []; return; end + % Check if plugin is a container + isContainer = IsContainer(PlugDesc); % Check if there is a URL to download - if isempty(PlugDesc.URLzip) + if isempty(PlugDesc.URLzip) && ~isContainer errMsg = ['No download URL for ', OsType, ': ', PlugName '']; return; end @@ -1869,55 +1871,66 @@ function Configure(PlugDesc) if ~isempty(LogoFile) bst_progress('setimage', LogoFile); end - % Get package file format - if strcmpi(PlugDesc.URLzip(end-3:end), '.zip') - pkgFormat = 'zip'; - elseif strcmpi(PlugDesc.URLzip(end-6:end), '.tar.gz') || strcmpi(PlugDesc.URLzip(end-3:end), '.tgz') - pkgFormat = 'tgz'; + % Code plugins + if ~isContainer + % Get package file format + if strcmpi(PlugDesc.URLzip(end-3:end), '.zip') + pkgFormat = 'zip'; + elseif strcmpi(PlugDesc.URLzip(end-6:end), '.tar.gz') || strcmpi(PlugDesc.URLzip(end-3:end), '.tgz') + pkgFormat = 'tgz'; + else + disp('BST> Could not guess file format, trying ZIP...'); + pkgFormat = 'zip'; + end + % Download file + pkgFile = bst_fullfile(PlugPath, ['plugin.' pkgFormat]); + disp(['BST> Downloading URL : ' PlugDesc.URLzip]); + disp(['BST> Saving to file : ' pkgFile]); + errMsg = gui_brainstorm('DownloadFile', PlugDesc.URLzip, pkgFile, ['Download plugin: ' PlugName], LogoFile); + % If file was not downloaded correctly + if ~isempty(errMsg) + errMsg = ['Impossible to download ' PlugName ' automatically:' 10 errMsg]; + if ~isCompiled + errMsg = [errMsg 10 10 ... + 'Alternative download solution:' 10 ... + '1) Copy the URL below from the Matlab command window: ' 10 ... + ' ' PlugDesc.URLzip 10 ... + '2) Paste it in a web browser' 10 ... + '3) Save the file and unzip it' 10 ... + '4) Add to the Matlab path the folder containing ' PlugDesc.TestFile '.']; + end + bst_progress('removeimage'); + return; + end + % Update progress bar + bst_progress('text', ['Installing plugin: ' PlugName '...']); + if ~isempty(LogoFile) + bst_progress('setimage', LogoFile); + end + % Unzip file + switch (pkgFormat) + case 'zip' + bst_unzip(pkgFile, PlugPath); + case 'tgz' + if ispc + untar(pkgFile, PlugPath); + else + curdir = pwd; + cd(PlugPath); + system(['tar -xf ' pkgFile]); + cd(curdir); + end + end + file_delete(pkgFile, 1, 3); else - disp('BST> Could not guess file format, trying ZIP...'); - pkgFormat = 'zip'; - end - % Download file - pkgFile = bst_fullfile(PlugPath, ['plugin.' pkgFormat]); - disp(['BST> Downloading URL : ' PlugDesc.URLzip]); - disp(['BST> Saving to file : ' pkgFile]); - errMsg = gui_brainstorm('DownloadFile', PlugDesc.URLzip, pkgFile, ['Download plugin: ' PlugName], LogoFile); - % If file was not downloaded correctly - if ~isempty(errMsg) - errMsg = ['Impossible to download ' PlugName ' automatically:' 10 errMsg]; - if ~isCompiled - errMsg = [errMsg 10 10 ... - 'Alternative download solution:' 10 ... - '1) Copy the URL below from the Matlab command window: ' 10 ... - ' ' PlugDesc.URLzip 10 ... - '2) Paste it in a web browser' 10 ... - '3) Save the file and unzip it' 10 ... - '4) Add to the Matlab path the folder containing ' PlugDesc.TestFile '.']; + % Import container image in container engine + [isOk, errMsg, imageSha] = bst_containers('ImportImage', PlugDesc.ImageSource); + if ~isOk + bst_progress('removeimage'); + return end - bst_progress('removeimage'); - return; + PlugDesc.ImageSha = imageSha; end - % Update progress bar - bst_progress('text', ['Installing plugin: ' PlugName '...']); - if ~isempty(LogoFile) - bst_progress('setimage', LogoFile); - end - % Unzip file - switch (pkgFormat) - case 'zip' - bst_unzip(pkgFile, PlugPath); - case 'tgz' - if ispc - untar(pkgFile, PlugPath); - else - curdir = pwd; - cd(PlugPath); - system(['tar -xf ' pkgFile]); - cd(curdir); - end - end - file_delete(pkgFile, 1, 3); % === SAVE PLUGIN.MAT === PlugDesc.Path = PlugPath; @@ -3044,6 +3057,7 @@ function MenuUpdate(jMenu, fontSize) end isLoaded = isInstalled && Plug.isLoaded; isManaged = isInstalled && Plug.isManaged; + isContainer = IsContainer(Plug); % Compiled included: no submenus if isCompiled && (PlugRef.CompiledStatus == 2) j.menu.setEnabled(1); @@ -3055,7 +3069,7 @@ function MenuUpdate(jMenu, fontSize) % Otherwise: all available else % Main menu: Available/Not available - j.menu.setEnabled(isInstalled || ~isempty(Plug.URLzip)); + j.menu.setEnabled(isInstalled || ~isempty(Plug.URLzip) || isContainer); % Current version if ~isInstalled j.version.setText('Not installed'); @@ -3469,6 +3483,20 @@ function SetProgressLogo(PlugDesc) pluginNames = { 'duneuro', 'mcxlab-cuda'}; end + +%% ===== IS CONTAINER PLUGIN ===== +% Check if plugin is a container +function isContainer = IsContainer(PlugDesc) + isContainer = 0; + if ischar(PlugDesc) + PlugDesc = GetDescription(PlugDesc); + end + if isempty(PlugDesc.URLzip) && ~isempty(PlugDesc.ImageSource) + isContainer = 1; + end +end + + %% ===== MATCH STRING EDGES ===== % Check if a string 'strA' starts (or ends) with string B function result = strMatchEdge(a, b, edge) From 622dcde63e06451c07c500c56c28452160b9c18f Mon Sep 17 00:00:00 2001 From: rcassani Date: Fri, 20 Mar 2026 21:32:16 -0400 Subject: [PATCH 08/48] Return errMsg in bst_containers --- toolbox/core/bst_containers.m | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/toolbox/core/bst_containers.m b/toolbox/core/bst_containers.m index 8e262bb506..fad859fca5 100644 --- a/toolbox/core/bst_containers.m +++ b/toolbox/core/bst_containers.m @@ -2,8 +2,8 @@ % BST_CONTAINERS: Manages containers for container-based plugins in Brainstorm % % USAGE: [isOk, eName, eStatus] = bst_containers('GetEngine') -% [isOk, imageSha] = bst_containers('ImportImage', imageSource) -% [isOk, containerName] = bst_containers('RunContainer', containerName, imageReference, [volumes], [isDaemon]) +% [isOk, errMsg, imageSha] = bst_containers('ImportImage', imageSource) +% [isOk, errMsg. containerName] = bst_containers('RunContainer', containerName, imageSha, [volumes], [isDaemon]) % [isOk, cmdout] = bst_containers('ExecInContainer', containerName, cmdStr) % [isOk, cmdout] = bst_containers('StatusContainer', containerName) @@ -110,10 +110,11 @@ %% ===== IMPORT IMAGE ===== -function [isOk, imageSha] = ImportImage(imageSource) +function [isOk, errMsg, imageSha] = ImportImage(imageSource) % Load container image into container engine -% USAGE: [isOk, imageSha] = bst_containers('ImportImage', imageSource) +% USAGE: [isOk, errMsg, imageSha] = bst_containers('ImportImage', imageSource) isOk = 0; + errMsg = ''; imageSha = ''; % Default container engine @@ -121,7 +122,6 @@ % Check status of container engine [isFound, engineName, errMsg] = GetEngine(engineName); if ~isFound || ~isempty(errMsg) - disp(errMsg) return end @@ -157,8 +157,8 @@ %% ===== RUN CONTAINER AS DAEMON ===== -function [isOk, containerName] = RunContainer(containerName, imageSha, volumes, isDaemon) -% USAGE: [isOk, imageSha] = bst_containers('RunDaemonContainer', imageSha, volumes) +function [isOk, errMsg, containerName] = RunContainer(containerName, imageSha, volumes, isDaemon) +% USAGE: [isOk, errMsg, imageSha] = bst_containers('RunDaemonContainer', imageSha, volumes) isOk = 0; % Validate inputs @@ -174,7 +174,6 @@ % Check status of container engine [isFound, engineName, errMsg] = GetEngine(engineName); if ~isFound || ~isempty(errMsg) - disp(errMsg) return end From fb249a015881796ec8773a9a82e853e47bfcfde4 Mon Sep 17 00:00:00 2001 From: rcassani Date: Fri, 20 Mar 2026 22:09:31 -0400 Subject: [PATCH 09/48] Remove image from container engine --- toolbox/core/bst_containers.m | 45 +++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/toolbox/core/bst_containers.m b/toolbox/core/bst_containers.m index fad859fca5..8f1e3e3955 100644 --- a/toolbox/core/bst_containers.m +++ b/toolbox/core/bst_containers.m @@ -296,4 +296,49 @@ isOk = status == 0; end +end + + +%% ===== REMOVE IMAGE ===== +function isOk = RemoveImage(imageSha, isForce) + isOk = 0; + + % Validate inputs + if nargin < 2 || isempty(isForce) + isForce = 0; + end + + isOk = 0; + errMsg = ''; + imageSha = ''; + + % Default container engine + engineName = bst_get('ContainerEngine'); + % Check status of container engine + [isFound, engineName, errMsg] = GetEngine(engineName); + if ~isFound || ~isempty(errMsg) + return + end + + % Default container engine + engineName = bst_get('ContainerEngine'); + % Check status of container engine + [isFound, engineName, errMsg] = GetEngine(engineName); + if ~isFound || ~isempty(errMsg) + disp(errMsg) + return + end + + % Stop container + switch engineName + case 'docker' + if ~isForce + % Remove image + [status, cmdout] = system(['docker rmi ' imageSha]); + else + % Force remove image + [status, cmdout] = system(['docker rmi -f ' imageSha]); + end + isOk = status == 0; + end end \ No newline at end of file From 03658d1ec23e1acefccce0d534dbb1211a8bdaa6 Mon Sep 17 00:00:00 2001 From: rcassani Date: Fri, 20 Mar 2026 22:10:02 -0400 Subject: [PATCH 10/48] Load container plugin --- toolbox/core/bst_plugin.m | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/toolbox/core/bst_plugin.m b/toolbox/core/bst_plugin.m index 812ff3b5e2..19e4e6cda8 100644 --- a/toolbox/core/bst_plugin.m +++ b/toolbox/core/bst_plugin.m @@ -2347,6 +2347,8 @@ function Configure(PlugDesc) errMsg = ['Plugin ', PlugDesc.Name ' is not supported on Apple silicon yet.']; return; end + % Check if plugin is a container + isContainer = IsContainer(PlugDesc); % Minimum Matlab version if ~isempty(PlugDesc.MinMatlabVer) && (PlugDesc.MinMatlabVer > 0) && (bst_get('MatlabVersion') < PlugDesc.MinMatlabVer) strMinVer = sprintf('%d.%d', ceil(PlugDesc.MinMatlabVer / 100), mod(PlugDesc.MinMatlabVer, 100)); @@ -2470,6 +2472,16 @@ function Configure(PlugDesc) else PlugHomeDir = PlugPath; end + % Run container if image was properly imported + if isContainer && ~isempty(PlugDesc.ImageSha) + % Get tmp dir to bind container + TmpDir = bst_get('BrainstormTmpDir', 0, PlugDesc.Name); + volumes = {TmpDir, '/data'}; + [isOk, errMsg] = bst_containers('RunContainer', PlugDesc.Name, PlugDesc.ImageSha, volumes, 1); + if ~isOk + return + end + end % Do not modify path in compiled mode isCompiled = bst_iscompiled(); if ~isCompiled From 3494c2ab96bfd951f17c52639d7f47c7f58e2ff2 Mon Sep 17 00:00:00 2001 From: rcassani Date: Fri, 20 Mar 2026 22:10:18 -0400 Subject: [PATCH 11/48] Unload container plugin --- toolbox/core/bst_plugin.m | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/toolbox/core/bst_plugin.m b/toolbox/core/bst_plugin.m index 19e4e6cda8..27f9da71e3 100644 --- a/toolbox/core/bst_plugin.m +++ b/toolbox/core/bst_plugin.m @@ -2607,6 +2607,8 @@ function Configure(PlugDesc) if ~isempty(errMsg) return; end + % Check if plugin is a container + isContainer = IsContainer(PlugDesc); % === PROCESS DEPENDENCIES === % Unload dependent plugins @@ -2631,6 +2633,13 @@ function Configure(PlugDesc) end end end + % Stop container + if isContainer + [isOk] = bst_containers('StopContainer', PlugDesc.Name, 1); + if ~isOk + return + end + end end % === TEST FUNCTION === From bc84516719ccf5256b94552c0396c188f50b6fcf Mon Sep 17 00:00:00 2001 From: rcassani Date: Fri, 20 Mar 2026 22:10:30 -0400 Subject: [PATCH 12/48] Uninstall container plugin --- toolbox/core/bst_plugin.m | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/toolbox/core/bst_plugin.m b/toolbox/core/bst_plugin.m index 27f9da71e3..ad01e3b657 100644 --- a/toolbox/core/bst_plugin.m +++ b/toolbox/core/bst_plugin.m @@ -2175,6 +2175,8 @@ function Configure(PlugDesc) errMsg = ['Plugin ' PlugName ' is not installed.']; return; end + % Check if plugin is a container + isContainer = IsContainer(PlugDesc); % === USER CONFIRMATION === if isInteractive @@ -2222,6 +2224,14 @@ function Configure(PlugDesc) end quit('force'); end + % Remove image from container engine + if isContainer && ~isempty(PlugDesc.ImageSha) + [isOk] = bst_containers('StopContainer', PlugDesc.Name, 1); + if ~isOk + return + end + end + % === CALLBACK: POST-UNINSTALL === [isOk, errMsg] = ExecuteCallback(PlugDesc, 'UninstalledFcn'); From 245547dc59c452094e9730ac08d7ba466819e638 Mon Sep 17 00:00:00 2001 From: rcassani Date: Mon, 23 Mar 2026 23:16:22 -0400 Subject: [PATCH 13/48] Get ImageSha befor running container --- toolbox/core/bst_plugin.m | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/toolbox/core/bst_plugin.m b/toolbox/core/bst_plugin.m index ad01e3b657..44199b064c 100644 --- a/toolbox/core/bst_plugin.m +++ b/toolbox/core/bst_plugin.m @@ -2483,13 +2483,16 @@ function Configure(PlugDesc) PlugHomeDir = PlugPath; end % Run container if image was properly imported - if isContainer && ~isempty(PlugDesc.ImageSha) - % Get tmp dir to bind container - TmpDir = bst_get('BrainstormTmpDir', 0, PlugDesc.Name); - volumes = {TmpDir, '/data'}; - [isOk, errMsg] = bst_containers('RunContainer', PlugDesc.Name, PlugDesc.ImageSha, volumes, 1); - if ~isOk - return + if isContainer + PlugDesc = GetInstalled(PlugDesc); + if ~isempty(PlugDesc.ImageSha) + % Get tmp dir to bind container + TmpDir = bst_get('BrainstormTmpDir', 0, PlugDesc.Name); + volumes = {TmpDir, '/data'}; + [isOk, errMsg] = bst_containers('RunContainer', PlugDesc.Name, PlugDesc.ImageSha, volumes, 1); + if ~isOk + return + end end end % Do not modify path in compiled mode From da76a1e2774536013d2a03118eeba1e8bf962c4c Mon Sep 17 00:00:00 2001 From: rcassani Date: Mon, 23 Mar 2026 23:17:52 -0400 Subject: [PATCH 14/48] GUI: Menu items for container plugins --- toolbox/core/bst_plugin.m | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/toolbox/core/bst_plugin.m b/toolbox/core/bst_plugin.m index 44199b064c..30b6cdde74 100644 --- a/toolbox/core/bst_plugin.m +++ b/toolbox/core/bst_plugin.m @@ -2940,6 +2940,8 @@ function Configure(PlugDesc) if isCompiled && (Plug.CompiledStatus == 0) continue; end + % Check if plugin is a container + isContainer = IsContainer(Plug); % === Add menus for each plugin === % One menu per plugin ij = length(j) + 1; @@ -2972,16 +2974,27 @@ function Configure(PlugDesc) % Main menu j(ij).menu = gui_component('Menu', jParent, [], Plug.Name, [], [], [], fontSize); % Version - j(ij).version = gui_component('MenuItem', j(ij).menu, [], 'Version', [], [], [], fontSize); + iconVersion = []; + if isContainer + iconVersion = IconLoader.ICON_OBJECT; + end + j(ij).version = gui_component('MenuItem', j(ij).menu, [], 'Version', iconVersion, [], [], fontSize); j(ij).versep = java_create('javax.swing.JSeparator'); j(ij).menu.add(j(ij).versep); % Install j(ij).install = gui_component('MenuItem', j(ij).menu, [], 'Install', IconLoader.ICON_DOWNLOAD, [], @(h,ev)InstallInteractive(Plug.Name), fontSize); + if isContainer + j(ij).install.setText('Import image'); + end % Update j(ij).update = gui_component('MenuItem', j(ij).menu, [], 'Update', IconLoader.ICON_RELOAD, [], @(h,ev)UpdateInteractive(Plug.Name), fontSize); + j(ij).update.setVisible(~isContainer); % Uninstall j(ij).uninstall = gui_component('MenuItem', j(ij).menu, [], 'Uninstall', IconLoader.ICON_DELETE, [], @(h,ev)UninstallInteractive(Plug.Name), fontSize); j(ij).menu.addSeparator(); + if isContainer + j(ij).install.setText('Remove image'); + end % Custom install j(ij).custom = gui_component('Menu', j(ij).menu, [], 'Custom install', IconLoader.ICON_FOLDER_OPEN, [], [], fontSize); j(ij).customset = gui_component('MenuItem', j(ij).custom, [], 'Select installation folder', [], [], @(h,ev)SetCustomPath(Plug.Name), fontSize); @@ -2989,10 +3002,17 @@ function Configure(PlugDesc) j(ij).custompath.setEnabled(0); j(ij).custom.addSeparator(); j(ij).customdel = gui_component('MenuItem', j(ij).custom, [], 'Ignore local installation', [], [], @(h,ev)SetCustomPath(Plug.Name, 0), fontSize); - j(ij).menu.addSeparator(); + j(ij).custom.setVisible(~isContainer); + if ~isContainer + j(ij).menu.addSeparator(); + end % Load j(ij).load = gui_component('MenuItem', j(ij).menu, [], 'Load', IconLoader.ICON_GOOD, [], @(h,ev)LoadInteractive(Plug.Name), fontSize); j(ij).unload = gui_component('MenuItem', j(ij).menu, [], 'Unload', IconLoader.ICON_BAD, [], @(h,ev)UnloadInteractive(Plug.Name), fontSize); + if isContainer + j(ij).load.setText('Run container (as daemon)'); + j(ij).unload.setText('Stop container'); + end j(ij).menu.addSeparator(); % Website j(ij).web = gui_component('MenuItem', j(ij).menu, [], 'Website', IconLoader.ICON_EXPLORER, [], @(h,ev)web(Plug.URLinfo, '-browser'), fontSize); @@ -3142,10 +3162,14 @@ function MenuUpdate(jMenu, fontSize) end % Install j.install.setEnabled(~isInstalled); + InstallText = 'Install'; + if isContainer + InstallText = 'Import image'; + end if ~isInstalled && ~isempty(PlugRef.Version) && ischar(PlugRef.Version) - j.install.setText(['Install    (' PlugRef.Version ')']) + j.install.setText(['' InstallText '    (' PlugRef.Version ')']) else - j.install.setText('Install'); + j.install.setText(InstallText); end % Update j.update.setEnabled(isManaged); From 9bd765e8254387374947ef32e2bc83f854170432 Mon Sep 17 00:00:00 2001 From: rcassani Date: Tue, 24 Mar 2026 12:02:18 -0400 Subject: [PATCH 15/48] Find container engine in Windows --- toolbox/core/bst_containers.m | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/toolbox/core/bst_containers.m b/toolbox/core/bst_containers.m index 8f1e3e3955..848278215c 100644 --- a/toolbox/core/bst_containers.m +++ b/toolbox/core/bst_containers.m @@ -56,10 +56,16 @@ % Tests container engines for iEngine = 1 : length(engineNames) if ispc - + [status, cmdout] = system(['where ' engineNames{iEngine}]); + if status == 0 + cmdout = strsplit(strtrim(cmdout), '\n'); + if length(cmdout) >= 1 + isFound = 1; + enginePath = strtrim(cmdout{1}); + end + end else [status, cmdout] = system(['which ' engineNames{iEngine}]); - % Container engine was found if status == 0 isFound = 1; enginePath = strtrim(cmdout); From f7bd83eab6934dccf2a8af6e980ca62e1f2e8c5d Mon Sep 17 00:00:00 2001 From: rcassani Date: Tue, 24 Mar 2026 13:13:48 -0400 Subject: [PATCH 16/48] Rename StatusContainer to GetContainerInfo --- toolbox/core/bst_containers.m | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/toolbox/core/bst_containers.m b/toolbox/core/bst_containers.m index 848278215c..1313056057 100644 --- a/toolbox/core/bst_containers.m +++ b/toolbox/core/bst_containers.m @@ -228,8 +228,8 @@ end % Check container status - [isExist, isRunning] = StatusContainer(containerName); - if ~isExist || ~isRunning + [containerName, isRunning] = GetContainerInfo(containerName); + if isempty(containerName) || ~isRunning return end @@ -244,8 +244,8 @@ %% ===== CHECK CONTAINER STATUS ===== -function [isExist, isRunning] = StatusContainer(containerName) - isExist = 0; +function [containerNameOut, isRunning, volumePairs, imageSha] = GetContainerInfo(containerName) + containerNameOut = []; isRunning = 0; % Default container engine @@ -264,8 +264,18 @@ [status, cmdout] = system(['docker inspect ' containerName ' --format "{{.Name}}"']); if status == 0 isExist = 1; - [status, cmdout] = system(['docker inspect ' containerName ' --format "{{.State.Status}}"']); - isRunning = strcmpi('running', strtrim(cmdout)); + containerNameOut = strrep(strtrim(cmdout), '/', ''); + [status, cmdout] = system(['docker inspect ' containerName ' --format "'... + '{{.State.Status}} # ' ... + '{{.HostConfig.Binds}} # ' ... + '{{.Image}}"']); + cmdout = strsplit(strtrim(cmdout), '#'); + isRunning = strcmpi('running', strtrim(cmdout{1})); + volumes = regexprep(strtrim(cmdout{2}), '^\[|\]$', ''); + volumePairs = strsplit(volumes, ':'); + volumePairs = reshape(volumePairs, 2, [])'; + tokens = regexp(cmdout{3}, 'sha256:[a-f0-9]+', 'match'); + imageSha = strtrim(tokens{1}); end end end From 2132cd092e698b9b11d355f40bdffd759dcfd022 Mon Sep 17 00:00:00 2001 From: rcassani Date: Tue, 24 Mar 2026 13:24:22 -0400 Subject: [PATCH 17/48] Do not Run (Load) container if it is already Running (Loaded) --- toolbox/core/bst_plugin.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toolbox/core/bst_plugin.m b/toolbox/core/bst_plugin.m index 30b6cdde74..4d027dc329 100644 --- a/toolbox/core/bst_plugin.m +++ b/toolbox/core/bst_plugin.m @@ -2485,7 +2485,7 @@ function Configure(PlugDesc) % Run container if image was properly imported if isContainer PlugDesc = GetInstalled(PlugDesc); - if ~isempty(PlugDesc.ImageSha) + if ~isempty(PlugDesc.ImageSha) && ~PlugDesc.isLoaded % Get tmp dir to bind container TmpDir = bst_get('BrainstormTmpDir', 0, PlugDesc.Name); volumes = {TmpDir, '/data'}; From 587a1ad4bb6e9ed1f5ebbe5829767323383fa140 Mon Sep 17 00:00:00 2001 From: rcassani Date: Tue, 24 Mar 2026 14:01:56 -0400 Subject: [PATCH 18/48] Windows: Handle colon in paths --- toolbox/core/bst_containers.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/toolbox/core/bst_containers.m b/toolbox/core/bst_containers.m index 1313056057..e998990eb1 100644 --- a/toolbox/core/bst_containers.m +++ b/toolbox/core/bst_containers.m @@ -272,7 +272,9 @@ cmdout = strsplit(strtrim(cmdout), '#'); isRunning = strcmpi('running', strtrim(cmdout{1})); volumes = regexprep(strtrim(cmdout{2}), '^\[|\]$', ''); + volumes = regexprep(volumes, ':\', ';\'); volumePairs = strsplit(volumes, ':'); + volumePairs = cellfun(@(x) regexprep(x, ';\', ':\'), volumePairs, 'UniformOutput', 0); volumePairs = reshape(volumePairs, 2, [])'; tokens = regexp(cmdout{3}, 'sha256:[a-f0-9]+', 'match'); imageSha = strtrim(tokens{1}); From 492fce1b22c6f7be95be6d37cefdf11ca52502f2 Mon Sep 17 00:00:00 2001 From: rcassani Date: Tue, 24 Mar 2026 14:02:17 -0400 Subject: [PATCH 19/48] Windows, quotes in calling `system()` --- toolbox/core/bst_containers.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toolbox/core/bst_containers.m b/toolbox/core/bst_containers.m index e998990eb1..39a50ced66 100644 --- a/toolbox/core/bst_containers.m +++ b/toolbox/core/bst_containers.m @@ -236,7 +236,7 @@ % Run command switch engineName case 'docker' - [status, cmdout] = system(['docker exec ' containerName ' sh -c ' '''' cmdStr '''']); + [status, cmdout] = system(['docker exec ' containerName ' sh -c ' '"' cmdStr '"']); isOk = status == 0; cmdout = strtrim(cmdout); end From 01b94ca2c489d91059d4f012a7ba8e37c88d7678 Mon Sep 17 00:00:00 2001 From: rcassani Date: Tue, 24 Mar 2026 14:27:59 -0400 Subject: [PATCH 20/48] Command wrappers for `ExecInContainer` depend on OS --- toolbox/core/bst_containers.m | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/toolbox/core/bst_containers.m b/toolbox/core/bst_containers.m index 39a50ced66..9ee35ef289 100644 --- a/toolbox/core/bst_containers.m +++ b/toolbox/core/bst_containers.m @@ -236,7 +236,11 @@ % Run command switch engineName case 'docker' - [status, cmdout] = system(['docker exec ' containerName ' sh -c ' '"' cmdStr '"']); + commandWrapper = ''''; % Single quote + if ispc + commandWrapper = ''''; % Double quote + end + [status, cmdout] = system(['docker exec ' containerName ' sh -c ' commandWrapper cmdStr commandWrapper]); isOk = status == 0; cmdout = strtrim(cmdout); end From c689a84ba2412cae93bf75de8de2b15f739058d5 Mon Sep 17 00:00:00 2001 From: rcassani Date: Tue, 24 Mar 2026 14:30:19 -0400 Subject: [PATCH 21/48] Delete temporary files on Unload() --- toolbox/core/bst_plugin.m | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/toolbox/core/bst_plugin.m b/toolbox/core/bst_plugin.m index 4d027dc329..526c00b2fa 100644 --- a/toolbox/core/bst_plugin.m +++ b/toolbox/core/bst_plugin.m @@ -2648,7 +2648,11 @@ function Configure(PlugDesc) end % Stop container if isContainer + % Retrieve info of container + [~, ~, volumePairs] = bst_containers('GetContainerInfo', PlugDesc.Name); [isOk] = bst_containers('StopContainer', PlugDesc.Name, 1); + % Delete temporary files + file_delete(volumePairs{1,1}, 1, 1); if ~isOk return end From 8cd4a4fe353e1759c1f1c792be4707b525120ba7 Mon Sep 17 00:00:00 2001 From: rcassani Date: Mon, 20 Apr 2026 11:25:55 -0400 Subject: [PATCH 22/48] Windows, double quotes in calling command for docker `exec` --- toolbox/core/bst_containers.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toolbox/core/bst_containers.m b/toolbox/core/bst_containers.m index 9ee35ef289..e9dbef3007 100644 --- a/toolbox/core/bst_containers.m +++ b/toolbox/core/bst_containers.m @@ -238,7 +238,7 @@ case 'docker' commandWrapper = ''''; % Single quote if ispc - commandWrapper = ''''; % Double quote + commandWrapper = '"'; % Double quote end [status, cmdout] = system(['docker exec ' containerName ' sh -c ' commandWrapper cmdStr commandWrapper]); isOk = status == 0; From ac70c452574773ada65d89a1f58477f1686d29e3 Mon Sep 17 00:00:00 2001 From: rcassani Date: Tue, 21 Apr 2026 12:18:34 -0400 Subject: [PATCH 23/48] Containers: Use tag for image --- toolbox/core/bst_plugin.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toolbox/core/bst_plugin.m b/toolbox/core/bst_plugin.m index 526c00b2fa..1c42c204b7 100644 --- a/toolbox/core/bst_plugin.m +++ b/toolbox/core/bst_plugin.m @@ -1924,7 +1924,7 @@ function Configure(PlugDesc) file_delete(pkgFile, 1, 3); else % Import container image in container engine - [isOk, errMsg, imageSha] = bst_containers('ImportImage', PlugDesc.ImageSource); + [isOk, errMsg, imageSha] = bst_containers('ImportImage', PlugDesc.ImageSource, ['BST_' PlugDesc.Name]); if ~isOk bst_progress('removeimage'); return From 416fd5ad31b3055631211ddaeb9c833b7c0fe58d Mon Sep 17 00:00:00 2001 From: rcassani Date: Tue, 21 Apr 2026 12:18:46 -0400 Subject: [PATCH 24/48] Containers: Container name --- toolbox/core/bst_plugin.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toolbox/core/bst_plugin.m b/toolbox/core/bst_plugin.m index 1c42c204b7..3f01e3e054 100644 --- a/toolbox/core/bst_plugin.m +++ b/toolbox/core/bst_plugin.m @@ -2489,7 +2489,7 @@ function Configure(PlugDesc) % Get tmp dir to bind container TmpDir = bst_get('BrainstormTmpDir', 0, PlugDesc.Name); volumes = {TmpDir, '/data'}; - [isOk, errMsg] = bst_containers('RunContainer', PlugDesc.Name, PlugDesc.ImageSha, volumes, 1); + [isOk, errMsg] = bst_containers('RunContainer', ['bst_' PlugDesc.Name], PlugDesc.ImageSha, volumes, 1); if ~isOk return end From 380f4a12e734b107e35169b0b594ae0802eb8a44 Mon Sep 17 00:00:00 2001 From: rcassani Date: Tue, 21 Apr 2026 12:19:37 -0400 Subject: [PATCH 25/48] Get list of available images --- toolbox/core/bst_containers.m | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/toolbox/core/bst_containers.m b/toolbox/core/bst_containers.m index e9dbef3007..df9c608e26 100644 --- a/toolbox/core/bst_containers.m +++ b/toolbox/core/bst_containers.m @@ -363,4 +363,30 @@ end isOk = status == 0; end +end + + +%% ===== GET AVAILABLE IMAGES ===== +function [errMsg, imageList] = GetImages() +% USAGE: [errMsg, imageList] = bst_containers('GetImages') + imageList = []; + + % Default container engine + engineName = bst_get('ContainerEngine'); + % Check status of container engine + [isFound, engineName, errMsg] = GetEngine(engineName); + if ~isFound || ~isempty(errMsg) + return + end + + % Import image + switch engineName + case 'docker' + [status, cmdout] = system('docker images --all --no-trunc --format "{{.Repository}}:{{.Tag}} {{.ID}}"'); + if status == 0 + imageList = reshape(strsplit(strtrim(strrep(cmdout, char(10), ' ')), ' '), 2, [])'; + else + errMsg = cmdout; + end + end end \ No newline at end of file From 9cb1e5a32d3ff157d70223e7a3ee6c005db6cfb2 Mon Sep 17 00:00:00 2001 From: rcassani Date: Tue, 21 Apr 2026 12:30:53 -0400 Subject: [PATCH 26/48] Implemente docker image tag --- toolbox/core/bst_containers.m | 57 +++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/toolbox/core/bst_containers.m b/toolbox/core/bst_containers.m index df9c608e26..cfe303e187 100644 --- a/toolbox/core/bst_containers.m +++ b/toolbox/core/bst_containers.m @@ -116,13 +116,17 @@ %% ===== IMPORT IMAGE ===== -function [isOk, errMsg, imageSha] = ImportImage(imageSource) +function [isOk, errMsg, imageSha] = ImportImage(imageSource, imageTag) % Load container image into container engine -% USAGE: [isOk, errMsg, imageSha] = bst_containers('ImportImage', imageSource) +% USAGE: [isOk, errMsg, imageSha] = bst_containers('ImportImage', imageSource, [imageTag]) isOk = 0; errMsg = ''; imageSha = ''; + if (nargin < 2) || isempty(imageTag) + imageTag = ''; + end + % Default container engine engineName = bst_get('ContainerEngine'); % Check status of container engine @@ -131,14 +135,20 @@ return end - % [TODO] Check imageSource is: reference, local file or download URL + % Origin of imageSource imageType = 'reference'; - - % [TODO] Get image from download link - if strcmpi(imageType, 'url') + if ~isempty(regexp(imageSource, '^http[s]*://', 'once')) % Download file in tmp % Update imageSource - % Change type to file + imageType = 'file'; + end + + % Get current available images + if ~isempty(imageTag) + [errMsg, imageListOld] = GetImages(); + if ~isempty(errMsg) + return + end end % Import image @@ -147,12 +157,39 @@ switch imageType case 'reference' [status, cmdout] = system(['docker pull ' imageSource]); + if status == 0 + % If new or existent image, SHA256 is returned in output + imageSha = regexp(cmdout, 'sha256:[a-f0-9]+', 'match', 'once'); + end + case 'file' [status, cmdout] = system(['docker load --input ' imageSource]); + if status == 0 + % If new or existent image, Image name (or SHA256 for nameless image) is returned in output, + token = regexp(output, '[a-z0-9._-]+:([a-zA-Z0-9._-]+', 'tokens', 'once'); + parts = strsplit(token, ':'); + if strcmp(parts{1}, 'sha256') && ~isempty(regexp(parts{2}, '^[a-f0-9]+$', 'once')) + imageSha = token; + else + [~, imageListNew] = GetImages(); + imageSha = imageListNew{strcmpi(imageListNew(:,1), imageSha), 2}; + end + end end - if status == 0 - tokens = regexp(cmdout, 'sha256:[a-f0-9]+', 'match'); - imageSha = strtrim(tokens{1}); + % Tag image + if status == 0 && ~isempty(imageTag) + % Was image added? + [~, imageListNew] = GetImages(); + iNew = setdiff(find(strcmpi(imageListNew(:,2), imageSha)), find(strcmpi(imageListOld(:,2), imageSha)), 'stable'); + % Tag image + [status, cmdout] = system(['docker tag ', imageSha, ' ', imageTag]); + if status == 0 && length(iNew) == 1 && ~strcmpi(imageListNew{iNew, 1}, ':') + [status, cmdout] = system(['docker rmi ', imageListNew{iNew, 1}]); + end + end + if status ~= 0 + errMsg = cmdout; + return end end isOk = status == 0; From c543f9ce246f74732c394e5e8d820551abaed80d Mon Sep 17 00:00:00 2001 From: rcassani Date: Tue, 21 Apr 2026 14:23:16 -0400 Subject: [PATCH 27/48] Bugfix on removing image --- toolbox/core/bst_containers.m | 1 - 1 file changed, 1 deletion(-) diff --git a/toolbox/core/bst_containers.m b/toolbox/core/bst_containers.m index cfe303e187..3b4632c5fb 100644 --- a/toolbox/core/bst_containers.m +++ b/toolbox/core/bst_containers.m @@ -369,7 +369,6 @@ isOk = 0; errMsg = ''; - imageSha = ''; % Default container engine engineName = bst_get('ContainerEngine'); From 9f88aab205c5df4847351bb641c8ab91873b2dbd Mon Sep 17 00:00:00 2001 From: rcassani Date: Tue, 21 Apr 2026 14:24:11 -0400 Subject: [PATCH 28/48] Correct tags for Brainstorm managed images and containers --- toolbox/core/bst_plugin.m | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/toolbox/core/bst_plugin.m b/toolbox/core/bst_plugin.m index 3f01e3e054..3429e26183 100644 --- a/toolbox/core/bst_plugin.m +++ b/toolbox/core/bst_plugin.m @@ -1924,7 +1924,7 @@ function Configure(PlugDesc) file_delete(pkgFile, 1, 3); else % Import container image in container engine - [isOk, errMsg, imageSha] = bst_containers('ImportImage', PlugDesc.ImageSource, ['BST_' PlugDesc.Name]); + [isOk, errMsg, imageSha] = bst_containers('ImportImage', PlugDesc.ImageSource, ['brainstorm_' PlugDesc.Name]); if ~isOk bst_progress('removeimage'); return @@ -2224,9 +2224,13 @@ function Configure(PlugDesc) end quit('force'); end - % Remove image from container engine + % Stop container if isContainer && ~isempty(PlugDesc.ImageSha) - [isOk] = bst_containers('StopContainer', PlugDesc.Name, 1); + isOk = bst_containers('StopContainer', ['bst_' PlugDesc.Name], 1); + if isOk + % Remove image from container engine + isOk = bst_containers('RemoveImage', ['brainstorm_' PlugDesc.Name], 1); + end if ~isOk return end @@ -2649,8 +2653,8 @@ function Configure(PlugDesc) % Stop container if isContainer % Retrieve info of container - [~, ~, volumePairs] = bst_containers('GetContainerInfo', PlugDesc.Name); - [isOk] = bst_containers('StopContainer', PlugDesc.Name, 1); + [~, ~, volumePairs] = bst_containers('GetContainerInfo', ['bst_' PlugDesc.Name]); + isOk = bst_containers('StopContainer', ['bst_' PlugDesc.Name], 1); % Delete temporary files file_delete(volumePairs{1,1}, 1, 1); if ~isOk From 4ca3c57c600ff7f97e19cb9935753d2231b10abb Mon Sep 17 00:00:00 2001 From: rcassani Date: Tue, 21 Apr 2026 15:03:21 -0400 Subject: [PATCH 29/48] Manange download and import of image file --- toolbox/core/bst_containers.m | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/toolbox/core/bst_containers.m b/toolbox/core/bst_containers.m index 3b4632c5fb..7a39dbf2fc 100644 --- a/toolbox/core/bst_containers.m +++ b/toolbox/core/bst_containers.m @@ -138,8 +138,18 @@ % Origin of imageSource imageType = 'reference'; if ~isempty(regexp(imageSource, '^http[s]*://', 'once')) - % Download file in tmp - % Update imageSource + % Get tmp dir to bind container + tmpDir = bst_get('BrainstormTmpDir', 0, 'pull_image'); + imageFile = bst_fullfile(tmpDir, 'image.tgz'); + disp(['BST> Downloading URL : ' imageSource]); + disp(['BST> Saving to file : ' imageFile]); + errMsg = gui_brainstorm('DownloadFile', imageSource, imageFile, 'Download container image: '); + % If file was not downloaded correctly + if ~isempty(errMsg) + errMsg = ['Impossible to download container image automatically:' 10 errMsg]; + return + end + imageSource = imageFile; imageType = 'file'; end @@ -166,13 +176,13 @@ [status, cmdout] = system(['docker load --input ' imageSource]); if status == 0 % If new or existent image, Image name (or SHA256 for nameless image) is returned in output, - token = regexp(output, '[a-z0-9._-]+:([a-zA-Z0-9._-]+', 'tokens', 'once'); + token = regexp(cmdout, '[a-z0-9._-]+:[a-zA-Z0-9._-]+', 'match', 'once'); parts = strsplit(token, ':'); if strcmp(parts{1}, 'sha256') && ~isempty(regexp(parts{2}, '^[a-f0-9]+$', 'once')) imageSha = token; else [~, imageListNew] = GetImages(); - imageSha = imageListNew{strcmpi(imageListNew(:,1), imageSha), 2}; + imageSha = imageListNew{strcmpi(imageListNew(:,1), token), 2}; end end end From 7f546562dccfa73cbd119a4c2de31fa9fb1dee7f Mon Sep 17 00:00:00 2001 From: rcassani Date: Thu, 23 Apr 2026 12:09:28 -0400 Subject: [PATCH 30/48] Improve tagging of new imported image --- toolbox/core/bst_containers.m | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/toolbox/core/bst_containers.m b/toolbox/core/bst_containers.m index 7a39dbf2fc..43fdcea123 100644 --- a/toolbox/core/bst_containers.m +++ b/toolbox/core/bst_containers.m @@ -188,13 +188,21 @@ end % Tag image if status == 0 && ~isempty(imageTag) - % Was image added? + % Compare images before and after import [~, imageListNew] = GetImages(); - iNew = setdiff(find(strcmpi(imageListNew(:,2), imageSha)), find(strcmpi(imageListOld(:,2), imageSha)), 'stable'); + iOld = find(strcmpi(imageListOld(:,2), imageSha)); + iNew = find(strcmpi(imageListNew(:,2), imageSha)); % Tag image [status, cmdout] = system(['docker tag ', imageSha, ' ', imageTag]); - if status == 0 && length(iNew) == 1 && ~strcmpi(imageListNew{iNew, 1}, ':') - [status, cmdout] = system(['docker rmi ', imageListNew{iNew, 1}]); + if status == 0 && (length(iNew) - length(iOld)) == 1 + if ~isempty(imageListOld) + imageDel = setdiff(imageListNew{iNew, 1}, imageListOld{iOld, 1}); + else + imageDel = imageListNew{iNew, 1}; + end + if ~strcmpi(imageDel, ':') + [status, cmdout] = system(['docker rmi ', imageListNew{iNew, 1}]); + end end end if status ~= 0 @@ -415,7 +423,7 @@ %% ===== GET AVAILABLE IMAGES ===== function [errMsg, imageList] = GetImages() % USAGE: [errMsg, imageList] = bst_containers('GetImages') - imageList = []; + imageList = cell(0,2); % Default container engine engineName = bst_get('ContainerEngine'); @@ -430,7 +438,9 @@ case 'docker' [status, cmdout] = system('docker images --all --no-trunc --format "{{.Repository}}:{{.Tag}} {{.ID}}"'); if status == 0 - imageList = reshape(strsplit(strtrim(strrep(cmdout, char(10), ' ')), ' '), 2, [])'; + if ~isempty(cmdout) + imageList = reshape(strsplit(strtrim(strrep(cmdout, char(10), ' ')), ' '), 2, [])'; + end else errMsg = cmdout; end From 771bc538ef5e1529577b4f1ffe7a6c0c18af972f Mon Sep 17 00:00:00 2001 From: rcassani Date: Thu, 23 Apr 2026 12:31:29 -0400 Subject: [PATCH 31/48] Only Docker for the moment, and clean up --- toolbox/core/bst_containers.m | 130 +++++++++++++++------------------- toolbox/core/bst_get.m | 2 +- 2 files changed, 58 insertions(+), 74 deletions(-) diff --git a/toolbox/core/bst_containers.m b/toolbox/core/bst_containers.m index 43fdcea123..af4241aae7 100644 --- a/toolbox/core/bst_containers.m +++ b/toolbox/core/bst_containers.m @@ -1,11 +1,14 @@ -function [varargout] = bst_containers(varargin) -% BST_CONTAINERS: Manages containers for container-based plugins in Brainstorm +function varargout = bst_containers(varargin) +% BST_CONTAINERS: Manage containers for container-based plugins in Brainstorm % % USAGE: [isOk, eName, eStatus] = bst_containers('GetEngine') -% [isOk, errMsg, imageSha] = bst_containers('ImportImage', imageSource) +% [errMsg, imageList] = bst_containers('GetImages') +% [isOk, errMsg, imageSha] = bst_containers('ImportImage', imageSource, [imageTag]) % [isOk, errMsg. containerName] = bst_containers('RunContainer', containerName, imageSha, [volumes], [isDaemon]) % [isOk, cmdout] = bst_containers('ExecInContainer', containerName, cmdStr) -% [isOk, cmdout] = bst_containers('StatusContainer', containerName) +% [containerName, isRunning, volumePairs, imageSha] = bst_containers('GetContainerInfo', containerName) +% [isOk, cmdout] = bst_containers('StopContainer', containerName, [isForced=0]) +% [isOk, cmdout] = bst_containers('RemoveImage', imageSha/Name, [isForced=0]) % @============================================================================= % This function is part of the Brainstorm software: @@ -28,7 +31,6 @@ % Authors: Raymundo Cassani, 2026 % Takfarinas Medani, 2026 - eval(macro_method); end @@ -55,25 +57,28 @@ % Tests container engines for iEngine = 1 : length(engineNames) - if ispc - [status, cmdout] = system(['where ' engineNames{iEngine}]); - if status == 0 - cmdout = strsplit(strtrim(cmdout), '\n'); - if length(cmdout) >= 1 - isFound = 1; - enginePath = strtrim(cmdout{1}); + switch engineNames{iEngine} + case {'docker'} + if ispc + [status, cmdout] = system(['where ' engineNames{iEngine}]); + if status == 0 + cmdout = strsplit(strtrim(cmdout), '\n'); + if length(cmdout) >= 1 + isFound = 1; + enginePath = strtrim(cmdout{1}); + end + end + else + [status, cmdout] = system(['which ' engineNames{iEngine}]); + if status == 0 + isFound = 1; + enginePath = strtrim(cmdout); + end end - end - else - [status, cmdout] = system(['which ' engineNames{iEngine}]); - if status == 0 - isFound = 1; - enginePath = strtrim(cmdout); - end end + % Break loop if found if isFound engineName = engineNames{iEngine}; - fprintf('Container engine "%s" found in "%s"\r', engineName, enginePath); break end end @@ -101,26 +106,43 @@ errMsg = cmdout; return end + end +end - case 'podman' - [status, cmdout] = system([engineName ' info', '-echo']); - if status == 1 - errMsg = cmdout; - return - end - otherwise +%% ===== GET AVAILABLE IMAGES ===== +function [errMsg, imageList] = GetImages() +% USAGE: [errMsg, imageList] = bst_containers('GetImages') + imageList = cell(0,2); + % Default container engine + engineName = bst_get('ContainerEngine'); + % Check status of container engine + [isFound, engineName, errMsg] = GetEngine(engineName); + if ~isFound || ~isempty(errMsg) + return + end + + % Import image + switch engineName + case 'docker' + [status, cmdout] = system('docker images --all --no-trunc --format "{{.Repository}}:{{.Tag}} {{.ID}}"'); + if status == 0 + if ~isempty(cmdout) + imageList = reshape(strsplit(strtrim(strrep(cmdout, char(10), ' ')), ' '), 2, [])'; + end + else + errMsg = cmdout; + end end end %% ===== IMPORT IMAGE ===== function [isOk, errMsg, imageSha] = ImportImage(imageSource, imageTag) -% Load container image into container engine +% Import container image into container engine % USAGE: [isOk, errMsg, imageSha] = bst_containers('ImportImage', imageSource, [imageTag]) isOk = 0; - errMsg = ''; imageSha = ''; if (nargin < 2) || isempty(imageTag) @@ -219,7 +241,7 @@ %% ===== RUN CONTAINER AS DAEMON ===== function [isOk, errMsg, containerName] = RunContainer(containerName, imageSha, volumes, isDaemon) -% USAGE: [isOk, errMsg, imageSha] = bst_containers('RunDaemonContainer', imageSha, volumes) +% USAGE: [isOk, errMsg, containerName] = bst_containers('RunContainer', imageSha, volumes, isDaemon) isOk = 0; % Validate inputs @@ -252,7 +274,6 @@ % Run container switch engineName case 'docker' - cmdStr = ['docker run -d --name ' containerName]; if ~isDaemon cmdStr = sprintf('docker run --rm --name %s %s %s', containerName, volumesStr, imageSha); else @@ -263,6 +284,7 @@ end isOk = status == 0; if ~isOk + errMsg = cmdout; return end end @@ -322,7 +344,6 @@ % Find containers with same name [status, cmdout] = system(['docker inspect ' containerName ' --format "{{.Name}}"']); if status == 0 - isExist = 1; containerNameOut = strrep(strtrim(cmdout), '/', ''); [status, cmdout] = system(['docker inspect ' containerName ' --format "'... '{{.State.Status}} # ' ... @@ -343,7 +364,7 @@ %% ===== STOP CONTAINER ===== -function isOk = StopContainer(containerName, isForce) +function [isOk, cmdout] = StopContainer(containerName, isForce) isOk = 0; % Validate inputs @@ -371,13 +392,14 @@ [status, cmdout] = system(['docker rm -f ' containerName]); end isOk = status == 0; + cmdout = strtrim(cmdout); end end %% ===== REMOVE IMAGE ===== -function isOk = RemoveImage(imageSha, isForce) +function [isOk, cmdout] = RemoveImage(imageSha, isForce) isOk = 0; % Validate inputs @@ -385,23 +407,11 @@ isForce = 0; end - isOk = 0; - errMsg = ''; - - % Default container engine - engineName = bst_get('ContainerEngine'); - % Check status of container engine - [isFound, engineName, errMsg] = GetEngine(engineName); - if ~isFound || ~isempty(errMsg) - return - end - % Default container engine engineName = bst_get('ContainerEngine'); % Check status of container engine [isFound, engineName, errMsg] = GetEngine(engineName); if ~isFound || ~isempty(errMsg) - disp(errMsg) return end @@ -416,33 +426,7 @@ [status, cmdout] = system(['docker rmi -f ' imageSha]); end isOk = status == 0; + cmdout = strtrim(cmdout); end end - -%% ===== GET AVAILABLE IMAGES ===== -function [errMsg, imageList] = GetImages() -% USAGE: [errMsg, imageList] = bst_containers('GetImages') - imageList = cell(0,2); - - % Default container engine - engineName = bst_get('ContainerEngine'); - % Check status of container engine - [isFound, engineName, errMsg] = GetEngine(engineName); - if ~isFound || ~isempty(errMsg) - return - end - - % Import image - switch engineName - case 'docker' - [status, cmdout] = system('docker images --all --no-trunc --format "{{.Repository}}:{{.Tag}} {{.ID}}"'); - if status == 0 - if ~isempty(cmdout) - imageList = reshape(strsplit(strtrim(strrep(cmdout, char(10), ' ')), ' '), 2, [])'; - end - else - errMsg = cmdout; - end - end -end \ No newline at end of file diff --git a/toolbox/core/bst_get.m b/toolbox/core/bst_get.m index 4ef5a4fb98..854dd2d237 100644 --- a/toolbox/core/bst_get.m +++ b/toolbox/core/bst_get.m @@ -2995,7 +2995,7 @@ end case 'ContainerEngine' - containerEngines = {'auto-detect', 'docker', 'podman'}; + containerEngines = {'auto-detect', 'docker'}; % Get saved value if isfield(GlobalData, 'Preferences') && isfield(GlobalData.Preferences, 'ContainerEngine') && ~isempty(GlobalData.Preferences.ContainerEngine) argout1 = GlobalData.Preferences.ContainerEngine; From feba1fdcbc72ace57e5c8eaf3d85162df97950fe Mon Sep 17 00:00:00 2001 From: rcassani Date: Thu, 23 Apr 2026 14:38:39 -0400 Subject: [PATCH 32/48] Update output args --- toolbox/core/bst_containers.m | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/toolbox/core/bst_containers.m b/toolbox/core/bst_containers.m index af4241aae7..5428a4988e 100644 --- a/toolbox/core/bst_containers.m +++ b/toolbox/core/bst_containers.m @@ -240,9 +240,10 @@ %% ===== RUN CONTAINER AS DAEMON ===== -function [isOk, errMsg, containerName] = RunContainer(containerName, imageSha, volumes, isDaemon) +function [isOk, errMsg, containerNameOut] = RunContainer(containerName, imageSha, volumes, isDaemon) % USAGE: [isOk, errMsg, containerName] = bst_containers('RunContainer', imageSha, volumes, isDaemon) isOk = 0; + containerNameOut = ''; % Validate inputs if nargin < 4 || isempty(isDaemon) @@ -287,6 +288,7 @@ errMsg = cmdout; return end + containerNameOut = containerName; end @@ -327,7 +329,9 @@ %% ===== CHECK CONTAINER STATUS ===== function [containerNameOut, isRunning, volumePairs, imageSha] = GetContainerInfo(containerName) containerNameOut = []; - isRunning = 0; + isRunning = 0; + volumePairs = []; + imageSha = ''; % Default container engine engineName = bst_get('ContainerEngine'); @@ -366,6 +370,7 @@ %% ===== STOP CONTAINER ===== function [isOk, cmdout] = StopContainer(containerName, isForce) isOk = 0; + cmdout = ''; % Validate inputs if nargin < 2 || isempty(isForce) @@ -401,6 +406,7 @@ %% ===== REMOVE IMAGE ===== function [isOk, cmdout] = RemoveImage(imageSha, isForce) isOk = 0; + cmdout = ''; % Validate inputs if nargin < 2 || isempty(isForce) From 71846e378e97398981e76bc3855f46f6d3d0b2fc Mon Sep 17 00:00:00 2001 From: rcassani Date: Thu, 23 Apr 2026 14:39:07 -0400 Subject: [PATCH 33/48] Check container image is imported before loading --- toolbox/core/bst_plugin.m | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/toolbox/core/bst_plugin.m b/toolbox/core/bst_plugin.m index 3429e26183..25cc60c8a8 100644 --- a/toolbox/core/bst_plugin.m +++ b/toolbox/core/bst_plugin.m @@ -2489,11 +2489,31 @@ function Configure(PlugDesc) % Run container if image was properly imported if isContainer PlugDesc = GetInstalled(PlugDesc); + imageName = ['brainstorm_' PlugDesc.Name]; if ~isempty(PlugDesc.ImageSha) && ~PlugDesc.isLoaded - % Get tmp dir to bind container - TmpDir = bst_get('BrainstormTmpDir', 0, PlugDesc.Name); - volumes = {TmpDir, '/data'}; - [isOk, errMsg] = bst_containers('RunContainer', ['bst_' PlugDesc.Name], PlugDesc.ImageSha, volumes, 1); + % Get available images in container engine + [errMsg, imageList] = bst_containers('GetImages'); + if ~isempty(errMsg) + return + end + % Check that image is imported in container engine + isImported = 0; + if ~isempty(imageList) + iImageSha = strcmpi(imageList(:,2), PlugDesc.ImageSha); + isImported = any(strncmpi(imageList(iImageSha,1), imageName, length(imageName))); + end + if isImported + % Get tmp dir to bind container + TmpDir = bst_get('BrainstormTmpDir', 0, PlugDesc.Name); + volumes = {TmpDir, '/data'}; + % Run container as daemon + [isOk, errMsg] = bst_containers('RunContainer', ['bst_' PlugDesc.Name], PlugDesc.ImageSha, volumes, 1); + else + % Uninstall container plugin + Uninstall(PlugDesc.Name, 0, 0); + errMsg = ['Reinstall plugin ' PlugDesc.Name '.' 10 10 'Missing container image: ' imageName 10 'SHA: ' PlugDesc.ImageSha]; + return + end if ~isOk return end From a6f77f92af44c7016b64a94af8966f4e4908d7fd Mon Sep 17 00:00:00 2001 From: rcassani Date: Fri, 24 Apr 2026 12:57:47 -0400 Subject: [PATCH 34/48] Update comments --- toolbox/core/bst_containers.m | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/toolbox/core/bst_containers.m b/toolbox/core/bst_containers.m index 5428a4988e..1a47a68c33 100644 --- a/toolbox/core/bst_containers.m +++ b/toolbox/core/bst_containers.m @@ -1,10 +1,11 @@ function varargout = bst_containers(varargin) % BST_CONTAINERS: Manage containers for container-based plugins in Brainstorm % -% USAGE: [isOk, eName, eStatus] = bst_containers('GetEngine') +% USAGE: +% [isFound, engineName, errMsg] = bst_containers('GetEngine') % [errMsg, imageList] = bst_containers('GetImages') % [isOk, errMsg, imageSha] = bst_containers('ImportImage', imageSource, [imageTag]) -% [isOk, errMsg. containerName] = bst_containers('RunContainer', containerName, imageSha, [volumes], [isDaemon]) +% [isOk, errMsg, containerName] = bst_containers('RunContainer', containerName, imageSha, [volumes], [isDaemon]) % [isOk, cmdout] = bst_containers('ExecInContainer', containerName, cmdStr) % [containerName, isRunning, volumePairs, imageSha] = bst_containers('GetContainerInfo', containerName) % [isOk, cmdout] = bst_containers('StopContainer', containerName, [isForced=0]) @@ -241,7 +242,7 @@ %% ===== RUN CONTAINER AS DAEMON ===== function [isOk, errMsg, containerNameOut] = RunContainer(containerName, imageSha, volumes, isDaemon) -% USAGE: [isOk, errMsg, containerName] = bst_containers('RunContainer', imageSha, volumes, isDaemon) +% USAGE: [isOk, errMsg, containerName] = bst_containers('RunContainer', containerName, imageSha, volumes, isDaemon) isOk = 0; containerNameOut = ''; @@ -421,7 +422,7 @@ return end - % Stop container + % Remove image switch engineName case 'docker' if ~isForce From 969eac1ada07455129dd11eb1e2a3392e708f77c Mon Sep 17 00:00:00 2001 From: rcassani Date: Fri, 24 Apr 2026 12:57:58 -0400 Subject: [PATCH 35/48] Clean up --- toolbox/core/bst_containers.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/toolbox/core/bst_containers.m b/toolbox/core/bst_containers.m index 1a47a68c33..afbb3b4cb4 100644 --- a/toolbox/core/bst_containers.m +++ b/toolbox/core/bst_containers.m @@ -64,7 +64,7 @@ [status, cmdout] = system(['where ' engineNames{iEngine}]); if status == 0 cmdout = strsplit(strtrim(cmdout), '\n'); - if length(cmdout) >= 1 + if ~isempty(cmdout) isFound = 1; enginePath = strtrim(cmdout{1}); end @@ -99,7 +99,7 @@ end % Check the container engine status - switch(engineName) + switch engineName case 'docker' [status, cmdout] = system([engineName ' info']); cmdout = strtrim(cmdout); From e19db12de536fc181a950eacabe3fb91574b6801 Mon Sep 17 00:00:00 2001 From: rcassani Date: Fri, 24 Apr 2026 13:38:51 -0400 Subject: [PATCH 36/48] Bugfix: Correct indexing of volume pairs --- toolbox/core/bst_containers.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toolbox/core/bst_containers.m b/toolbox/core/bst_containers.m index afbb3b4cb4..ab977992f7 100644 --- a/toolbox/core/bst_containers.m +++ b/toolbox/core/bst_containers.m @@ -268,7 +268,7 @@ nPairs = size(volumes, 1); pairs = cell(nPairs, 1); for iPair = 1 : nPairs - pairs{iPair} = ['-v' volumes{1} ':' volumes{2}]; + pairs{iPair} = ['-v' volumes{iPair, 1} ':' volumes{iPair, 2}]; end volumesStr = strjoin(pairs, ' '); end From 4fcbf19ec0918072c7fd6e84945e40c3af888e40 Mon Sep 17 00:00:00 2001 From: rcassani Date: Fri, 24 Apr 2026 13:48:56 -0400 Subject: [PATCH 37/48] Bugfix: Correct capitalization --- toolbox/core/bst_containers.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toolbox/core/bst_containers.m b/toolbox/core/bst_containers.m index ab977992f7..de76e6a860 100644 --- a/toolbox/core/bst_containers.m +++ b/toolbox/core/bst_containers.m @@ -103,7 +103,7 @@ case 'docker' [status, cmdout] = system([engineName ' info']); cmdout = strtrim(cmdout); - if status == 1 || ~isempty(strfind(lower(cmdout), 'failed')) || ~isempty(strfind(lower(cmdout), 'ERROR')) + if status == 1 || ~isempty(strfind(lower(cmdout), 'failed')) || ~isempty(strfind(lower(cmdout), 'error')) errMsg = cmdout; return end From eba1a65629623a116f685f5b38108d0a46805fd3 Mon Sep 17 00:00:00 2001 From: rcassani Date: Fri, 24 Apr 2026 14:17:55 -0400 Subject: [PATCH 38/48] Update calls to `GetEngine` --- toolbox/core/bst_containers.m | 73 +++++++++++++---------------------- 1 file changed, 27 insertions(+), 46 deletions(-) diff --git a/toolbox/core/bst_containers.m b/toolbox/core/bst_containers.m index de76e6a860..9f8753d5d1 100644 --- a/toolbox/core/bst_containers.m +++ b/toolbox/core/bst_containers.m @@ -2,7 +2,7 @@ % BST_CONTAINERS: Manage containers for container-based plugins in Brainstorm % % USAGE: -% [isFound, engineName, errMsg] = bst_containers('GetEngine') +% [errMsg, engineName] = bst_containers('GetEngine') % [errMsg, imageList] = bst_containers('GetImages') % [isOk, errMsg, imageSha] = bst_containers('ImportImage', imageSource, [imageTag]) % [isOk, errMsg, containerName] = bst_containers('RunContainer', containerName, imageSha, [volumes], [isDaemon]) @@ -37,10 +37,9 @@ %% ===== GET CONTAINER ENGINE ===== -function [isFound, engineName, errMsg] = GetEngine(engineName) -% USAGE: [isFound, engineName, errMsg] = bst_containers('GetEngine') % Find, test and set a supported container engine -% [isFound, engineName, errMsg] = bst_containers('GetEngine', engineName) % Test the requested container engine - isFound = 0; +function [errMsg, engineName] = GetEngine(engineName) +% USAGE: [errMsg, engineName] = bst_containers('GetEngine') % Find, test and set a supported container engine +% [errMsg, engineName] = bst_containers('GetEngine', engineName) % Test the requested container engine errMsg = ''; % Get and test all the supported container engines @@ -58,6 +57,7 @@ % Tests container engines for iEngine = 1 : length(engineNames) + isFound = 0; switch engineNames{iEngine} case {'docker'} if ispc @@ -91,10 +91,8 @@ errMsg = ['Container engine ' engineName ' was not found']; end return - end - % Set as default the container engine found - if isSetDefault + elseif isSetDefault bst_set('ContainerEngine', engineName); end @@ -116,11 +114,9 @@ % USAGE: [errMsg, imageList] = bst_containers('GetImages') imageList = cell(0,2); - % Default container engine - engineName = bst_get('ContainerEngine'); - % Check status of container engine - [isFound, engineName, errMsg] = GetEngine(engineName); - if ~isFound || ~isempty(errMsg) + % Check status of default container engine + [errMsg, engineName] = GetEngine(bst_get('ContainerEngine')); + if ~isempty(errMsg) return end @@ -150,11 +146,9 @@ imageTag = ''; end - % Default container engine - engineName = bst_get('ContainerEngine'); - % Check status of container engine - [isFound, engineName, errMsg] = GetEngine(engineName); - if ~isFound || ~isempty(errMsg) + % Check status of default container engine + [errMsg, engineName] = GetEngine(bst_get('ContainerEngine')); + if ~isempty(errMsg) return end @@ -254,11 +248,9 @@ volumes = []; end - % Default container engine - engineName = bst_get('ContainerEngine'); - % Check status of container engine - [isFound, engineName, errMsg] = GetEngine(engineName); - if ~isFound || ~isempty(errMsg) + % Check status of default container engine + [errMsg, engineName] = GetEngine(bst_get('ContainerEngine')); + if ~isempty(errMsg) return end @@ -298,12 +290,9 @@ isOk = 0; cmdout = ''; - % Default container engine - engineName = bst_get('ContainerEngine'); - % Check status of container engine - [isFound, engineName, errMsg] = GetEngine(engineName); - if ~isFound || ~isempty(errMsg) - disp(errMsg) + % Check status of default container engine + [errMsg, engineName] = GetEngine(bst_get('ContainerEngine')); + if ~isempty(errMsg) return end @@ -334,12 +323,9 @@ volumePairs = []; imageSha = ''; - % Default container engine - engineName = bst_get('ContainerEngine'); - % Check status of container engine - [isFound, engineName, errMsg] = GetEngine(engineName); - if ~isFound || ~isempty(errMsg) - disp(errMsg) + % Check status of default container engine + [errMsg, engineName] = GetEngine(bst_get('ContainerEngine')); + if ~isempty(errMsg) return end @@ -378,12 +364,9 @@ isForce = 0; end - % Default container engine - engineName = bst_get('ContainerEngine'); - % Check status of container engine - [isFound, engineName, errMsg] = GetEngine(engineName); - if ~isFound || ~isempty(errMsg) - disp(errMsg) + % Check status of default container engine + [errMsg, engineName] = GetEngine(bst_get('ContainerEngine')); + if ~isempty(errMsg) return end @@ -414,11 +397,9 @@ isForce = 0; end - % Default container engine - engineName = bst_get('ContainerEngine'); - % Check status of container engine - [isFound, engineName, errMsg] = GetEngine(engineName); - if ~isFound || ~isempty(errMsg) + % Check status of default container engine + [errMsg, engineName] = GetEngine(bst_get('ContainerEngine')); + if ~isempty(errMsg) return end From ed0629595aa7a0e9260d9f1256e13eaed573ef26 Mon Sep 17 00:00:00 2001 From: rcassani Date: Fri, 24 Apr 2026 14:28:21 -0400 Subject: [PATCH 39/48] Update code to `GetImages` --- toolbox/core/bst_containers.m | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/toolbox/core/bst_containers.m b/toolbox/core/bst_containers.m index 9f8753d5d1..94aa76e9ea 100644 --- a/toolbox/core/bst_containers.m +++ b/toolbox/core/bst_containers.m @@ -3,7 +3,7 @@ % % USAGE: % [errMsg, engineName] = bst_containers('GetEngine') -% [errMsg, imageList] = bst_containers('GetImages') +% [errMsg, imageList] = bst_containers('GetImages') % [isOk, errMsg, imageSha] = bst_containers('ImportImage', imageSource, [imageTag]) % [isOk, errMsg, containerName] = bst_containers('RunContainer', containerName, imageSha, [volumes], [isDaemon]) % [isOk, cmdout] = bst_containers('ExecInContainer', containerName, cmdStr) @@ -112,7 +112,7 @@ %% ===== GET AVAILABLE IMAGES ===== function [errMsg, imageList] = GetImages() % USAGE: [errMsg, imageList] = bst_containers('GetImages') - imageList = cell(0,2); + imageList = cell(0,2); % [Name:Tag, SHA] % Check status of default container engine [errMsg, engineName] = GetEngine(bst_get('ContainerEngine')); @@ -120,15 +120,18 @@ return end - % Import image + % List of available images switch engineName case 'docker' [status, cmdout] = system('docker images --all --no-trunc --format "{{.Repository}}:{{.Tag}} {{.ID}}"'); - if status == 0 - if ~isempty(cmdout) - imageList = reshape(strsplit(strtrim(strrep(cmdout, char(10), ' ')), ' '), 2, [])'; + if status == 0 && ~isempty(cmdout) + imageList = strsplit(strtrim(strrep(cmdout, char(10), ' ')), ' '); + if mod(length(imageList), 2) ~= 0 + errMsg = 'Error parsing Docker image list'; + return end - else + imageList = reshape(imageList, 2, [])'; + elseif status ~= 0 errMsg = cmdout; end end From abc5a3f39e20e67755cc6d9d6ab67401b15d2e9c Mon Sep 17 00:00:00 2001 From: rcassani Date: Fri, 24 Apr 2026 14:40:10 -0400 Subject: [PATCH 40/48] Update calls to `ImportImage` --- toolbox/core/bst_containers.m | 16 ++++++++-------- toolbox/core/bst_plugin.m | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/toolbox/core/bst_containers.m b/toolbox/core/bst_containers.m index 94aa76e9ea..ff3f10d22b 100644 --- a/toolbox/core/bst_containers.m +++ b/toolbox/core/bst_containers.m @@ -4,7 +4,7 @@ % USAGE: % [errMsg, engineName] = bst_containers('GetEngine') % [errMsg, imageList] = bst_containers('GetImages') -% [isOk, errMsg, imageSha] = bst_containers('ImportImage', imageSource, [imageTag]) +% [errMsg, imageSha] = bst_containers('ImportImage', imageSource, [imageTag]) % [isOk, errMsg, containerName] = bst_containers('RunContainer', containerName, imageSha, [volumes], [isDaemon]) % [isOk, cmdout] = bst_containers('ExecInContainer', containerName, cmdStr) % [containerName, isRunning, volumePairs, imageSha] = bst_containers('GetContainerInfo', containerName) @@ -139,10 +139,9 @@ %% ===== IMPORT IMAGE ===== -function [isOk, errMsg, imageSha] = ImportImage(imageSource, imageTag) -% Import container image into container engine -% USAGE: [isOk, errMsg, imageSha] = bst_containers('ImportImage', imageSource, [imageTag]) - isOk = 0; +function [errMsg, imageSha] = ImportImage(imageSource, imageTag) +% Import container image into container engine, and create a tag +% USAGE: [errMsg, imageSha] = bst_containers('ImportImage', imageSource, [imageTag]) imageSha = ''; if (nargin < 2) || isempty(imageTag) @@ -155,8 +154,9 @@ return end - % Origin of imageSource + % Default: imageSource is an image reference imageType = 'reference'; + % If imageSource is a URL, download image file if ~isempty(regexp(imageSource, '^http[s]*://', 'once')) % Get tmp dir to bind container tmpDir = bst_get('BrainstormTmpDir', 0, 'pull_image'); @@ -164,7 +164,6 @@ disp(['BST> Downloading URL : ' imageSource]); disp(['BST> Saving to file : ' imageFile]); errMsg = gui_brainstorm('DownloadFile', imageSource, imageFile, 'Download container image: '); - % If file was not downloaded correctly if ~isempty(errMsg) errMsg = ['Impossible to download container image automatically:' 10 errMsg]; return @@ -195,7 +194,7 @@ case 'file' [status, cmdout] = system(['docker load --input ' imageSource]); if status == 0 - % If new or existent image, Image name (or SHA256 for nameless image) is returned in output, + % If new or existent image, Image name (or SHA256 for nameless image) is returned in output token = regexp(cmdout, '[a-z0-9._-]+:[a-zA-Z0-9._-]+', 'match', 'once'); parts = strsplit(token, ':'); if strcmp(parts{1}, 'sha256') && ~isempty(regexp(parts{2}, '^[a-f0-9]+$', 'once')) @@ -214,6 +213,7 @@ iNew = find(strcmpi(imageListNew(:,2), imageSha)); % Tag image [status, cmdout] = system(['docker tag ', imageSha, ' ', imageTag]); + % Keep only the tag image IF the image was added in this call to ImportImage() if status == 0 && (length(iNew) - length(iOld)) == 1 if ~isempty(imageListOld) imageDel = setdiff(imageListNew{iNew, 1}, imageListOld{iOld, 1}); diff --git a/toolbox/core/bst_plugin.m b/toolbox/core/bst_plugin.m index 25cc60c8a8..2509ae98a9 100644 --- a/toolbox/core/bst_plugin.m +++ b/toolbox/core/bst_plugin.m @@ -1924,8 +1924,8 @@ function Configure(PlugDesc) file_delete(pkgFile, 1, 3); else % Import container image in container engine - [isOk, errMsg, imageSha] = bst_containers('ImportImage', PlugDesc.ImageSource, ['brainstorm_' PlugDesc.Name]); - if ~isOk + [errMsg, imageSha] = bst_containers('ImportImage', PlugDesc.ImageSource, ['brainstorm_' PlugDesc.Name]); + if ~isempty(errMsg) bst_progress('removeimage'); return end From 162f66bded880d848c91737d3c1e62bbe978b0cf Mon Sep 17 00:00:00 2001 From: rcassani Date: Fri, 24 Apr 2026 15:14:50 -0400 Subject: [PATCH 41/48] Update code to `GetContainerInfo` --- toolbox/core/bst_containers.m | 52 +++++++++++++++++++---------------- toolbox/core/bst_plugin.m | 5 +++- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/toolbox/core/bst_containers.m b/toolbox/core/bst_containers.m index ff3f10d22b..4497f68091 100644 --- a/toolbox/core/bst_containers.m +++ b/toolbox/core/bst_containers.m @@ -7,9 +7,9 @@ % [errMsg, imageSha] = bst_containers('ImportImage', imageSource, [imageTag]) % [isOk, errMsg, containerName] = bst_containers('RunContainer', containerName, imageSha, [volumes], [isDaemon]) % [isOk, cmdout] = bst_containers('ExecInContainer', containerName, cmdStr) -% [containerName, isRunning, volumePairs, imageSha] = bst_containers('GetContainerInfo', containerName) % [isOk, cmdout] = bst_containers('StopContainer', containerName, [isForced=0]) % [isOk, cmdout] = bst_containers('RemoveImage', imageSha/Name, [isForced=0]) +% [errMsg, containerInfo] = bst_containers('GetContainerInfo', containerName) % @============================================================================= % This function is part of the Brainstorm software: @@ -300,8 +300,8 @@ end % Check container status - [containerName, isRunning] = GetContainerInfo(containerName); - if isempty(containerName) || ~isRunning + [errMsg, containerInfo] = GetContainerInfo(containerName); + if ~isempty(errMsg) || ~containerInfo.isRunning return end @@ -320,11 +320,13 @@ %% ===== CHECK CONTAINER STATUS ===== -function [containerNameOut, isRunning, volumePairs, imageSha] = GetContainerInfo(containerName) - containerNameOut = []; - isRunning = 0; - volumePairs = []; - imageSha = ''; +function [errMsg, containerInfo] = GetContainerInfo(containerName) +% [containerNameOut, isRunning, volumePairs, imageSha] + containerInfo = struct(); + containerInfo.name = ''; + containerInfo.isRunning = 0; + containerInfo.volumes = []; + containerInfo.imageSha = ''; % Check status of default container engine [errMsg, engineName] = GetEngine(bst_get('ContainerEngine')); @@ -337,22 +339,26 @@ case 'docker' % Find containers with same name [status, cmdout] = system(['docker inspect ' containerName ' --format "{{.Name}}"']); - if status == 0 - containerNameOut = strrep(strtrim(cmdout), '/', ''); - [status, cmdout] = system(['docker inspect ' containerName ' --format "'... - '{{.State.Status}} # ' ... - '{{.HostConfig.Binds}} # ' ... - '{{.Image}}"']); - cmdout = strsplit(strtrim(cmdout), '#'); - isRunning = strcmpi('running', strtrim(cmdout{1})); - volumes = regexprep(strtrim(cmdout{2}), '^\[|\]$', ''); - volumes = regexprep(volumes, ':\', ';\'); - volumePairs = strsplit(volumes, ':'); - volumePairs = cellfun(@(x) regexprep(x, ';\', ':\'), volumePairs, 'UniformOutput', 0); - volumePairs = reshape(volumePairs, 2, [])'; - tokens = regexp(cmdout{3}, 'sha256:[a-f0-9]+', 'match'); - imageSha = strtrim(tokens{1}); + if status ~= 0 + errMsg = strtrim(cmdout); + return + end + containerInfo.name = strrep(strtrim(cmdout), '/', ''); + [status, cmdout] = system(['docker inspect ' containerName ' --format "'... + '{{.State.Status}} # {{.HostConfig.Binds}} # {{.Image}}"']); + if status ~= 0 + errMsg = strtrim(cmdout); + return end + cmdout = strsplit(strtrim(cmdout), '#'); + containerInfo.isRunning = strcmpi('running', strtrim(cmdout{1})); + volumes = regexprep(strtrim(cmdout{2}), '^\[|\]$', ''); + volumes = regexprep(volumes, ':\', ';\'); + volumePairs = strsplit(volumes, ':'); + volumePairs = cellfun(@(x) regexprep(x, ';\', ':\'), volumePairs, 'UniformOutput', 0); + containerInfo.volumes = reshape(volumePairs, 2, [])'; + tokens = regexp(cmdout{3}, 'sha256:[a-f0-9]+', 'match'); + containerInfo.imageSha = strtrim(tokens{1}); end end diff --git a/toolbox/core/bst_plugin.m b/toolbox/core/bst_plugin.m index 2509ae98a9..9b79bbc197 100644 --- a/toolbox/core/bst_plugin.m +++ b/toolbox/core/bst_plugin.m @@ -2673,7 +2673,10 @@ function Configure(PlugDesc) % Stop container if isContainer % Retrieve info of container - [~, ~, volumePairs] = bst_containers('GetContainerInfo', ['bst_' PlugDesc.Name]); + [errMsg, containerInfo] = bst_containers('GetContainerInfo', ['bst_' PlugDesc.Name]); + if ~isempty(errMsg) + return + end isOk = bst_containers('StopContainer', ['bst_' PlugDesc.Name], 1); % Delete temporary files file_delete(volumePairs{1,1}, 1, 1); From 491f974d67c9266031e35a17ff0d918131d79c2d Mon Sep 17 00:00:00 2001 From: rcassani Date: Fri, 24 Apr 2026 15:26:42 -0400 Subject: [PATCH 42/48] Update calls to `RunContainer` --- toolbox/core/bst_containers.m | 14 ++++---------- toolbox/core/bst_plugin.m | 8 ++++---- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/toolbox/core/bst_containers.m b/toolbox/core/bst_containers.m index 4497f68091..0f7bfd9d9d 100644 --- a/toolbox/core/bst_containers.m +++ b/toolbox/core/bst_containers.m @@ -5,7 +5,7 @@ % [errMsg, engineName] = bst_containers('GetEngine') % [errMsg, imageList] = bst_containers('GetImages') % [errMsg, imageSha] = bst_containers('ImportImage', imageSource, [imageTag]) -% [isOk, errMsg, containerName] = bst_containers('RunContainer', containerName, imageSha, [volumes], [isDaemon]) +% errMsg = bst_containers('RunContainer', containerName, imageSha, [volumes], [isDaemon]) % [isOk, cmdout] = bst_containers('ExecInContainer', containerName, cmdStr) % [isOk, cmdout] = bst_containers('StopContainer', containerName, [isForced=0]) % [isOk, cmdout] = bst_containers('RemoveImage', imageSha/Name, [isForced=0]) @@ -238,11 +238,8 @@ %% ===== RUN CONTAINER AS DAEMON ===== -function [isOk, errMsg, containerNameOut] = RunContainer(containerName, imageSha, volumes, isDaemon) -% USAGE: [isOk, errMsg, containerName] = bst_containers('RunContainer', containerName, imageSha, volumes, isDaemon) - isOk = 0; - containerNameOut = ''; - +function errMsg = RunContainer(containerName, imageSha, volumes, isDaemon) +% USAGE: errMsg = bst_containers('RunContainer', containerName, imageSha, volumes, isDaemon) % Validate inputs if nargin < 4 || isempty(isDaemon) isDaemon = 0; @@ -279,12 +276,9 @@ end [status, cmdout] = system(cmdStr); end - isOk = status == 0; - if ~isOk + if status ~= 0 errMsg = cmdout; - return end - containerNameOut = containerName; end diff --git a/toolbox/core/bst_plugin.m b/toolbox/core/bst_plugin.m index 9b79bbc197..fe4d08fbce 100644 --- a/toolbox/core/bst_plugin.m +++ b/toolbox/core/bst_plugin.m @@ -2507,16 +2507,16 @@ function Configure(PlugDesc) TmpDir = bst_get('BrainstormTmpDir', 0, PlugDesc.Name); volumes = {TmpDir, '/data'}; % Run container as daemon - [isOk, errMsg] = bst_containers('RunContainer', ['bst_' PlugDesc.Name], PlugDesc.ImageSha, volumes, 1); + errMsg = bst_containers('RunContainer', ['bst_' PlugDesc.Name], PlugDesc.ImageSha, volumes, 1); + if ~isempty(errMsg) + return + end else % Uninstall container plugin Uninstall(PlugDesc.Name, 0, 0); errMsg = ['Reinstall plugin ' PlugDesc.Name '.' 10 10 'Missing container image: ' imageName 10 'SHA: ' PlugDesc.ImageSha]; return end - if ~isOk - return - end end end % Do not modify path in compiled mode From c213afe05dc6cb87ebfd933d2ef5c83efc8e331c Mon Sep 17 00:00:00 2001 From: rcassani Date: Fri, 24 Apr 2026 15:31:40 -0400 Subject: [PATCH 43/48] Update call and code ExecInContainer --- toolbox/core/bst_containers.m | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/toolbox/core/bst_containers.m b/toolbox/core/bst_containers.m index 0f7bfd9d9d..17b204dc92 100644 --- a/toolbox/core/bst_containers.m +++ b/toolbox/core/bst_containers.m @@ -6,7 +6,7 @@ % [errMsg, imageList] = bst_containers('GetImages') % [errMsg, imageSha] = bst_containers('ImportImage', imageSource, [imageTag]) % errMsg = bst_containers('RunContainer', containerName, imageSha, [volumes], [isDaemon]) -% [isOk, cmdout] = bst_containers('ExecInContainer', containerName, cmdStr) +% [errMsg, cmdout] = bst_containers('ExecInContainer', containerName, cmdStr) % [isOk, cmdout] = bst_containers('StopContainer', containerName, [isForced=0]) % [isOk, cmdout] = bst_containers('RemoveImage', imageSha/Name, [isForced=0]) % [errMsg, containerInfo] = bst_containers('GetContainerInfo', containerName) @@ -283,8 +283,7 @@ %% ===== EXECUTE COMMAND IN CONTAINER ===== -function [isOk, cmdout] = ExecInContainer(containerName, cmdStr) - isOk = 0; +function [errMsg, cmdout] = ExecInContainer(containerName, cmdStr) cmdout = ''; % Check status of default container engine @@ -292,8 +291,7 @@ if ~isempty(errMsg) return end - - % Check container status + % Check if container is running [errMsg, containerInfo] = GetContainerInfo(containerName); if ~isempty(errMsg) || ~containerInfo.isRunning return @@ -302,13 +300,15 @@ % Run command switch engineName case 'docker' - commandWrapper = ''''; % Single quote if ispc - commandWrapper = '"'; % Double quote + commandWrapper = '"'; % Double quote + else + commandWrapper = ''''; % Single quote end [status, cmdout] = system(['docker exec ' containerName ' sh -c ' commandWrapper cmdStr commandWrapper]); - isOk = status == 0; - cmdout = strtrim(cmdout); + if status ~= 0 + errMsg = strtrim(cmdout); + end end end From eb5e9e320e0cb76883171958433a11315e2c6a34 Mon Sep 17 00:00:00 2001 From: rcassani Date: Fri, 24 Apr 2026 15:36:13 -0400 Subject: [PATCH 44/48] Update StopContainer --- toolbox/core/bst_containers.m | 13 +++++-------- toolbox/core/bst_plugin.m | 21 +++++++++++---------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/toolbox/core/bst_containers.m b/toolbox/core/bst_containers.m index 17b204dc92..332d011931 100644 --- a/toolbox/core/bst_containers.m +++ b/toolbox/core/bst_containers.m @@ -7,9 +7,9 @@ % [errMsg, imageSha] = bst_containers('ImportImage', imageSource, [imageTag]) % errMsg = bst_containers('RunContainer', containerName, imageSha, [volumes], [isDaemon]) % [errMsg, cmdout] = bst_containers('ExecInContainer', containerName, cmdStr) -% [isOk, cmdout] = bst_containers('StopContainer', containerName, [isForced=0]) % [isOk, cmdout] = bst_containers('RemoveImage', imageSha/Name, [isForced=0]) % [errMsg, containerInfo] = bst_containers('GetContainerInfo', containerName) +% errMsg = bst_containers('StopContainer', containerName, [isForced=0]) % @============================================================================= % This function is part of the Brainstorm software: @@ -358,10 +358,7 @@ %% ===== STOP CONTAINER ===== -function [isOk, cmdout] = StopContainer(containerName, isForce) - isOk = 0; - cmdout = ''; - +function errMsg = StopContainer(containerName, isForce) % Validate inputs if nargin < 2 || isempty(isForce) isForce = 0; @@ -383,10 +380,10 @@ % Kill [status, cmdout] = system(['docker rm -f ' containerName]); end - isOk = status == 0; - cmdout = strtrim(cmdout); + if status ~=0 + errMsg = strtrim(cmdout); + end end - end diff --git a/toolbox/core/bst_plugin.m b/toolbox/core/bst_plugin.m index fe4d08fbce..9810f8c992 100644 --- a/toolbox/core/bst_plugin.m +++ b/toolbox/core/bst_plugin.m @@ -2226,14 +2226,13 @@ function Configure(PlugDesc) end % Stop container if isContainer && ~isempty(PlugDesc.ImageSha) - isOk = bst_containers('StopContainer', ['bst_' PlugDesc.Name], 1); - if isOk - % Remove image from container engine - isOk = bst_containers('RemoveImage', ['brainstorm_' PlugDesc.Name], 1); - end - if ~isOk + errMsg = bst_containers('StopContainer', ['bst_' PlugDesc.Name], 1); + if ~isempty(errMsg) return end + % Remove image from container engine + isOk = bst_containers('RemoveImage', ['brainstorm_' PlugDesc.Name], 1); + end @@ -2677,12 +2676,14 @@ function Configure(PlugDesc) if ~isempty(errMsg) return end - isOk = bst_containers('StopContainer', ['bst_' PlugDesc.Name], 1); - % Delete temporary files - file_delete(volumePairs{1,1}, 1, 1); - if ~isOk + errMsg = bst_containers('StopContainer', ['bst_' PlugDesc.Name], 1); + if ~isempty(errMsg) return end + % Delete temporary files + for iVolume = 1 : size(containerInfo.volumes, 1) + file_delete(containerInfo.volumes{iVolume,1}, 1, 1); + end end end From 28400b361179c4c44b6a52acf05093bc5e29e2ef Mon Sep 17 00:00:00 2001 From: rcassani Date: Fri, 24 Apr 2026 15:54:35 -0400 Subject: [PATCH 45/48] Update `RemoveImage` --- toolbox/core/bst_containers.m | 12 +++++------- toolbox/core/bst_plugin.m | 12 ++++-------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/toolbox/core/bst_containers.m b/toolbox/core/bst_containers.m index 332d011931..df9d6e569d 100644 --- a/toolbox/core/bst_containers.m +++ b/toolbox/core/bst_containers.m @@ -7,9 +7,9 @@ % [errMsg, imageSha] = bst_containers('ImportImage', imageSource, [imageTag]) % errMsg = bst_containers('RunContainer', containerName, imageSha, [volumes], [isDaemon]) % [errMsg, cmdout] = bst_containers('ExecInContainer', containerName, cmdStr) -% [isOk, cmdout] = bst_containers('RemoveImage', imageSha/Name, [isForced=0]) % [errMsg, containerInfo] = bst_containers('GetContainerInfo', containerName) % errMsg = bst_containers('StopContainer', containerName, [isForced=0]) +% errMsg = bst_containers('RemoveImage', imageSha/Name, [isForced=0]) % @============================================================================= % This function is part of the Brainstorm software: @@ -388,10 +388,7 @@ %% ===== REMOVE IMAGE ===== -function [isOk, cmdout] = RemoveImage(imageSha, isForce) - isOk = 0; - cmdout = ''; - +function errMsg = RemoveImage(imageSha, isForce) % Validate inputs if nargin < 2 || isempty(isForce) isForce = 0; @@ -413,8 +410,9 @@ % Force remove image [status, cmdout] = system(['docker rmi -f ' imageSha]); end - isOk = status == 0; - cmdout = strtrim(cmdout); + if status ~=0 + errMsg = strtrim(cmdout); + end end end diff --git a/toolbox/core/bst_plugin.m b/toolbox/core/bst_plugin.m index 9810f8c992..4164496b05 100644 --- a/toolbox/core/bst_plugin.m +++ b/toolbox/core/bst_plugin.m @@ -2224,15 +2224,12 @@ function Configure(PlugDesc) end quit('force'); end - % Stop container + % Remove image from container engine if isContainer && ~isempty(PlugDesc.ImageSha) - errMsg = bst_containers('StopContainer', ['bst_' PlugDesc.Name], 1); + errMsg = bst_containers('RemoveImage', ['brainstorm_' PlugDesc.Name], 1); if ~isempty(errMsg) return end - % Remove image from container engine - isOk = bst_containers('RemoveImage', ['brainstorm_' PlugDesc.Name], 1); - end @@ -2673,10 +2670,9 @@ function Configure(PlugDesc) if isContainer % Retrieve info of container [errMsg, containerInfo] = bst_containers('GetContainerInfo', ['bst_' PlugDesc.Name]); - if ~isempty(errMsg) - return + if isempty(errMsg) + errMsg = bst_containers('StopContainer', ['bst_' PlugDesc.Name], 1); end - errMsg = bst_containers('StopContainer', ['bst_' PlugDesc.Name], 1); if ~isempty(errMsg) return end From 6f9aae5fe80885f5d71d7419063dbfeda9bb3d39 Mon Sep 17 00:00:00 2001 From: rcassani Date: Fri, 24 Apr 2026 16:04:29 -0400 Subject: [PATCH 46/48] Further clean up --- toolbox/core/bst_containers.m | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/toolbox/core/bst_containers.m b/toolbox/core/bst_containers.m index df9d6e569d..9df20b9b35 100644 --- a/toolbox/core/bst_containers.m +++ b/toolbox/core/bst_containers.m @@ -56,8 +56,8 @@ end % Tests container engines + isFound = 0; for iEngine = 1 : length(engineNames) - isFound = 0; switch engineNames{iEngine} case {'docker'} if ispc @@ -230,10 +230,6 @@ return end end - isOk = status == 0; - if ~isOk - return - end end @@ -260,7 +256,7 @@ nPairs = size(volumes, 1); pairs = cell(nPairs, 1); for iPair = 1 : nPairs - pairs{iPair} = ['-v' volumes{iPair, 1} ':' volumes{iPair, 2}]; + pairs{iPair} = ['-v ' volumes{iPair, 1} ':' volumes{iPair, 2}]; end volumesStr = strjoin(pairs, ' '); end @@ -269,6 +265,7 @@ switch engineName case 'docker' if ~isDaemon + % Run ENTRYPOINT cmdStr = sprintf('docker run --rm --name %s %s %s', containerName, volumesStr, imageSha); else % Replace ENTRYPOINT (if any) with `sleep infinity` From e23603e9dfbc911dd1bfc24f9f5cc6e72a00beb1 Mon Sep 17 00:00:00 2001 From: rcassani Date: Wed, 29 Apr 2026 15:11:30 -0400 Subject: [PATCH 47/48] Load: Run container if it does not exist or is not running --- toolbox/core/bst_plugin.m | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/toolbox/core/bst_plugin.m b/toolbox/core/bst_plugin.m index fac03db9e8..81bc189556 100644 --- a/toolbox/core/bst_plugin.m +++ b/toolbox/core/bst_plugin.m @@ -2486,7 +2486,7 @@ function Configure(PlugDesc) if isContainer PlugDesc = GetInstalled(PlugDesc); imageName = ['brainstorm_' PlugDesc.Name]; - if ~isempty(PlugDesc.ImageSha) && ~PlugDesc.isLoaded + if ~isempty(PlugDesc.ImageSha) % Get available images in container engine [errMsg, imageList] = bst_containers('GetImages'); if ~isempty(errMsg) @@ -2499,13 +2499,22 @@ function Configure(PlugDesc) isImported = any(strncmpi(imageList(iImageSha,1), imageName, length(imageName))); end if isImported - % Get tmp dir to bind container - TmpDir = bst_get('BrainstormTmpDir', 0, PlugDesc.Name); - volumes = {TmpDir, '/data'}; - % Run container as daemon - errMsg = bst_containers('RunContainer', ['bst_' PlugDesc.Name], PlugDesc.ImageSha, volumes, 1); - if ~isempty(errMsg) - return + % Check if container exist + [~, containerInfo] = bst_containers('GetContainerInfo', ['bst_' PlugDesc.Name]); + % Run container + if ~containerInfo.isRunning + % Remove container + if ~isempty(containerInfo.name) + bst_containers('StopContainer', containerInfo.name, 1); + end + % Get tmp dir to bind container + TmpDir = bst_get('BrainstormTmpDir', 0, PlugDesc.Name); + volumes = {TmpDir, '/data'}; + % Run container as daemon + errMsg = bst_containers('RunContainer', ['bst_' PlugDesc.Name], PlugDesc.ImageSha, volumes, 1); + if ~isempty(errMsg) + return + end end else % Uninstall container plugin From 587d04db15c5ab2dd1f5291b39edac4954b3518a Mon Sep 17 00:00:00 2001 From: rcassani Date: Wed, 29 Apr 2026 15:29:49 -0400 Subject: [PATCH 48/48] Stop containers on exiting Brainstorm --- toolbox/core/bst_exit.m | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/toolbox/core/bst_exit.m b/toolbox/core/bst_exit.m index f3e2bd8468..bd2fda637a 100644 --- a/toolbox/core/bst_exit.m +++ b/toolbox/core/bst_exit.m @@ -1,6 +1,6 @@ function status = bst_exit() % BST_EXIT: Exit Brainstorm -% Save database, remove callbacks, close windows, reset environment. +% Save database, remove callbacks, close windows, reset environment, stop containers % % Return : 1 if exited % 0 if Brainstorm was not started @@ -99,6 +99,15 @@ bst_mutex('release', 'Brainstorm'); +%% ===== STOP CONTAINERS ===== +PlugDesc = bst_plugin('GetInstalled'); +for iPlug = 1 : length(PlugDesc) + if bst_plugin('IsContainer', PlugDesc(iPlug)) + bst_plugin('Unload', PlugDesc(iPlug), 0); + end +end + + %% ===== SAVE DATABASE ===== db_save(1); % Close file to indicate that Brainstorm was started