An Octory Pro tour

What you will learn in this tutorial

Prerequisites

To not get lost in this tutorial, you should know how to start the Octory application on your device. You can refer the tutorial Getting started with Octory to learn that.

First part: Onboard
Second part: Watch over


Insert and remove components

Start by copying the file Octory_first_part_start.plist in the folder /Library/Application Support/Octory. You might want to rename it to remove the “start” part. I will rename it to Octory_pro.plist. In this tutorial, we will work mainly inside the folder /Library/Application Support/Octory, assuming the Octory.app is in there. But you are free to launch Octory from anywhere you want, as long as the file Octory_pro.plist is at /Library/Application Support/Octory.

Assuming that you are in the folder Octory (cd "/Library/Application Support/Octory"), start by launching Octory by specifying the configuration we just copied:

open Octory.app --args -c Octory_pro.plist

You should see the following window with the (current logged in user name) appearing on your screen.

If you have multiple screens, you can choose on which screen the window should appear with the key WindowScreenIndex. Rather than dive into the configuration file right now, you should try to set the key value with:
PlistBuddy
/usr/libexec/PlistBuddy -c "Set :Window:ScreenIndex 1" Octory_pro.plist
Scout:
scout set "Window.ScreenIndex"=1 -m Octory_pro.plist .

To know which screen corresponds to which index, you can use the brightness tool and identify your screens by their resolution.

brew install brightness
brightness -lv

Add a variable to play with

We will start by adding a ListInput component so that we can play with its variable. Please open the configuration file (Octory_pro.plist in this tutorial), and expand the Slides array.

Here is the Slides array as Plist.

<array>
	<dict>
		<key>Containers</key>
		<array>
			<dict>
				<key>Components</key>
				<array>
					<dict>
						<key>Type</key>
						<string>Spacer</string>
					</dict>
					<dict>
						<key>Text</key>
						<string>Welcome to ${CompanyName} ${USER_FULL_NAME}!</string>
						<key>TextFontConfiguration</key>
						<string>Title</string>
						<key>Type</key>
						<string>Text</string>
					</dict>
					<dict>
						<key>Type</key>
						<string>Spacer</string>
					</dict>
				</array>
			</dict>
		</array>
	</dict>
</array>

Insert the following ListInput component as a new component in the components array:

<dict>
	<key>Type</key>
	<string>Input</string>
	<key>InputType</key>
	<string>List</string>
	<key>Variable</key>
	<string>FoodLike</string>
	<key>Label</key>
	<string>What food do you like?</string>
	<key>PercentWidth</key>
	<real>0.2</real>
	<key>Margins</key>
	<dict>
		<key>Top</key>
		<integer>30</integer>
	</dict>
	<key>Items</key>
	<array>
		<string>Sushis</string>
		<string>Pasta</string>
		<string>Tartiflette</string>
	</array>
</dict>

The components array should now look like this:

Notes

Reload the app interface with ⌘R. The ListInput component should now be present, and you can select a value inside it.

Now it’s time to add conditions!

Add conditions

We will add three Text components: one for each possible meal. Each Text component will appear when the user selects a meal.

In Octory, a condition is a String key which is most often named Condition. The value of this key is very similar to what you might use in Bash or another programming language. For example, to state that a component should only be visible when the variable FoodLike has the value “Sushis”, we would write

FoodLike == "Sushis"

That’s it! So to insert a Text component which only appears when the user select the “Sushi” value, we would insert the following below the ListInput component we added previously:

<dict>
	<key>Type</key>
	<string>Text</string>
	<key>Text</key>
	<string>So, do you like sushis?</string>
	<key>Condition</key>
	<string>FoodLike == "Sushis"</string>
	<key>TextFontConfiguration</key>
	<string>Body</string>
</dict>

The key TextFontConfiguration uses a FontConfiguration defined in the FontStyles section.

Now, reload the app interface with ⌘R and… wait a minute! The text is visible although we just added a condition to it! Well, it’s logical actually. The value “Sushis” is selected by default in the ListInput component. So the variable FoodLike has the “Sushis” value. We could add validation to it to prevent this behaviour but it is not the purpose of this tutorial.

If you select another value than the “Sushis” one, you should see the text disappear, and reappear when you select again “Sushis”.

