-
Notifications
You must be signed in to change notification settings - Fork 4
Command Contexts
Command context management used to be A LOT more complicated but shotgun 7.x and up simplifies context management tremendously :D
So what is a command context you ask? Well if you just came from the Getting Started page then what you probably didn't know is that shotgun was managing a command context for you internally. In a simple terminal application like the example one we wrote, letting shotgun handle the context by itself works just fine. However, what if your application were to have more than one user at a time? For example, web applications are bombarded by incoming requests and each of those requests could be from any number of different users.
Web applications handle many users by storing a unique session for each user and storing that session ID in a cookie that is saved to the user's browser. Think of calls to shell.execute like requests; shotgun has no way to know who is making the call unless they tell it who they are. For this reason, shell.execute takes a second argument called contextData. To really demonstrate this I'm going to modify the sample app we created to handle input from two different users.
In order to show you how contextData works we need to build a simple custom command module first. You'll learn more about custom command modules in the next article but for now let's put together a small module and place it in a shotgun_cmds folder at the root of our project.
// shotgun_cmds/memstore.js
exports.description = "A simple command module that lets the user store a message that they can later display.";
exports.invoke = function (shell, options) {
if (options.message) {
shell.context.setVar('message', options.message);
shell.log("Message saved.");
}
else if (options.retrieve) {
var message = shell.context.getVar('message');
shell.log("Your message was: " + message);
}
};The module we just wrote is super simple. If the user supplies memstore --message "Hello!" then the value "Hello!" would be stored on the context object. If they supply memstore --retrieve then the value will be retrieved from the context object and passed to shell.log. Before we modify our sample app let's test our module as it is right now. Here it is for reference:
var readline = require('readline'),
shotgun = require('../index'),
shell = new shotgun.Shell();
// Create interface that reads from console and outputs to console.
var rl = readline.createInterface(process.stdin, process.stdout);
rl.setPrompt('> ');
shell
.on('log', function (text, options) {
console[options.type](text);
})
.on('clear', function () {
console.log('\u001B[2J\u001B[0;0f');
})
.on('exit', function () {
rl.close();
process.exit();
})
.on('error', console.error.bind(console));
rl.on('line', function (cmdStr) {
shell.execute(cmdStr);
rl.prompt();
});
rl.prompt();After running the application type memstore --message "Hello, world!" into the prompt. You should see a message that says "Message saved.". Remember, shotgun is handling the context internally still and it should have stored this value on the context.
Now type memstore --retrieve into the prompt and your message is displayed. Everything is working as expected right? Sure, unless we try to let more than one user execute commands against the same shell instance. Let's add one more tiny custom command module that let's us switch "users".
// shotgun_cmds/su.js
exports.description = "Simple command that fires a switchUser event which the application can handle to change users.";
// Don't worry too much about this yet. You'll learn more about custom command modules later.
exports.options = {
username: {
noName: true,
required: true,
description: "The name of the user to switch to."
}
}
exports.invoke = function (shell, options) {
shell.emit('switchUser', options.username);
};Now let's modify our app to handle this new "switchUser" event. We're also going to call rl.setPrompt to modify the prompt character (defaults to >) so that it shows the username before the prompt character.
Note: In case you're confused,
readlineis a core node module and not directly related to shotgun.
var readline = require('readline'),
shotgun = require('../index'),
shell = new shotgun.Shell(),
currentUser = "guest";
// Create interface that reads from console and outputs to console.
var rl = readline.createInterface(process.stdin, process.stdout);
rl.setPrompt(currentUser + '> ');
shell
.on('switchUser', function (username) {
currentUser = username;
shell.log("User switched to: " + username);
rl.setPrompt(currentUser + '> ');
})
.on('log', function (text, options) {
if (text.length > 0)
text = currentUser + ': ' + text;
console[options.type](text);
})
.on('clear', function () {
console.log('\u001B[2J\u001B[0;0f');
})
.on('exit', function () {
rl.close();
process.exit();
})
.on('error', console.error.bind(console));
rl.on('line', function (cmdStr) {
shell.execute(cmdStr);
rl.prompt();
});
rl.prompt();You can test this out by running the app and typing help. You'll notice that all the lines displayed by help are prepended with the current username "guest". Now type su joe and you should see a message that says "User switched to: joe". Try help again and you'll see that the name is now "joe" instead of "guest". So now we have the pseudo-ability to change users.
What you may have already guessed is that we now have a problem with all these users sharing a single context. To see this problem in action just use our previous memstore command to store a message under one user, switch users, then use memstore to retrieve the message.
guest> memstore --message "I am a guest right now."
Message saved.
guest> su joe
User switched to: joe
joe> memstore --retrieve
Your message was: I am a guest right now.
Obviously what we'd really want is for guest and joe to have their own contexts so they can store their own messages. Modify your app as follows and let's walk through what we did.
// app.js
var readline = require('readline'),
shotgun = require('../index'),
shell = new shotgun.Shell({ debug: true }),
currentUser = "guest",
contexts = {
guest: {},
joe: {}
};
var rl = readline.createInterface(process.stdin, process.stdout);
rl.setPrompt(currentUser + '> ');
shell
.on('switchUser', function (username, currentUserContext) {
contexts[currentUser] = currentUserContext;
currentUser = username;
shell.log("User switched to: " + currentUser);
// Set prompt string so it includes the active username.
rl.setPrompt(currentUser + '> ');
})
.on('log', function (text, options) {
if (text.length > 0)
text = currentUser + ': ' + text;
console[options.type](text);
})
.on('clear', function () {
console.log('\u001B[2J\u001B[0;0f');
})
.on('exit', function () {
rl.close();
process.exit();
})
.on('error', console.error.bind(console));
rl.on('line', function (userInput) {
shell.execute(userInput, contexts[currentUser]);
rl.prompt();
});
rl.prompt();// shotgun_cmds/su.js
exports.description = "Simple command that fires a switchUser event which the application can handle to change users.";
// Don't worry too much about this yet. You'll learn more about custom command modules later.
exports.options = {
username: {
noName: true,
required: true,
description: "The name of the user to switch to."
}
}
exports.invoke = function (shell, options) {
shell.emit('switchUser', options.username, shell.context.data);
};In the above code we added a new variable called contexts that has a context object for each user of our application. When switchUser is invoked we accept a new argument currentUserContext. Then in our su.js command module we pass in the current context which is available at shell.context.data. We store the context data for the current user and then we switch users. Finally, whenever shell.execute is invoked we pass in the correct context for the current user. Viola! Shotgun now has a separate context for each of our users.
guest> memstore --message "I am a guest right now."
Message saved.
guest> su joe
User switched to: joe
joe> memstore --retrieve
Your message was: undefined
Then if you switch back to "guest" you'll see the expected message.
joe> su guest
User switched to: guest
guest> memstore --retrieve
Your message was: I am a guest right now.
How freaking awesome is that? And that is how command contexts work! :D
PS - For reference, this is the complete context API as it is defined within shotgun:
// Shell context API. Easily overridden if needed.
shell.context = {
data: {},
setVar: function (name, value) {
this.data[name] = value;
},
getVar: function (name) {
return this.data[name];
},
delVar: function (name) {
delete this.data[name];
}
};
```
Pretty simple huh? You are welcome to override this API if needed, just make sure to expose those same three methods and the `data` object and shotgun should work just fine. The only time shotgun accesses the `data` object is when `shell.execute` is invoked and passed in a context object; shotgun takes that object and [overrides `shell.context.data` with it](https://github.com/codetunnel/shotgun/blob/master/utils/execute.js#L10) for the duration of that execution.
---
Next up: [Command Modules >>](https://github.com/codetunnel/shotgun/wiki/Command-Modules)