-
Notifications
You must be signed in to change notification settings - Fork 32
08 How does terminal work
- Introduction
- class Base
- class LinuxSessionWrapper
- class MetaShell
- class Shell
- class Log
- class SpawnWrapper
- Summary
This module was written and tested in JavaScript. If you prefer CoffeeScript, you can use coffee
to translate the module. I used JavaScript because that is what I am comfortable with.
terminal
is a module that I created for the atom-python-run module. Outside of this use, I see no real need or purpose for it. It determines the Operating System and sets a default value for the type of CLI, CLI execution option, and passes user defined arguments to be executed by the ChildProcess
instance.
The whole point of this is to automate the process of creating the right type of ChildProcess
for a given system. I wanted atom-python-run
to be able to execute on any system I might use. This allows me to use this module on Windows, Mac OS X, and Linux based Operating Systems that support the Atom text editor.
atom-python-run
executes your code in a Command Line Interface instance. Different Operating Systems use different CLI's. These CLI's might even require different commands. The code is executed and then paused by the cp
module. terminal
optionally logs stdout
and stderr
to the console.log
.
I created classes based on functions that were grouped together by all working on the same object. Each object has a single function and outputs that object based on its input. The key to understanding how terminal
works is to realize it has cascading side-effects. This will become clear later on. The easiest way to explain what terminal actually does is to just go through it and break it down.
This is a special class and contains meta data used for a generic property called object
. It has a method set()
and a method get()
. The constructor
uses Base
's built-in set()
method. The isEmpty()
method is used to determine whether or not the object
itself is empty. This will be our base, or root, class. All other classes inherent this class.
- base - the base object
- source - a list of objects to be merged with
base
class Base {
constructor(base, ...source){
this.set(base, ...source)
}
get() {
return this.object;
}
set(base, ...source) {
if (null == base) {
this.object = new Object();
} else if (null == source) {
this.object = Object.assign({}, base);
} else {
this.object = Object.assign(base, ...source);
}
}
isEmpty() {
for(var property in this.object) {
if(this.object.hasOwnProperty(property)) {
return false;
}
}
return JSON.stringify(this.object) === JSON.stringify({});
}
}
Now that we have our Base
defined, we can test it out in node
. Open a CLI instance, go to the working directory where terminal.js
is located, and start nodejs
.
$ pwd
/home/myUserName/.atom/packages/atom-python-run/lib
$ node
>
> terminal = require('./terminal')
{ Base: [Function: Base],
LinuxSessionWrapper: [Function: LinuxSessionWrapper],
MetaShell: [Function: MetaShell],
Shell: [Function: Shell],
Log: [Function: Log],
SpawnWrapper: [Function: SpawnWrapper] }
>
> base = new terminal.Base()
Base { object: {} }
It is literally what it looks like. Instead of repeating my code every where, I encapsulated the common operations within this class. I needed to be able to set objects and retrieve objects. These objects are stored as the generic property named object.
> base.set({'shell':'gnome-terminal'})
undefined
> base.get()
{ shell: 'gnome-terminal' }
> base.object
{ shell: 'gnome-terminal' }
You can pass as many objects as necessary. It's important to note that if you modify a object, you need to copy it and then append a new object to it; Otherwise it will be lost.
> base.set({'shell':'gnome-terminal'})
undefined
> base.get()
{ shell: 'gnome-terminal' }
> base.set({'option': '-x'})
undefined
> base.get()
{ option: '-x' }
In order to preserve the object, I did this instead.
> base.set(base.get(), {'option':['-x']})
undefined
> base.get()
{ shell: 'gnome-terminal', option: [ '-x' ] }
Obviously, I could have done something simpler like this.
> base.object.call = 'python'
'python'
> base.object
{ shell: 'gnome-terminal', option: [ '-x' ], call: 'python' }
Regardless, in all instances, it's the same end result. I create a new Object
instance and create a copy of any object passed to any Base
instance. The Base
instance is an abstract representation of what ever it may be. This allowed me to create objects on the fly in a more coherent and consistent manner.
I did this because I wanted each of the classes to have one object as its input and output. This allowed me to use arbitrary key/value pairs for each class which would ultimately only share the object passed between them.
Everything else is accessible of course; however, this is unnecessary since each object instance is taken care of by its constructor. All that's required it to retrieve the object and pass it to the next one without ever having to explicitly call it.
This object determines the default shell, and the default shell option, by determining the users current desktop environment (or session) for Linux. This object applies to the Linux context alone.
None
class LinuxSessionWrapper extends Base {
constructor() {
super();
let shell = this.setupDefaultShell();
let option = this.setupDefaultOption(shell);
this.set({
'shell': shell,
'option': option
});
};
getSessionTerminal() {
switch (process.env.GDMSESSION) {
case 'ubuntu':
case 'ubuntu-2d':
case 'gnome':
case 'gnome-shell':
case 'gnome-classic':
case 'gnome-fallback':
case 'cinnamon':
return "gnome-terminal";
case 'xfce':
return "xfce4-terminal";
case 'kde-plasma':
return "konsole";
case 'Lubuntu':
return "lxterminal";
default:
return null;
}
}
getDesktopTerminal() {
switch (process.env.XDG_CURRENT_DESKTOP) {
case 'Unity':
case 'GNOME':
case 'X-Cinnamon':
return "gnome-terminal";
case 'XFCE':
return "xfce4-terminal";
case 'KDE':
return "konsole";
case 'LXDE':
return "lxterminal";
default:
return "xterm";
}
}
setupDefaultShell() {
let shell = this.getSessionTerminal();
if (null === shell) {
shell = this.getDesktopTerminal();
}
return shell;
}
setupDefaultOption(shell) {
// determine the shells default execution option
switch (shell) {
case 'gnome-terminal':
case 'xfce4-terminal':
return "-x";
case 'konsole':
case 'lxterminal':
default:
return "-e";
}
}
};
The environment variable GDMSESSION
is checked first. If that does NOT yield a value, the environment variable XDG_CURRENT_DESKTOP
is checked. If that fails as well, the default graphical shell, xterm
, is used instead.
The value yielded from the shell
is then used to determine the option. These values are then set()
to its built-in object
property for later access.
$ node
>
> terminal = require('./terminal')
{ Base: [Function: Base],
LinuxSessionWrapper: [Function: LinuxSessionWrapper],
MetaShell: [Function: MetaShell],
Shell: [Function: Shell],
Log: [Function: Log],
SpawnWrapper: [Function: SpawnWrapper] }
>
> session = new terminal.LinuxSessionWrapper()
LinuxSessionWrapper { object: { shell: 'gnome-terminal', option: '-x' } }
>
> session.object
{ shell: 'gnome-terminal', option: '-x' }
The only reason this object exists is to assist in determining the default shell
and default execution option
for that shell
. There are no arguments passed to the constructor
and the constructor
takes care of the generic object
s creation.
This object is a blueprint for the Shell
class. MetaShell
is a special meta class that helps automate the construction of metadata used to create a terminal instance.
- base
- source
class MetaShell extends Base {
constructor(base, ...source) {
super();
this.setBase(base, ...source);
this.setPath(base);
this.setCommand();
}
setBase(base, ...source) {
this.set({
'shell': base.shell,
'option': base.option,
'call': base.call
}, ...source);
}
setPath(base) {
function defaultPath() {
if ('win32' === process.platform) {
return `${process.env.USERPROFILE}\\.atom\\packages\\atom-python-run\\cp\\main.py`;
}
return `${process.env.HOME}/.atom/packages/atom-python-run/cp/main.py`;
}
this.set(this.get(), {
'script': (function(path) {
if (null == path) {
path = defaultPath();
}
return path;
})(base.script)
});
}
setCommand() {
let object = this.get();
this.set(object, {
'command': function (args) {
let path = `${object.call} ${object.script}`;
for (let token of args) {
path += ` ${token}`;
}
return `tell app "Terminal" to do script "${path}"`
}
});
}
}
$ node
>
> terminal = require('./terminal')
{ Base: [Function: Base],
LinuxSessionWrapper: [Function: LinuxSessionWrapper],
MetaShell: [Function: MetaShell],
Shell: [Function: Shell],
Log: [Function: Log],
SpawnWrapper: [Function: SpawnWrapper] }
>
> session = new terminal.LinuxSessionWrapper()
LinuxSessionWrapper { object: { shell: 'gnome-terminal', option: '-x' } }
>
> meta = new terminal.MetaShell(session.object, {'call': 'python'})
MetaShell {
object:
{ shell: 'gnome-terminal',
option: '-x',
call: 'python',
script: '/home/th3ros/.atom/packages/atom-python-run/cp/main.py',
command: [Function: command] } }
It's important to note that this meta.object
is useless on it's own. It is literally meta data that is used only by the SpawnWrapper
class. This is explained best while summing everything up later on.
While this object can be changed once instantiated by utilizing the setBase()
method, it's typically best to pass any arguments to the constructor
instead.
The MetaShell
object requires at least 3 key/value pairs. The other 2 are optional.
All MetaShell
objects have 5 properties:
-
shell
The terminal instance to be created -
option
The terminal instance execution option -
call
A REPL or Interpreter that executes 'script' -
script
A program to execute given arguments within a 'shell' -
command
The commands to be executed by 'script' (Mac OS X Only)
script
is created by either the user or by terminal
. If a path is undefined, it defaults to using an expected absolute path
. Otherwise is uses the user defined path
instead.
command
is constructed by an anonymous function that is only accessible through a MetaShell.command
method call. This call will yield a string that can be passed to osascript
. This is Mac OS X specific, but may work on another system that can execute osascript
itself; I'm assuming since I haven't test this theory my self.
i.e. :
$ shell option call script
translates to
$ gnome-terminal -x python script.py
The meta.object
is used to construct meta objects through the use of a Shell
object. We will cover what the Shell
object does in detail next.
The Shell
class creates a Shell.object
according to its local environment. A path
can be passed to the constructor
, appropriate method according to the local environment, or by manually invoking the setUp()
method which determines the local environment automatically.
-
path
A 'String' containing a path to a executable
class Shell extends Base {
constructor(path) {
super();
this.set(this.setUp(path));
}
win32(path) {
return new MetaShell({
'shell': process.env.COMSPEC,
'option': ['/c', 'start'],
'call': 'python',
'script': path
});
}
darwin(path) {
return new MetaShell({
'shell': 'osascript',
'option': ['-e'],
'call': 'python',
'script': path
});
}
linux(path) {
let session = new LinuxSessionWrapper();
return new MetaShell({
'shell': session.get().shell,
'option': [session.get().option],
'call': 'python',
'script': path
});
}
setUp(path) {
switch (process.platform) {
case 'win32':
return this.win32(path).object;
case 'darwin':
return this.darwin(path).object;
case 'linux':
return this.linux(path).object;
default:
return null;
}
}
}
> terminal = require('./terminal')
{ Base: [Function: Base],
LinuxSessionWrapper: [Function: LinuxSessionWrapper],
MetaShell: [Function: MetaShell],
Shell: [Function: Shell],
Log: [Function: Log],
SpawnWrapper: [Function: SpawnWrapper] }
>
> shell = new terminal.Shell()
Shell {
object:
{ shell: 'gnome-terminal',
option: [ '-x' ],
call: 'python',
script: '/home/th3ros/.atom/packages/atom-python-run/cp/main.py',
command: [Function: command] } }
Notice here how we do NOT have to pass any parameters to the terminal.Shell
call. Also note how the path
parameter is optional as well. script
has its own path value built-in. However, we can always change the path value.
> shell = new terminal.Shell('/some/path/to/some/executable')
Shell {
object:
{ shell: 'gnome-terminal',
option: [ '-x' ],
call: 'python',
script: '/some/path/to/some/executable',
command: [Function: command] } }
Shell
is one of only 2 objects that are required. SpawnWrapper
is the other required object. Shell
creates a meta object
that contains meta data to be passed to the child_process.spawn
method call.
On it's own, it does nothing. However, it contains basic required information to spawn a CLI instance for any particular OS depending on the system it's executed in.
Automate the logging process while executing in Atom. Creates a Log.object
to capture and print basic I/O operations to stdout
.
- base
- source
class Log extends Base {
constructor(base, ...source) {
super();
this.set(base, ...source)
}
metaData() {
console.log(
`platform: ${process.platform}\n` +
`shell: ${this.object.meta.shell}\n` +
`option: ${this.object.meta.option}\n` +
`call: ${this.object.meta.call}\n` +
`script: ${this.object.meta.script}\n` +
`command: ${this.object.meta.command(this.get().args)}`
);
}
params() {
console.log(
`options: ${JSON.stringify(this.object.options, null, 4)}\n` +
`args: ${this.object.args}\n` +
`cp: ${this.object.cp}`
);
}
tty() {
this.object.tty.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});
this.object.tty.stderr.on('data', (data) => {
console.log(`stderr: ${data}`);
});
this.object.tty.on('close', (code) => {
console.log(`child process exited with code ${code}`);
});
}
call() {
this.metaData();
this.params();
this.tty();
}
}
A Log call will require an object during instantiation with the following format:
-
tty
Requires a 'ChildProcess' instance -
options
The 'options' object passed to 'spawn()' -
args
The parameters to be executed by the cli instance -
cp
A 'Boolean' value that determines a cli instance call -
meta
The meta data to be passed to a cli instance call
> terminal = require('./terminal')
{ Base: [Function: Base],
LinuxSessionWrapper: [Function: LinuxSessionWrapper],
MetaShell: [Function: MetaShell],
Shell: [Function: Shell],
Log: [Function: Log],
SpawnWrapper: [Function: SpawnWrapper] }
>
> shell = new terminal.Shell()
Shell {
object:
{ shell: 'gnome-terminal',
option: [ '-x' ],
call: 'python',
script: '/home/th3ros/.atom/packages/atom-python-run/cp/main.py',
command: [Function: command] } }
>
> log = new terminal.Log({'meta': shell.object}, {'args': ['python', '/path/to/hello.py']})
Log {
object:
{ meta:
{ shell: 'gnome-terminal',
option: [Object],
call: 'python',
script: '/home/th3ros/.atom/packages/atom-python-run/cp/main.py',
command: [Function: command] },
args: [ 'python', '/path/to/hello.py' ] } }
>
> log.metaData()
platform: linux
shell: gnome-terminal
option: -x
call: python
script: /home/th3ros/.atom/packages/atom-python-run/cp/main.py
command: tell app "Terminal" to do script "python /home/th3ros/.atom/packages/atom-python-run/cp/main.py python /path/to/hello.py"
undefined
The meta
property should contain a Shell
instance object because the metaData()
method expects specific properties to be available. This object should be used ONLY after spawn
has executed and a tty instance has been created.
It's important to note that at least one key/value pair is required to create a valid Log instance, but will ultimately fail once the call()
method is executed.
This object is used by the SpawnWrapper
object to log spawn
s execution to Atoms console.log
.
SpawnWrapper
class creates a child_process.spawn()
wrapper object. A Shell.object
is passed to SpawnWrapper
during instantiation. The Shell.tty()
method is used to create a ChildProcess
object.
- base
- source
class SpawnWrapper extends Base {
constructor(base, ...source) {
super();
this.set(base, ...source);
}
spawnWin32(model) {
function cp(object, ...args) {
if (model.cp) {
return [...object.option, object.call, object.script, ...args];
} else {
return [...object.option, ...args];
}
}
return spawn(
this.object.shell,
[...cp(this.object, ...model.args)],
model.options
)
}
spawnDarwin(model) {
// darwin should never be modified or configured in any way. doing
// so risks breaking the required tool chain.
return spawn(
this.object.shell,
[ ...this.object.option,
this.object.command(...model.args)
],
model.options
);
}
spawnWrapper(model) {
switch (process.platform) {
case 'linux':
case 'win32':
return this.spawnWin32(model);
case 'darwin':
return this.spawnDarwin(model);
default:
return null;
}
}
optionWrapper(options) {
function isEmpty(object) {
for(var property in object) {
if(object.hasOwnProperty(property)) {
return false;
}
}
return JSON.stringify(object) === JSON.stringify({});
}
if (isEmpty(options)) {
options = {
'cwd': undefined,
'detached': false
};
}
return options;
}
tty(model) {
model.options = this.optionWrapper(model.options);
let tty = this.spawnWrapper(model);
if (model.log) {
new Log({
'tty': tty,
'args': model.args,
'options': model.options,
'cp': model.cp,
'meta': this.object
}).call();
}
return tty;
}
}
> terminal = require('./terminal')
{ Base: [Function: Base],
LinuxSessionWrapper: [Function: LinuxSessionWrapper],
MetaShell: [Function: MetaShell],
Shell: [Function: Shell],
Log: [Function: Log],
SpawnWrapper: [Function: SpawnWrapper] }
>
> shell = new terminal.Shell()
Shell {
object:
{ shell: 'gnome-terminal',
option: [ '-x' ],
call: 'python',
script: '/home/th3ros/.atom/packages/atom-python-run/cp/main.py',
command: [Function: command] } }
>
> spawn = new terminal.SpawnWrapper(shell.object)
SpawnWrapper {
object:
{ shell: 'gnome-terminal',
option: [ '-x' ],
call: 'python',
script: '/home/th3ros/.atom/packages/atom-python-run/cp/main.py',
command: [Function: command] } }
The tty()
method takes a single object called model
which is a pseudo representation of how the ChildProcess
object should be created.
-
log
A flag based 'Boolean' value -
cp
Ditto... -
args
An 'Array' of arguments to be executed by a cli instance -
options
The 'options' object passed to 'child_process.spawn()'
> model = {
... 'log': true,
... 'cp': true,
... 'options': {},
... 'args': [
... 'python',
... `${process.env.HOME}/Documents/Python/hello.py`
... ]
... }
{ log: true,
cp: true,
options: {},
args: [ 'python', '/home/th3ros/Documents/Python/hello.py' ] }
The object passed to this wrapper is the Shell
object. The instance of the Shell
object is used to determine the call that is made to the child_process.spawn
method. Once a wrapper instance has been instantiated, the only method that should be called is the tty()
method. SpawnWrapper.tty()
will return the active ChildProcess
object after execution.
> spawn.tty(model)
platform: linux
shell: gnome-terminal
option: -x
call: python
script: /home/th3ros/.atom/packages/atom-python-run/cp/main.py
command: tell app "Terminal" to do script "python /home/th3ros/.atom/packages/atom-python-run/cp/main.py python /home/th3ros/Documents/Python/hello.py"
options: {
"detached": false
}
args: python,/home/th3ros/Documents/Python/hello.py
cp: true
ChildProcess { ...lots of stuff here... }
child process exited with code 0
You only need the Shell
and SpawnWrapper
classes. Create a Shell
object and pass Shell.object
or Shell.get
to the SpawnWrapper
object.
> let shell = new Shell();
When shell
is created, it automatically determines the OS and creates an object defining the parameters to pass to child_process.spawn
in the background.
> let spawn = new(shell.object);
When spawn
is created, it uses the shell.object
to define the arguments that are passed to child_process.spawn
when it is finally called. Before we call spawn.tty
, we need to define a special object required by the method call.
> let object = {
'log': true,
'cp': true,
'options': {},
'args': [
'python',
`${process.env.HOME}/Documents/Python/hello.py`
]
};
-
log
is a flag for toggling 'stdout' and 'stderr' to atoms 'console.log'. -
cp
is a flag for using the 'cp' python module to run scripts. -
options
is the object to be passed to 'child_process.spawn' parameter. -
args
is an array that contains the arguments to pass to theshell
; or to thecp
script.
Then call spawn.tty
and pass the object defining the parameters to be passed to child_process.spawn
.
> spawn.tty(object);
spawn.tty
returns a tty
object called ChildProcess
.