It was easy, wasn’t it? And now a little exercise! You did not think I would do all the work myself, did you? 😉 Let’s add the two other Text components. You can set their Text keys to whatever you like. You should add them below the Text component we just added.
Small tip: you should copy the Text component we just added and paste it twice.

When you have finished, here is the solution:

<dict>
	<key>Type</key>
	<string>Text</string>
	<key>Text</key>
	<string>So, do you like sushis?</string>
	<key>Condition</key>
	<string>FoodLike == "Sushis"</string>
	<key>TextFontConfiguration</key>
	<string>Body</string>
</dict>

<dict>
	<key>Type</key>
	<string>Text</string>
	<key>Text</key>
	<string>We all love pasta</string>
	<key>Condition</key>
	<string>FoodLike == "Pasta"</string>
	<key>TextFontConfiguration</key>
	<string>Body</string>
</dict>

<dict>
	<key>Type</key>
	<string>Text</string>
	<key>Text</key>
	<string>You are keen of cheese too, he?</string>
	<key>Condition</key>
	<string>FoodLike =="Tartiflette"</string>
	<key>TextFontConfiguration</key>
	<string>Body</string>
</dict>

Now reload the application interface with … ⌘R (you got it!). You should see the several Text components appear and disappear depending on the value you choose.

The condition key is available in all components. Even a Spacer component can be inserted conditionally! Several components can also have the same condition for example, and thus appear at the same time. If you want to learn more about conditions, you can refer to the documentation. Take the time to try them, as they are really useful in many scenarios. We can imagine a dynamic form whose content will appear under specific conditions. But also a way to spare the space and to display an information to the user only when it is relevant, and so on…


Conditionally execute a bash command or a Jamf policy

Playing with a file

Displaying information depending on the user inputs is great. But now it’s action time! We will execute a bash command depending on the user choice. Then we will see how to do the same to execute Jamf policy.

There are several actions you can execute in Octory. Some let you interact with the user like the DisplayAlert action, while some let you send instruction to the Mac, like the RestartComputer action. You can find the full actions list in the documentation.

We will start to play with the ExecuteCommand action. It is certainly the most useful action as you can use it to execute bash/zsh commands with root privileges. Let’s make a file to play with. Run the following command in terminal:

echo "Octory Pro actions rock!" >> "/Library/Application Support/Octory/file"
sudo chown root:wheel "/Library/Application Support/Octory/file"
sudo chmod 600 "/Library/Application Support/Octory/file"

Now if you try to run cat "/Library/Application Support/Octory/file" you should get a Permission denied error. Run sudo !! to make sure you can output the file with root rights.

Installing Octory Helper

As Octory has the rights of the user launching it, it cannot execute actions which require root privileges. A solution would be to run Octory as the root user. But really: this is a bad solution. An application should never be ran as root, especially when it is customisable like Octory. It is a fault which can be easily exploited. As an application can all the same need to execute actions with root rights, Apple provides a way to it safely (or at least with more security than running the app as root). The developer (your humble servant) has to develop a Helper. It’s a program which will be ran as root by a LaunchDaemon. Only the application signed with the same certificate as the one used to sign the Helper can use it. The LaunchDaemon will act as a listener. When the application requests the right to use the Helper, it will forward the request to the Helper. The latter then ensure the application is authorised, and execute the request.

You can find the Helper in the downloads section of Octory website:

It contains three files. The Helper, the LaunchDaemon and a script installHelper.sh to install them. The script will install the Helper in the required folder: /Library/PrivilegedHelperTools. Then it will copy the LaunchDaemon in /Library/LaunchDaemons and load it. The script requires root rights to run as it installs files in system folders. It will output the success of the operation. You can then check that everything is setup with

sudo launchctl list | grep com.amaris.octory.helper

If everything has been installed correctly, the command should output something like

-	0	com.amaris.octory.helper
If you face an issue in the process, please let me know on Slack.

You might be wondering: can’t the application install the Helper itself? Well… it can actually. And it will try to if it has to execute an action with root rights although the Helper is not installed and ready to run. But - and it is an important “but” - it will require a user with admin rights to install the Helper for the same reasons I mentioned above. Thus, when you deploy the app, the user will be asked to enter the admin credentials to install the Helper. This is the reason why the Helper is provided separately so that you can install it when deploying Octory with your root privileges.

Build the action

