MAE Example App - Hello, World - Python
Contents
A great way to start learning about the MAE platform is to examine what "Hello, World!" looks like as a MAE application. We'll call it the Hello app from here onward. This is the Python version; alternately, see this app in the C++ version.
File |
Description |
The Hello app can handle multiple users, so each user session is kept in a cache of sessions managed here. | |
A class instance of HelloApp exists for each user session. It handles all events related to user activity. | |
Fundamental app integration with MAE - announcing the app, initializing, quitting. | |
Initial page served to user by usergw when app starts. It lacks content; the content quickly follows; guibroker sends menu configuration; hello sends other page content. | |
MAE registration file to declare all event/message activity that the Hello app accepts from MAE. | |
An HTML snippet to display an in-window dialog box to ask the user for their name. The interactive components in the HTML are coded differently to give cues to MAE and facilitate event routing and processing. | |
The instructions to make(1) to convert the hellopy.mreg into msgHello.py using genmae and converting the helloPrompt.ui file into /usr/mae/html/record/helloPrompt.html using genformcode. | |
MAE includes the supervisor daemon, which monitors all other running app and daemons. It can tweak run-time parameters in them. This file contains the integration to support supiervisor. | |
MAE only allows registered applications to run inside its ecosystem. This CSV file is the Hello app's registration file. | |
This file is automatically generated by genmae from hellopy.mreg. It contains all the logic that bridges your app with the MAE system to route events, user respones, API calls, and remote procedure calls (RPC) to the approrpriate method in Hello app. |
The classic "Hello, World!" program demonstrates how to generate the message "Hello, World!" to the user's output device in the context of the programming envrionment (programming language, output device, supporting platform/OS). In that spirit, the Hello app will certainly greet the user with a hearty "Hello!", but instead of the anonymous "World", the Hello app greets the user by name. Two different ways are demonstrated for greeting the user
The Hello app demonstrates the code needed to ask the user for their name, query their name from the database, and output the friendly greeting to the user's display.
In the MAE context for the Hello app, the user has connected via web browser, so the Hello app's objective is to use that browser interface for user interaction.
The MAE environment's database includes the User table. MAE typically wraps every database table into a class, so the Hello app will demonstrate how to use the User class to query that table.
Although a MAE program can stay in charge by launching and controlling execution flow, most MAE apps turn event management over to MAE and simply respond as various events come in (a user response, an API call, etc.). Regardless of the approach, a MAE app needs a name to declare to MAE; that name needs to be unique within the current MAE ecosystem instance; that name will be used by MAE to ensure your app is allowed to participate in the current MAE ecosystem; that name will be used as the primary communications channel name. It's not a requirement, but it's a convention to use that same channel name in a number of other places in your app - used in class names, source code filenames, the app's web URL, and so forth. When managing source code for a number of apps within the same MAE ecosystem, this convention simplifies tracking, managing and debugging MAE apps.
In Python, your program defines init(), main(), and quit() before declaring itself a MAE program in your main app file; the declaration then invokes the MAE framework, which takes over: MAE soon calls your init(). Shortly after it calls init(), it calls your main(). If your application needs to maintain control of program flow, then your logic will go into your main(). Otherwise, you can leave main() devoid of statements except pass; MAE will manage the event processing and call parts of your app at the appropriate times as determined by how you register your app interactions with MAE. Without specific code, your main app file includes:
def init(argv):
# initializaation code
def main():
# main program functionality
def quit(rc= 0, msg= ):
# clean up, then exit
# Connect into MAE platform
MAEApp('hello')
# (ignored here onward)
Your app can be messaged by the MAE ecosystem in multiple ways:
But your app will not be messaged except for what it is registered to receive. Use the .mreg (MAE Registration) file to register to receive various messages. Your filename should use your selected channel name in the filename: channel.mreg.
The very first user-triggered message your app will receive is when the user first pulls up your app page. For https://maeserver/hello, usergw will serve up /usr/mae/html/hello.html, which is content-empty, but structured to receive content:
After usergw serves /usr/mae/html/hello.html and guibroker has populated the menus, then guibroker sends the userstart message to hello. Inside hellopy.mreg, you'll see #userstart, which specifies which method inside the Hello app should be called - userstart(param). Note that higher up in the file, the #appclass directive specifies that userstart() can be found in the HelloApp class. We could override this class assumption by instead declaring: ::userstart(param). The param parameter provided is akin to argv for main() and is not used by the Hello app. See What's Your Name? for what how that method prompts the user with a question.
#userstart userstart(param)
The menu options are declared with #menu and #msg directives to define the menu heading (Display), menu option (Help), unique message request keyword (help), and HelloApp method to call (menu_Display_Help()). Note that your event handlers return nothing; the return value is ignored by MAE since these messages are asynchronous with no response back to the message sender.
#menu "Display" "Help"
#msg "help" menu_Display_Help();
The Hello app does not declare any RPC or API messages, but you can learn about them and other directives in genmae, the tool used to convert the hellopy.mreg file into the msgHello.py file.
With the Hello app now registered to receive userstart messages, the HelloApp.userstart() in HelloApp.py specifies what actions to take to fill in content for the user to see. Since the Hello app's purpose is to display "Hello!" to the user, our only setup is to ask the user for their name. We have three options to consider doing this:
Option 1. To prompt the user in the MAE-provided prompt box, we would use something like
self.queryText("Your name? ", "name", XMLData(), "promptbox");
which will present the question "Your name?" to user user. When the user provides a response, it will be sent to the Hello app using the message request keyword name (with no other callback data since XMLData is empty). The target for the question to appear will be the promptbox div. Note that we would have to add this message registration to hellopy.mreg so tell MAE which method will process the response:
#response "name" inputName(name: str)
Option 2. To prompt the user in a specific area (perhaps by adding a <div id="nameq"></div> in hello.html), we would use something like
self.queryText("Your name? ", "name", XMLData(), "nameq");
#response "name" inputName(name: str)
The response needs to be registered with MAE. Instead of explicitly adding it to hellopy.mreg like the previous example, we add it in the source code in a comment for genmae to find. This is very handy to keep the question and the directive to the response handler together. To enable genmae directives like #response to be included into hellopy.mreg, we need to add this line to the bottom of hellopy.mreg:
#process "HelloApp.py"
which tells genmae to scan HelloApp.py for genmae directives inside comments.
Option 3. However, in the Hello app we are not choosing either of the above options to prompt the user. Instead, Hello app uses a HTML code snippet to ask the question, coded in helloPrompt.ui. Any proper HTML can go in there. It can be small and simple or large and extravigant; it can contain one question or be a large form with many questions. It's HTML. The Hello app places the question and answer in two different cells of a table with an empty row, which will be used for output later. This code snippet is what MAE calls a record; in other contexts, it can be used to display myriad data for a record of key/value data.
When you look at helloPrompt.ui, you'll notice that the HTML for the input box is not HTML, instead you see text
[text:name->name->HelloApp.inputName(name: str)]
which is in the format
[type:htmltag->msgrequest->pymethod]
The type can be text, textarea, button, or checkbox; the htmltag is an id of your choosing, but note that genformcode will prefix helloPrompt_ to your id; msgrequest is the message keyword that will be sent to the Hello app which triggers the calling of your method; and pymethod is the prototype of your method to handle this user input. Inside hellopy.mreg, you see this line at the bottom:
#process "helloPrompt.ui"
which tells genmae to use genformcode to scan helloPrompt.ui for messages that need to be handled. In our case, it finds the name message, which should route its user response payload to HelloApp.inputName(name: str). If we did not want to use this special formatting, we would have needed to add this line into hellopy.mreg:
#response "name" inputName(name: str)
What we've accomplished in this section is to prompt the user "Your name:" and registered the response message with MAE so the response is routed back to our method HelloApp.inputName(). We discussed three different ways we could do this, but chosen the last method - to use a record (HTML code snippet). So, HelloApp.inputName() will be called with the user's response.
The HelloApp.inputName() method is called each time the user enters a new response to our "Your Name:" question. This is where the Hello app reacts to this user input. For our simple app, we will echo back the provided input with "Hello, " prepended. The UserDevice class is used to setup all sorts of page content for the user to see; we saw queryText() used above, but we can also output (formatted) text, images, sounds, and even download content.
The helloPrompt.ui HTML snippet not only contained our question for the user, but also contained an empty table cell with the id helloPrompt_greeting. (Note that it's good practice to prepend the .ui file's name to all ids in the file to prevent id name clashing with other HTML code in that HTML page.) This is where we will generate our "Hello" greeting to the user. In MAE, we call that id name a tag and the content it holds in a region of the page. We could clear() the region before we display our greeting. We could replace any existing content there with our new greeting using settext(). But the Hello app will append the text using
self.writeln(f"Hello, {name}!", "helloPrompt_greeting");
Note that the difference between write() and writeln() is that writeln() issues a <br> after its content so any new content will start on a new line.
Because the Hello app appends the text, the user can enter multiple names and see them accumulate in the display. However, if the user wants to clean up the display, the user can select the Display->Clear menu option. From hellopy.mreg, the Display->Clear menu option was registered to call menu_Display_Clear()):
#menu "Display" "Clear"
#msg "clrdisp" menu_Display_Clear();
to start clean. The #menu directive tells guibroker to display this menu option when the user first connects. The #msg directive specifies that the clrdisp request keyword should be sent as a message to the Hello app and, once received, should call menu_Display_Clear()).
Why did we ask the user for their name? It was a handy, if not terse, exposure to MAE's UserDevice class which is critical for user interaction. However, instead of asking the user for their name, we should already know their name since they had to log into the MAE system to even get to the Hello app. But where is that information kept? Create users using the tuser program, which manipulates MAE's User database table, which contains the user's name that we seek.
When using the MAE database interface, it is best practice to create a class for each database table and then use that class to access the table. Creating source code for that class is quick and easy with the genmae tool. If you wanted to create a simple table of names and e-mail addresses, you could create this file called Contacts:
DbRecNum id; # Contacts table record id
string name; # the contact's name
string email; # the contact's email
which can quickly be turned into a class by using
genmae py Contacts > Contacts.py
genmae h2sql Contacts > Contacts.sql
This generated and commented code can be further modified by you, of course. This is what the MAE development team did to create the User class. Using this class to fetch a user's name is straightforward:
userId= getUserId();
User user(userId);
name: str= user.getName();
Note that getUserId() is a method in the UserDevice class. The UserDevice class is MAE's connection to the user as we demonstrated previously for user interaction. It also contains basic information about the user; most notably, it contains the user's MAE user id, which is queried using getUserId().
With all this information, we then created the menu_Display_FromDB() method.
Before a user can run our Hello app, they must have a user login on the system where MAE is installed. To add user Anthony Edwards with e-mail ave@vegas.home with password mae, use this tuser command:
tuser add -n "Anthony Edwards" -e "ave@vegas.home" -p mae
The tuser program can be used to add users, change users, enable or disable users, and look up users by email. Users are unique inside MAE by email address; two users may not have the same email address.
For our purposes here, we will call the MAE server maehello and assume the HTTP port has not been changed from the default of 8048. To access the Hello app, we would then use:
https://maehello:8048/hello
which will first prompt us to login using our email address and password.
Note that you can configure the default app that runs when a user logs in by setting a usergw parameter. When MAE is running, you can change the firstapp paramter using the tpset program with
tpset usergw firstapp hello
which directs user logins to the Hello app on the hello channel. Presumably the Hello app will allow the user access to the other applications that the user can use. If so, the Hello app would have menu options or other user interaction which would then call UserDevice::run() to switch to the other app. Subsequently, the other app would call UserDevice::quit() to come back to the default login Hello app.
After changing the firstapp parameter, you will need to restart usergw so it re-reads that parameter and changes its behavior. You can do this with trestart:
trestart usergw
Of course it may feel like the Hello app is just for us and nobody else, but the Hello app needs to sevice the enterprise, which has many users. How does Hello app keep all those users straight?
We created the HelloApp class to handle all the user interaction. Each instance of that class handles a separate user. The unique id of the instance is the user's id. Its super class is UserDevice, which simplifies session management since UserDevice is a user session.
Let's now go back to hellopy.mreg to revisit how MAE matches the UserDevice session to our HelloApp instance. There are directives at the top of hellopy.mreg that are citical for MAE to do this:
#appclass=HelloApp
#appcache=HelloCache
These two directives tell MAE about our user instance (HelloApp) and our vector cache of instances (HelloCache). Next, we specify the code to determine our HelloApp id from the UserDevice session:
#idcode
user_id= device.getUserId()
pos= HelloCache.findPos(user_id)
# insert error checking code here to ensure pos is valid, e.g. 0+
instance= HelloCache.getitem(pos)
All inbound messages for user interaction will have a valid (UserDevice) device value set, so our code extracts the user id out of it. The cache of HelloApp instances is defined in the HelloCache.py file; it is a vector with some data management to limit how large the vector can be and other cache management functions. The findPos() method locates a HelloApp instance with an id value that matches the provided user_id value.
The first time ever that the user connects to the Hello app, there will be no existing cache instance for HelloApp. In this case, we need to create a new instance and add it to the cache:
if pos < 0 and user_id > 0:
# a new connection - add to cache
newUser= HelloApp(user_id)
# remember our path to the user
newUser.setDevice(device)
# add this user to our in-memory cache
pos= HelloCache.add(user_id, newUser)
The last line of the #idcode block is
instance= HelloCache.getitem(pos)
This instance variable is used to call the HelloApp methods that have been registered to handle user interactions and other message requests.
By caching and managing the various HelloApp instances, the Hello app is able to handle multiple users without any data mingling.
To view this code in its final context, see the generated code in msgHello.py.
Up to now, we have focused on the functional part of the Hello app - how the Hello app talks to the user and receives user responses. But the Hello app needs to setup with MAE to plug into MAE's ecosystem instance at run-time startup. This code is located in hello.py. This file contains four key parts:
Use MAE::init() to initialize the app from command line arguments (argv).
At the point that MAE::init() is called, the MAE environment is operational: messages can be sent and the database is available, but the app is not considered ready; CommHub shows the app status as Init; other apps are not told that this app is running yet. Your app's readiness is declared after MAE::init() is called and before MAE::main() is called.
This is the main body of the MAE program. If we do anything more than respond to events, that logic appears here. To allow MAE to get and process events, call task.process(). The task variable is a global in all MAE programs of type MAETask.
This method is where your program saves out any unsaved data and generally cleans up just before it exits. Your program should not call exit(); always use quit() for a clean exit.
You application must declare the maeapp variable as either type MAEDaemon, MAEApp, or MAEUtility. Initialize it with the name of your app. For the Hello app, we use MAEApp:
MAEApp maeapp("hello");
App Type |
Description |
If your program is a daemon that is expected to always run and does not accept user UI responses, then use the MAEDaemon type. | |
If your program is an app that is expected to always run and process user UI responses, then use the MAEApp type. | |
If your program performs a task and then exits, it is a MAEUtility type. |
Note that there is an optional second parameter, which can be used if your app communicates messages on more than one channel.
See the Hello app Makefile that is used to generate parts of the app. Here are notable parts:
genformcode html helloPrompt.ui /usr/mae/html/record/helloPrompt.html
genmae hellopy.mreg -msg msgHello.py
genmae hellopy.mreg -apipy -apih
Your resulting binary does not need to be installed in /usr/mae/bin; you can choose where to install it. You will need to know its installation location in order to MAE-enable your app.
When you type tstatus (if /usr/mae/bin is in your PATH), you see a list of all daemons, apps, and tasks in your MAE ecosystem instance. To add your app, you will need to create an application registration file and register it with MAE. For the Hello app, we use mgmttaskdb.csv:
Id,Name,Restart,Enabled,MultiExec,DbPermit,MsgPermit,Command,Integration,DepList,ClusterName,ExitTimeout,WatchdogTimeout,ProxyHost,Schedule
4,hello,T,T,F,T,T,/usr/mae/bin/hello.py,1,msgbroker,,1,60,,
Here's an explation of the fields:
Field |
Description | ||||||||
Id |
In the mgmttaskdb, this is the record id. Just use a low number; tadd will change it before adding this information to /usr/mae/conf/mgmttaskdb.csv. | ||||||||
Name |
This is the name of your MAE program. It does not need to match the channel your program uses, but it must match the name that your program uses when talking with CommHub, e.g. the MAEApp/MAEDaemon/MAEUtility line in hello.py. | ||||||||
Restart |
This true/false flag tells CommHub if this MAE task should be restarted if it quits/fails on its own. If T for true, CommHub will ensure it is restarted when it fails. CommHub uses a back-off algorithm to give increasing amounts of time before restarting a repeatedly failing program. If a user issues tstop, then CommHub will not restart the program. | ||||||||
Enabled |
This true/false flag tells CommHub whether this application is allowed to run or not. If T for true, the application may run; if F for false, CommHub will not allow the application to connect and participate in this MAE ecosystem instance. | ||||||||
MultiExec |
This true/false flag tells CommHub if multiple instances of this program are permitted to run at once or if this program may only have one instance running. Note that the base instance of a program is instance 0; successive instances must be unique with a value of 1 or larger. To view all instances of a running program (e.g. the hello program), use tstatus hello.* If T for true, CommHub will allow multiple instances; if F for false, it will reject attempts by other instances to participate in this MAE ecosystem instance. | ||||||||
DbPermit |
This true/false flag tells CommHub whether this program is allowed to access the database via dbbroker or not. If T for true, the program may access the database. | ||||||||
MsgPermit |
This true/false flag tells CommHub whether this program is allowed to send and receive messages via msgbroker or not. If T for true, the program may send and receive messages. | ||||||||
Command |
This is the command (with full path) and command line parameters needed to start the program. | ||||||||
Integration |
This is the integration level implemented by the program.
| ||||||||
DepList |
This is a list of one or more other programs that must be running before this program can run. At this time, only one dependency is supported. | ||||||||
ClusterName |
This is the name of the CommHub spoke where the program runs. If not specified, the program will be launched on the root CommHub. Note that the spoke CommHub must connect to CommHub before CommHub can launch the program there. | ||||||||
ExitTimeout |
This is a count of the number of seconds that CommHub will permit this program to clean up and exit when it's been told to stop. After the timeout, the program will be forced to quit via OS signal. | ||||||||
WatchdogTimeout |
This is a count of the number of seconds that CommHub will permit this program to run without any response before determining that this program is hung/frozen/unresponsive. A hung program is killed via OS signal and restarted. | ||||||||
ProxyHost |
This field is not presently used. | ||||||||
Schedule |
This field is not presently used. |
Once the mgmttaskdb.csv is configured, add the configuration into MAE using
Note that CommHub will not immediately recognize the new program; CommHub must be restarted:
The first tstop stops all daemons and apps; the second one stops CommHub itself.
The Hello app is simple and therefore not likely to need management, but the management interface is required in every MAE program. It is realized through the MgmtClient class.
The management interface allows the supervisor app to query the current state (such as number of active sessions) of the app and possibly allow supervisor to modify that number.
This is an advanced topic outside the scope of the Hello app, but the minimal implementation can be found in mgmtHello.py.
At this point, everything is registered, assembled, and enabled.
If it is not already running, start the Hello app with
tstart hello
Connect to the app using https://maehello:8048/hello, where the MAE server is maehello and the HTTP port has not been changed from the default of 8048. You will see:
Go ahead and type a name into the box and press Enter. You will see the response, "Hello, name!" appear below the box. You can do this several times if you like.
Then select Display->From DB. You will see the response "Hello, username!" with your MAE user name printed below the box.
And thus we have the traditional "Hello, World!" program running in MAE.
Since the Hello app was already written and working we did not need to do any debugging steps. However, when you're developing your own app, you will likely need to use some MAE debugging tools. When writing the Hello app, the development team utilized the following tools/techniques:
tdebug hello
/usr/mae/log/guibrokerdbg.log
/usr/mae/log/hellodbg.log
if debug_on: dbgf.write(f"Checkpoint 3\n")
ttap hello