diff --git a/README.md b/README.md index 923f57a..620311b 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ The following actions are available : - Stop - Restart - Exec shell +- Set a custom shell command ## Screenshot diff --git a/src/docker.js b/src/docker.js index 1271b23..2af3ee8 100644 --- a/src/docker.js +++ b/src/docker.js @@ -24,6 +24,8 @@ const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); const Utils = Me.imports.src.utils; +var customShellCommandsToContainers = {}; + /** * Dictionary for Docker actions * @readonly @@ -75,8 +77,12 @@ const getDockerActionCommand = (dockerAction, containerName) => { case DockerActions.REMOVE: return "docker rm " + containerName; case DockerActions.OPEN_SHELL: - return "docker exec -it " + containerName + " /bin/bash; " - + "if [ $? -ne 0 ]; then docker exec -it " + containerName + " /bin/sh; fi;"; + let customCommand = customShellCommandsToContainers[containerName]; + customCommand = customCommand.split('"').join('\\\\\\"'); + customCommand = customCommand ? ` -c "${customCommand}"` : '' + + return "docker exec -it " + containerName + " /bin/bash" + customCommand + "; " + + "if [ $? -ne 0 ]; then docker exec -it " + containerName + " /bin/sh" + customCommand + "; fi;"; case DockerActions.RESTART: return "docker restart " + containerName; case DockerActions.PAUSE: diff --git a/src/dockerSubMenuMenuItem.js b/src/dockerSubMenuMenuItem.js index e06c19a..27b258b 100644 --- a/src/dockerSubMenuMenuItem.js +++ b/src/dockerSubMenuMenuItem.js @@ -27,6 +27,7 @@ const Me = ExtensionUtils.getCurrentExtension(); const DockerActions = Me.imports.src.docker.DockerActions; const DockerMenuItem = Me.imports.src.dockerMenuItem; const Utils = Me.imports.src.utils; +const Docker = Me.imports.src.docker; /** * Create a St.Icon @@ -58,6 +59,10 @@ var DockerSubMenuMenuItem = class DockerSubMenuMenuItem extends PopupMenu.PopupS _init(containerName, containerStatusMessage) { super._init(containerName); + Utils.getCustomShellCommandsToContainersFromStorage((res) => { + Docker.customShellCommandsToContainers = res; + }); + switch (getStatus(containerStatusMessage)) { case "stopped": this.actor.insert_child_at_index(createIcon('process-stop-symbolic', 'status-stopped'), 1); @@ -70,6 +75,25 @@ var DockerSubMenuMenuItem = class DockerSubMenuMenuItem extends PopupMenu.PopupS this.menu.addMenuItem(new DockerMenuItem.DockerMenuItem(containerName, DockerActions.RESTART)); this.menu.addMenuItem(new DockerMenuItem.DockerMenuItem(containerName, DockerActions.PAUSE)); this.menu.addMenuItem(new DockerMenuItem.DockerMenuItem(containerName, DockerActions.STOP)); + + const customShellCommandButton = new PopupMenu.PopupMenuItem('Set custom shell command'); + + this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + this.menu.addMenuItem(customShellCommandButton); + + customShellCommandButton.connect('activate', () => { + const textEntryProps = { + title: 'Set a custom shell command', + text: `Insert here a command to be executed when opening the \\"${containerName}\\" shell.`, + entryText: Docker.customShellCommandsToContainers[containerName], + }; + + Utils.openWindowTextEntry(textEntryProps, (customCommand) => { + Docker.customShellCommandsToContainers[containerName] = customCommand; + Utils.saveCustomShellCommandsToContainers(Docker.customShellCommandsToContainers); + }); + }); + break; case "paused": this.actor.insert_child_at_index(createIcon('media-playback-pause-symbolic', 'status-paused'), 1); diff --git a/src/utils.js b/src/utils.js index cba81e0..3d9dda3 100644 --- a/src/utils.js +++ b/src/utils.js @@ -18,6 +18,7 @@ 'use strict'; +const Gio = imports.gi.Gio; const GLib = imports.gi.GLib; const Config = imports.misc.config; @@ -35,3 +36,129 @@ var isGnomeShellVersionLegacy = () => { * @param {Function} callback The callback to call after fn */ var async = (fn, callback) => GLib.timeout_add(GLib.PRIORITY_DEFAULT, 0, () => callback(fn())); + +/** + * Run a command asynchronously + * @param {string} commandLine Command to execute + * @param {(output: string) => void} callback The callback to call after shell return an output + */ +var spawnCommandLineAsync = (commandLine, callback) => { + function readOutput(stream, lineBuffer) { + stream.read_line_async(0, null, (stream, res) => { + try { + let line = stream.read_line_finish_utf8(res)[0]; + + if (line !== null) { + lineBuffer.push(line); + readOutput(stream, lineBuffer); + } + } catch (e) { + logError(e); + } + }); + } + + try { + let [, pid, stdin, stdout, stderr] = GLib.spawn_async_with_pipes( + null, + ['/bin/sh', '-c', `${commandLine}`], + null, + GLib.SpawnFlags.SEARCH_PATH | GLib.SpawnFlags.DO_NOT_REAP_CHILD, + null + ); + + GLib.close(stdin); + + let stdoutStream = new Gio.DataInputStream({ + base_stream: new Gio.UnixInputStream({ + fd: stdout, + close_fd: true + }), + close_base_stream: true + }); + + let stdoutLines = []; + readOutput(stdoutStream, stdoutLines); + + let stderrStream = new Gio.DataInputStream({ + base_stream: new Gio.UnixInputStream({ + fd: stderr, + close_fd: true + }), + close_base_stream: true, + }); + + let stderrLines = []; + readOutput(stderrStream, stderrLines); + + GLib.child_watch_add(GLib.PRIORITY_DEFAULT_IDLE, pid, (pid, status) => { + if (status === 0) { + callback(stdoutLines.join('\n')); + } else { + logError(new Error(stderrLines.join('\n'))); + } + + stdoutStream.close(null); + stderrStream.close(null); + GLib.spawn_close_pid(pid); + }); + } catch(err) { + logError(err); + } +} + +/** + * Creates/changes a file + * @param {string} filename Filename of the file that will be created/changed + * @param {string} content Content that will be written inside the file that will be created/changed + */ +var writeFile = (filename, content = '') => { + if (!filename) { + throw new Error('The filename is a required parameter'); + } + + content = content.split('"').join('\\\\\\"'); + + GLib.spawn_command_line_async(`/bin/sh -c "echo \\"${content}\\" > ${filename}"`); +} + +/** + * Get custom shell commands to containers from storage + * @param {(customShellCommandsToContainers: {[containerName: string]: string}) => void} callback The callback to call after get the custom shell commands + */ +var getCustomShellCommandsToContainersFromStorage = (callback) => { + spawnCommandLineAsync( + 'cat ~/.dockerIntegrationCustomShellCommandsToContainers.json', + (res) => { + const customShellCommandsToContainers = JSON.parse(res); + callback(customShellCommandsToContainers); + } + ); +} + +/** + * Save custom shell commands to containers in storage + * @param {{[containerName: string]: string}} customShellCommandsToContainers The custom shell commands to save in storage + */ +var saveCustomShellCommandsToContainers = (customShellCommandsToContainers) => { + writeFile( + '~/.dockerIntegrationCustomShellCommandsToContainers.json', + JSON.stringify(customShellCommandsToContainers) + ); +} + +/** + * Opens a window with an input + * @param {{title?: string, text?: string, entryText?: string}} textEntryProps The properties of the window elements + * @param {(inputValue: string) => void} callback The callback to call after the user filled the input + */ +var openWindowTextEntry = (textEntryProps, callback) => { + const { title = '', text = '', entryText = '' } = textEntryProps || {}; + + const script = 'zenity --entry ' + + `--title="${title}" ` + + `--text="${text}" ` + + `--entry-text "${entryText}"`; + + spawnCommandLineAsync(script, callback); +}