Now that everything is setup, let’s build our action. Actions are specified within an ActionSet. You can specify an ActionSet in the ActionSets section array, or as the OnClick key of a button. In this tutorial, we will start by defining one action set in the ActionSets section array and add one action into it.
Add a new array key at the top of the configuration file (just below the root element if you are using PLIST Editor) and name it ActionSets (only one “s” to “Set”):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>ActionSets</key>
	<array/>
	<key>Admin</key>
	<dict>
		<key>IsAdminModeEnabled</key>
		<true/>
	</dict>
	...

An ActionSet is a dictionary with several children keys, whose two are required: the String Type key and the Array Actions key. The Type key can take two values: “Parallel” and “Chained”. Those let you specify how you want the actions to be executed: one after the previous has finished with “Chained”, or concurrently with “Parallel”. Last but not least, you should specify an Array key of Triggers to let Octory know when the actions in the set should be executed. You can specify several triggers. Among them is “Launch” to execute the action set when the application launches, or “Update(variable)” to execute the set when a variable is updated. You can find the list here.

For now, we will set the Type to “Chained”, add a “Launch” trigger and add the required Actions key:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
	<dict>
		<key>Type</key>
		<string>Chained</string>
		<array>
			<string>Launch</string>
		</array>
		<key>Actions</key>
		<array/>
	</dict>
</array>
<key>Admin</key>
...

Make sure the application can retrieve the configuration by reloading the interface and checking the log.
Let’s add our action. An Action is a dictionary key with one required child key: Type. Depending on the provided value for the Type key, other keys can be required. The action we want to add has the type “ExecuteCommand” and thus requires one key to be specified: the Command key. This key has a String value which is the bash/zsh command to execute. It supports pipes | to let you manipulate the command output. This output can be stored as a variable in Octory with the Variable key. Let’s read the content of the file we just created and store that into a variable named message. Here how it looks like:

<array>
	<dict>
		<key>Type</key>
		<string>Chained</string>
		<key>Triggers</key>
		<array>
			<string>Launch</string>
		</array>
		<key>Actions</key>
		<array>
			<dict>
				<key>Type</key>
				<string>ExecuteCommand</string>
				<key>Command</key>
				<string>cat "/Library/Application Support/Octory/file"</string>
				<key>Variable</key>
				<string>message</string>
			</dict>
		</array>
	</dict>
</array>

Now restart the app. Ostensibly nothing happened. We don’t use the variable in a Text component. To see it, open your Octory log and search for the line “Successfully updated a variable [ message: ‘Octory Pro actions rocks!’]”. Under the hood, a huge work has been done (trust me I know a few things about it 😉): convert the string to a bash command, send it to the Helper to execute it with root privileges, wait for the result and write it into the variable message.

To see the variable in the interface, you can add a Text component below the last Text component with a condition, and use the message variable inside it (I added a top margin with 15 points to ventilate the layout):

<dict>
	<key>Type</key>
	<string>Text</string>
	<key>Text</key>
	<string>You are keen of cheese too, he?</string>
	<key>Condition</key>
	<string>FoodLike == "Tartiflette"</string>
	<key>TextFontConfiguration</key>
	<string>Body</string>
</dict>

<dict>
	<key>Type</key>
	<string>Text</string>
	<key>Text</key>
	<string>Here is the message: ${message}</string>
	<key>Margins</key>
	<dict>
		<key>Top</key>
		<integer>15</integer>
	</dict>
	<key>TextFontConfiguration</key>
	<string>Body</string>
</dict>

You can now restart the application to see the variable in the added Text component (reloading the interface would have no effect, as the action set is executed at the application launch):

About the Helper
You can find a log which shows the commands the Helper executed as well as the outputs at /tmp/octoryHelper.log. It can be useful for debugging. But once you are sure everything is working correctly you might want to avoid to output the commands on your end-users machines. To prevent that, run the following (you can also edit the LaunchDaemon manually):
PlistBuddy

sudo /usr/libexec/PlistBuddy -c "Delete StandardErrorPath" /Library/LaunchDaemons/com.amaris.octory.helper.plist

Scout

sudo scout delete StandardErrorPath -m /Library/LaunchDaemons/com.amaris.octory.helper.plist

To finish this chapter, let’s add a command to read the message variable. The key to add will be very similar to the one we just used: an ExecuteCommand action with the following command: “say ${message}”. Once again, you can try to find the solution yourself.


And here is mine (you are fast!).

