Manual:BoxedCommand

From mediawiki.org
Jump to navigation Jump to search
MediaWiki version:
1.36
Gerrit change 626548

BoxedCommand is a part of the Shellbox library that allows for transparently executing shell commands either locally (like normal) or on a remote server running the Shellbox server component. This page covers how to use it inside MediaWiki, as it mostly replaces the Shell framework introduced in 1.30.

A lightly edited and annotated example from Extension:SyntaxHighlight:

// Obtain a BoxedCommand
$command = MediaWikiServices::getInstance()->getShellCommandFactory()
    // Route to $wgShellboxUrls['syntaxhighlight']
	->createBoxed( 'syntaxhighlight' )
	// Disable network access
	->disableNetwork()
	// Use firejail's default seccomp filter
	->firejailDefaultSeccomp()
	->routeName( 'syntaxhighlight-pygments' );
// Construct and execute the command
$result = $command
    // Build parameters, automatically escaped
	->params(
		'/usr/bin/pygmentize',
		'-l', $lexer,
		'-f', 'html',
		'-O', implode( ',', $optionPairs ),
		'file'
	)
	// Create a file named "file" with contents of $code
	->inputFileFromString( 'file', $code )
	// Run the command!
	->execute();
// Interpret the result
$output = $result->getStdout();
// Check if exit code indicates failure
if ( $result->getExitCode() != 0 ) {
	throw new PygmentsException( $output );
}
return $output;

Overview[edit]

BoxedCommand allows executing commands in a sandbox, including possibly on an entirely different server. Commands are executed in an empty temporary directory, so any files need to be copied in using the BoxedCommand functions. Regardless of what sandboxing method sysadmins have configured, BoxedCommand aims to transparently make things just work.

Getting a BoxedCommand[edit]

A ShellCommandFactory can be obtained from MediaWikiServices, then call createBoxed(). The argument to createBoxed() should be the name of the Shellbox configured in $wgShellboxUrls . Typically each extension will use its own name here, unless there is an advantage to further isolate commands from each other by using separate Shellboxes.

There are various additional hardening measures that can be set at this point such as enabling firejail's default seccomp filter (firejailDefaultSeccomp()) or disabling network access (disableNetwork()). They will only be used if the sandboxing system supports them.

Finally, set a route name (TODO: this doesn't really do anything).

It is common for extensions to have a static function named boxedCommand() that returns a configured BoxedCommand object:

private static function boxedCommand(): BoxedCommand {
	return MediaWikiServices::getInstance()->getShellCommandFactory()
		->createBoxed( 'syntaxhighlight' )
		->disableNetwork()
		->firejailDefaultSeccomp()
		->routeName( 'syntaxhighlight-pygments' );
}

Constructing the command[edit]

Parameters can be passed by calling params(). These will be automatically escaped, raw parameters can be passed by using unsafeParams(). The utility function Shellbox::escape() allows for escaping user-input before passing it to unsafeParams(). Environment variables can be passed as an array to environment().

If you want stderr to be merged with stdout, call includeStderr().

Many commands deal with files, so Shellbox makes it convenient to do so with the following helper functions:

  • inputFileFromString( $boxedName, $contents ): Creates a file with the name $boxedName with the contents $contents before execution
  • inputFileFromFile( $boxedName, $path ): Same as inputFileFromString, but reads the contents from $path
  • outputFileToString( $boxedName ): If it exists, save the file with name $boxedName into memory after execution
  • outputFileToFile( $boxedName, $path ): Same as outputFileToString, except it saves the contents to $path
  • outputGlobToString( $prefix, $extension ): Similar to outputFileToString, except it saves everything that matches the glob of ${prefix}*.${extension}
  • outputGlobToFile( $prefix, $extension, $dir ): Similar to outputGlobToString, except all files are saved into the directory $dir

Standard input can be passed by using stdin( $input ).

Using a shell wrapper[edit]

It is common to run external commands in pipeline. For example, Score will run abc2ly to convert ABC markup to LilyPond and then generate images using lilypond from the LilyPond markup. Traditionally this would have been two Shell::command() invocations, one for abc2ly, then another for lilypond using the output from abc2ly. However, this can cause unnecessary overhead when using a remote Shellbox server, since file contents need to be sent back and forth as HTTP requests unnecessarily. One practice is to use a shell script to run multiple commands, possibly interpreting the output.

Take a look at Score's scripts/ directory for examples on how this works.

Interpreting the result[edit]

Calling $command->execute() will give you a BoxedResult object. The following methods are useful:

  • getExitCode()
  • getStderr(): null if includeStderr() was used
  • getStdout()
  • getFileContents( $name ): if an output file was registered with outputFileToString, get that file's contents
  • wasReceived( $name ): whether that file was received

Examples[edit]