<dict>
	<key>Type</key>
	<string>ExecuteCommand</string>
	<key>Command</key>
	<string>say ${message}</string>
</dict>

Make sure your speakers are on and restart the app. You should hear your Mac saying “Octory Pro actions rock!”… I’m not the one saying that, it’s your computer! More seriously, note that as we setup the Type key of the ActionSet to “Chained”, Octory waits for the first action to finish before executing the second one. And this is great as the second action needs to access the message variable which is written by the first action!

Jamf policy

We will not explain how to setup a Jamf policy in this tutorial, but I will tell you how to use it in Octory. It is another Action with the Type key set to “ExecuteJamfPolicy” and one required child key: Name to let you specify policy custom trigger. For example:

<dict>
 	<key>Type<key>
 	<string>ExecuteJamfPolicy</string>
 	<key>Name</key>
 	<string>StartEnrolling</string>
</dict>

Pretty simple, right? Note that Octory uses the Helper to execute the policy as it requires root privileges.


How to conditionally display a monitor

If it is great to display components depending on conditions, it would be also interesting to conditionally display an application monitor. For example, it might be useful to execute a Jamf policy to install a package depending on the user choice, and then to display the corresponding monitor. Naturally, if I’m talking about such a feature, it is because it is possible to do so!

Setup

We will play with three monitors to insert and remove them, while another one will stay in place. As monitoring with Octory is not the purpose of this tutorial, I will ask you to make several copy and paste actions. It is not the most interesting part of the tutorial but I promise you that it will over quickly. You just need attention and we will go through this together, ready? Go!

Monitors

First: the monitors. the monitors: please copy the following. If you are not using PLIST Editor, copy only between the plist keys as indicated by the comments. Otherwise, you can copy the overall text.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<!-- Copy from here for a standard text editor -->
<dict>
	<key>Monitors</key>
	<array>
		<dict>
			<key>Type</key>
			<string>Application</string>
			<key>Installer</key>
			<string>System</string>
			<key>Name</key>
			<string>Drink</string>
			<key>IconURL</key>
			<string>${Monitors}/drink.png</string>
		</dict>
		<dict>
			<key>Type</key>
			<string>Application</string>
			<key>Installer</key>
			<string>System</string>
			<key>Name</key>
			<string>Sushis</string>
			<key>IconURL</key>
			<string>${Monitors}/sushis.png</string>
		</dict>
		<dict>
			<key>Type</key>
			<string>Application</string>
			<key>Installer</key>
			<string>System</string>
			<key>Name</key>
			<string>Pasta</string>
			<key>IconURL</key>
			<string>${Monitors}/pasta.png</string>
		</dict>
		<dict>
			<key>Type</key>
			<string>Application</string>
			<key>Installer</key>
			<string>System</string>
			<key>Name</key>
			<string>Tartiflette</string>
			<key>IconURL</key>
			<string>${Monitors}/tartiflette.png</string>
		</dict>
	</array>
</dict>
<!-- Copy up to here for a standard text editor -->
</plist>
PLIST Editor

Paste the copied text at the end of the plist, below the closing tag </dict> of the Window section:

Then change the “New item –2” key name to “Monitoring”:

Standard text editor

Paste the copied text between the closing </dict> tag of the Window section and the global closing </dict> tag:

Add the key name below the closing closing </dict> tag of the Window section : <key>Monitoring</key>:

Variables

To enjoy the (beautiful) icons for the monitors we will add the variables to the resources folder. So please copy the following in GeneralVariables, below the “CompanyName” key:

PLIST Editor
PLIST Editor
Atom
Atom
PLIST Editor
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<string>/Library/Application Support/Octory/Resources</string>
</plist>

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<string>${Resources}/Monitors</string>
</plist>
Standard text editor
<key>Resources</key>
<string>/Library/Application Support/Octory/Resources</string>
<key>Monitors</key>
<string>${Resources}/Monitors</string>

Appearance

Finally, let’s just take the left container take2/3 of the width, by adding the Boolean key IsLarge to it, with the value true:


And we are ready to go! Run the app, it should look like this now:

Conditionally display a monitor

We will display the “Sushis” monitor only when the user selects the “Sushis” option. For the “Pasta” and “Tartiflette” monitors, we will display them together with a less specific condition.
Let’s add the “Sushis” condition. It is exactly the same as for the sushis Text component. So please add a Condition key to the “Sushis” monitor with the value FoodLike == "Sushis".

<dict>
	<key>Type</key>
	<string>Application</string>
	<key>Installer</key>
	<string>System</string>
	<key>Name</key>
	<string>Sushis</string>
	<key>IconURL</key>
	<string>${Monitors}/sushis.png</string>
	<key>Condition</key>
	<string>FoodLike == "Sushis"</string>
</dict>

If you reload the interface now, and change the option to something else than “Sushis”, the monitor should disappear.

Let’s now add the condition to display both “Pasta” and “Tartiflette” monitors together. We will add a condition “IsFoodEuropean” which will be true when the FoodLike variable equals “Pasta” or “Tartiflette”. To avoid to repeat ourselves, let’s store this condition and use it in both monitors. To do so, please add a Conditions section in the file, and add a key named IsFoodEuropean with the value "Pasta, Tartiflette" <: FoodLike into it:

<plist>
<dict>
	<key>Conditions</key>
	<dict>
		<key>IsFoodEuropean</key>
		<string>"Pasta, Tartiflette" <: FoodLike</string>
	</dict>
	<key>ActionsSets</key>
	<dict>
...

Here is how it works:
- We specify an array of possibilities between double quotes, separating items with a coma: "Pasta, Tartiflette". Note that we could also define a variable to contains those items.
- The operator <: translates a “contains” condition. The condition is true is the left operand "Pasta, Tartiflette" contains the right operand; the FoodLike variable.

Thus, the condition is true only when the FoodLike variable is equal to “Pasta” or “Tartiflette”.
Finally, let’s add our new condition to the remaining monitors “Pasta” and “Tartiflette”. Here is the “Pasta” monitor. Note that as the variable “IsFoodEuropean” has a true or false value, we can specify its true condition by simply writing it. I’ll let you add the condition to the “Tartiflette” one.

<dict>
	<key>Type</key>
	<string>Application</string>
	<key>Installer</key>
	<string>System</string>
	<key>Name</key>
	<string>Pasta</string>
	<key>IconURL</key>
	<string>${Monitors}/pasta.png</string>
	<key>Condition</key>
	<string>IsFoodEuropean</string>
</dict>

Now reload the interface. The monitors “Pasta” and “Tartiflette” should have disappear. If you choose “Pasta” or “Tartiflette” as an option, they should appear while the “Sushis” monitor should disappear. Also, note that the Text components still appear and disappear depending on your choice. This is starting to be really dynamic!

Inform the user when the onboarding process is over

Display an alert

Now, to end the first part of this tutorial, let’s use a nice feature to finish the onboarding process. We will display an alert and play a custom sound to the end-user when all monitors are installed.

To do so, we will add another ActionSet to our ActionSets section. Remember it ? We used that to execute a bash command to output a root owned file content and make the Mac speak out loud. So, please add a new item to the ActionSets array, setting its Type key to “Parallel”, add a Trigger array and an Actions array into it.

Small tip: you should copy and paste the first existing item and paste it below. Then modify the Type key and remove the items in the arrays

<key>ActionSets</key>
<array>
	<dict>
		...
	</dict>

	<dict>
		<key>Type</key>
		<string>Parallel</string>
		<key>Triggers</key>
		<array/>
		<key>Actions</key>
		<array/>
	</dict>
</array>

The trigger we are going to use let us execute the ActionSet when a variable is updated. The variable we are going to observe is a one generated by Octory: INSTALLATION_COMPLETE. It is set to true when all monitors (applications and mandatory files) are installed/present. By using the trigger “Update(INSTALLATION_COMPLETE)”, we are telling Octory to execute the set whenever this variable changes.
Hm, this is not exactly what we want. We want to execute the action to display an alert when the INSTALLATION_COMPLETE variable is true only. Not when it is set to false. So… let’s add a condition! An ActionSet can have a Condition key to specify to Octory whether the set should be executed when it is triggered. The condition here is simply “INSTALLATION_COMPLETE”. Here is how our set looks like when adding those keys:

<key>ActionSets</key>
<array>
	<dict>
		...
	</dict>

	<dict>
		<key>Type</key>
		<string>Parallel</string>
		<key>Condition</key>
		<string>INSTALLATION_COMPLETE</string>
		<key>Triggers</key>
		<array>
			<string>Update(INSTALLATION_COMPLETE)</string>
		</array>
		<key>Actions</key>
		<array/>
	</dict>
</array>

Our set is ready to execute the DisplayAlert action, so let’s configure it. A DisplayAlert action has three required keys: the string Title key is the alert title, the string Message key is the message displayed below the title, and the string Style key lets us choose the alert style: “Informational”, “Warning”, “Critical”. We will use the “Informational” value, but you are free to test the two others. Here is the reference in the documentation.

<dict>
	<key>Type</key>
	<string>Parallel</string>
	<key>Condition</key>
	<string>INSTALLATION_COMPLETE</string>
	<key>Triggers</key>
	<array>
		<string>Update(INSTALLATION_COMPLETE)</string>
	</array>
	<key>Actions</key>
	<array>
		<dict>
			<key>Type</key>
			<string>DisplayAlert</string>
			<key>Title</key>
			<string>Finished!</string>
			<key>Message</key>
			<string>Everything is installed and your ${DEVICE_MODEL_NAME} is ready to use.</string>
			<key>Style</key>
			<string>Informational</string>
		</dict>
	</array>
</dict>

Now relaunch the application (not reload its interface). Whatever the current displayed monitors, if you navigate to the Admin menu bar and clicks on “Reach end of simulation” (⌥⌘M), all the monitors should be set as “Installed” and the alert should appear in the top of Octory window:

You can customise an alert with many options like adding buttons and associate variables to them. In this tutorial, we will only add a custom icon to use. Let’s first add an “Images” variable, and as you are there, add a “Sounds” which will be useful soon.

<key>General</key>
<dict>
	<key>Variables</key>
	<dict>
		<key>CompanyName</key>
		<string>Acme, Inc.</string>
		<key>Resources</key>
		<string>/Library/Application Support/Octory/Resources</string>
		<key>Images</key>
		<string>${Resources}/Images</string>
		<key>Sounds</key>
		<string>${Resources}/Sounds</string>
		<key>Monitors</key>
		<string>${Resources}/Monitors</string>
	</dict>
</dict>

If you navigate to the Images folder in the provided materials, you will see an image named “acme_logo.png”. It is the image we are going to use. Here is how:

<dict>
	<key>Type</key>
	<string>Parallel</string>
	<key>Condition</key>
	<string>INSTALLATION_COMPLETE</string>
	<key>Triggers</key>
	<array>
		<string>Update(INSTALLATION_COMPLETE)</string>
	</array>
	<key>Actions</key>
	<array>
		<dict>
			<key>Type</key>
			<string>DisplayAlert</string>
			<key>Title</key>
			<string>Finished!</string>
			<key>Message</key>
			<string>Everything is installed and your ${DEVICE_MODEL_NAME} is ready to use.</string>
			<key>Style</key>
			<string>Informational</string>
			<key>IconPath</key>
			<string>${Images}/acme_logo.png</string>
		</dict>
	</array>
</dict>

Now restart the application and use the same option “Reach end of simulation” (⌥⌘M):

Play a sound

You can ask Octory to play a sound with the PlaySound action. It can play a sound from the system, like “Hero”, or play a sound in a file. We will be using the second option. As we want the sound to be played when the installation is finished, we will just add the PlaySound action in the set which already contains the DisplayAlert action. As the Type key of the set has a “Parallel” value, the DisplayAlert and PlaySound actions will be executed concurrently. Let’s do this!

Note: Sorry for the sound which will be played. I could not resist.

<dict>
	<key>Type</key>
	<string>Parallel</string>
	<key>Condition</key>
	<string>INSTALLATION_COMPLETE</string>
	<key>Triggers</key>
	<array>
		<string>Update(INSTALLATION_COMPLETE)</string>
	</array>
	<key>Actions</key>
	<array>
		<dict>
			<key>Type</key>
			<string>DisplayAlert</string>
			<key>Title</key>
			<string>Finished!</string>
			<key>Message</key>
			<string>Everything is installed and your MacBook Pro is ready to use.</string>
			<key>Style</key>
			<string>Informational</string>
			<key>IconPath</key>
			<string>${Images}/acme_logo.png</string>
		</dict>
		<dict>
			<key>Type</key>
			<string>PlaySound</string>
			<key>SoundNameOrFilePath</key>
			<string>${Sounds}/roadrunner.wav</string>
		</dict>
	</array>
</dict>

Finally, if you restart the application and reach the end of the simulation one again, you should here the custom sound (mip mip!).

And that’s all for the first par of this tutorial! Pfeew! So far, you learned:
- how to conditionally insert and remove interface components
- how to conditionally execute a bash command with root rights as well as a Jamf policy
- how to conditionally display a monitor
- how to inform the user when the onboarding process is over

I hope this gave you many ideas to onboard your users with a nice experience, as well as facilitate your workflow. Take a break to think about it. We will soon dive into Octory features as a day-to-day software to watch over your users. Exciting!


Move Octory to the menu bar

Moving Octory to the menu bar offers new possibilities to use the software on a day to day basis. We will see first that it can be useful to display relevant information to the user using placeholders, then how to use it to propose custom actions to the user. Executing them with root privileges if necessary.

You will find a configuration file named Octory-second-part-start.plist. Please use this file now for the second part by moving it to the /Library/Applications Support/Octory/ or /Library/Managed Preferences/, and launch Octory in the command-line with the configuration option:

open Octory.app --args -c Octory-second-part-start.plist

To move Octory to the menu bar, we have to change the key WindowOnScreen to “MenuBar”, and to add a new section in the configuration file, named MenuBarSlide. This MenuBarSlide is a sole slide which will be displayed when clicking on the Octory menu bar icon. Let’s make those changes.

OnScreen key
<dict>
	<key>MaximumSize</key>
	<dict>
		<key>Height</key>
		<integer>960</integer>
		<key>Width</key>
		<integer>1280</integer>
	</dict>
	<key>MinimumSize</key>
	<dict>
		<key>Height</key>
		<integer>768</integer>
		<key>Width</key>
		<integer>1024</integer>
	</dict>
	<key>OnScreen</key>
	<string>MenuBar</string>
	<key>Position</key>
	<dict>
		<key>Horizontal</key>
		<string>Center</string>
		<key>Vertical</key>
		<string>Center</string>
	</dict>
	<key>ScreenIndex</key>
	<integer>2</integer>
</dict>

We will add a simple slide to start: one Text component with two Spacers to center it.

<key>Window</key>
<dict>
...
</dict>

<key>MenuBarSlide</key>
<dict>
	<key>Containers</key>
	<array>
		<dict>
			<key>Components</key>
			<array>
				<dict>
					<key>Type</key>
					<string>Spacer</string>
				</dict>
				<dict>
					<key>Type</key>
					<string>Text</string>
					<key>Text</key>
					<string>Help</string>
					<key>TextFontConfiguration</key>
					<string>Title</string>
				</dict>
				<dict>
					<key>Type</key>
					<string>Spacer</string>
				</dict>
			</array>
		</dict>
	</array>
</dict>

Launch the app now, using open Octory.app --args -c Octory-second-part-start.plist if you did not already run this command once. You should be able to see the Octory icon in your menu bar, and to click on it.

Display relevant information to the user

When an end-user contacts the support to find some help, it might be useful for them to find all relevant information in one place. For example, it is possible to display the Mac serial number and UUID, but also more specific values like the Mac asset-tag if one is set. If we can use Octory placeholders to display the Mac serial number and UUID, retrieving the asset tag would be done by executing a command.

Let’s add those placeholders.

Note

To edit Octory when it is on the Menu bar, it might be painful to have to click on the menu bar icon to reload the interface or see the changes. To make your workflow easier, you might want to add the key AdminIsMenuBarSemiTransient and set its value to true. Thus, the window will not disappear when you click outside of its bounds, which is easier if you have to move the to configuration file often to edit it. When you want to hide the window on the menu bar, right click on the menu bar icon.

Device serial number

Add a new Text component to the Components array in the MenuBarSlide. This component will display the device serial number using a placeholder.

<!-- First text component -->
<dict>
	<key>Type</key>
	<string>Text</string>
	<key>Text</key>
	<string>Help</string>
	<key>TextFontConfiguration</key>
	<string>Title</string>
</dict>
<!-- Label text component: Mac serial number -->
<dict>
	<key>Type</key>
	<string>Text</string>
	<key>Text</key>
	<string>Mac serial number</string>
	<key>TextFontConfiguration</key>
	<dict>
		<key>Style</key>
		<string>Body</string>
		<key>SystemFontWeight</key>
		<string>Semibold</string>
	</dict>
	<key>Margins</key>
	<dict>
		<key>Top</key>
		<integer>10</integer>
	</dict>
</dict>
<!-- Placeholder text component: Mac serial number -->
<dict>
	<key>Type</key>
	<string>Text</string>
	<key>Text</key>
	<string>${DEVICE_SERIAL_NUMBER}</string>
	<key>TextFontConfiguration</key>
	<string>Body</string>
	<key>Margins</key>
	<dict>
		<key>Top</key>
		<integer>5</integer>
	</dict>
</dict>

Now once you click on the menu bar icon and the window appeared, reload the application interface with ⌘R.

Device UUID and user name

And now, it’s your turn! Insert two new components to display the placeholders DEVICE_UUID and USER_NAME. You can simply copy and paste the Text component we just added. Once your done, here is how the application should look like.

And here is my solution.

<!-- Label text component: Mac UUID -->
<dict>
	<key>Type</key>
	<string>Text</string>
	<key>Text</key>
	<string>Mac UUID</string>
	<key>TextFontConfiguration</key>
	<dict>
		<key>Style</key>
		<string>Body</string>
		<key>SystemFontWeight</key>
		<string>Semibold</string>
	</dict>
	<key>Margins</key>
	<dict>
		<key>Top</key>
		<integer>10</integer>
	</dict>
</dict>
<!-- Placeholder text component: Mac UUID -->
<dict>
	<key>Type</key>
	<string>Text</string>
	<key>Text</key>
	<string>${DEVICE_UUID}</string>
	<key>TextFontConfiguration</key>
	<string>Body</string>
	<key>Margins</key>
	<dict>
		<key>Top</key>
		<integer>5</integer>
	</dict>
</dict>
<!-- Label text component: User name -->
<dict>
	<key>Type</key>
	<string>Text</string>
	<key>Text</key>
	<string>User name</string>
	<key>TextFontConfiguration</key>
	<dict>
		<key>Style</key>
		<string>Body</string>
		<key>SystemFontWeight</key>
		<string>Semibold</string>
	</dict>
	<key>Margins</key>
	<dict>
		<key>Top</key>
		<integer>10</integer>
	</dict>
</dict>
<!-- Placeholder text component: User name -->
<dict>
	<key>Type</key>
	<string>Text</string>
	<key>Text</key>
	<string>${USER_NAME}</string>
	<key>TextFontConfiguration</key>
	<string>Body</string>
	<key>Margins</key>
	<dict>
		<key>Top</key>
		<integer>5</integer>
	</dict>
</dict>
While I am writing this tutorial, I realise it would be cool to have a “Copy” button for each placeholder displayed, so that your end-user can simply click on it to copy the information. I tried to do it with an ExecuteCommand but using the Helper seems to prevent to correctly use pbcopy. Thus, a CopyClipboard action will be added in a future release to make this possible.

Execute a support command

As we are talking about support, let’s now add a small useful feature to let your users contact the support by email. This will be done with an ExecuteCommand command action which opens a link: mailto:support@acme.com

To execute this command, let’s add a Button component with an ActionSet. This is done by specifying the OnClick key, which is a single ActionSet. The button should be added at the bottom. We will also add a spacer with a Height to 25 to ventilate the layout a bit. Here is what it looks like:

<dict>
	<key>Type</key>
	<string>Spacer</string>
</dict>
<dict>
	<key>Type</key>
	<string>Button</string>
	<key>Text</key>
	<string>Contact Support</string>
	<key>Style</key>
	<string>Simple</string>
	<key>Alignment</key>
	<string>Center</string>
	<key>OnClick</key>
	<dict>
		<key>Type</key>
		<string>Parallel</string>
		<key>Actions</key>
		<array>
			<dict>
				<key>Type</key>
				<string>ExecuteCommand</string>
				<key>Command</key>
				<string>open mailto:support@acme.com</string>
			</dict>
		</array>
	</dict>
</dict>
<dict>
	<key>Type</key>
	<string>Spacer</string>
	<key>Height</key>
	<integer>25</integer>
</dict>

If you reload the interface, you should see the button. And if you click on it, your default mail client should open with the given destination address.

This could be convenient for your users when they do not remember the correct support email address!


Summary

This was a huge tutorial! I hope you found it useful. If you have any questions, do not hesitate to reach me on the admin slack channel, or ask them in the Octory channel.

See you soon on your Octory journey!

Author

Alexis Bridoux
Octory Lead developer & Product owner
Email
Github