StartupLoader: running startUp scripts in Pharo

Hi. Some time ago, I wrote a post about how I build images for my projects. I am downloading new images all the time and because of that I used to have 2 problems: 1) I needed to load several external packages to the standard Pharo image; 2) I needed to set my own settings and preferences. Today, Pharo 2.0 (which is in development and unstable state) includes most of the packages I always needed to install: shout, code completion, nice browser (Nautilus or OB), refactoring integration, spotlight, etc.  So nowadays I only had problem 1).

In this post, I will show you how I solve that problem using StartupLoader, a nice utility present in Pharo (since Pharo 1.4). IMPORTANT: Everything I mention in this blog is taking into account the “new version” I did of StartupLoader. Before writing this post I have improved it and therefore you need at least a Pharo 2.0 version 20071.

UPDATE: Stephane Ducasse read,  converted and improved this blog post into a new chapter for a future Pharo book. You can find a draft here StartupPreferences.pier.pdf

Why we need StartupLoader?

How can we execute something at startup with Pharo? There have traditionally been two known ways (and now in Pharo there is a new third option and it is the reason of this post):

1) Send a .st file as parameter to the VM, that is, you execute the VM like this:

/Users/mariano/Pharo/VM/Pharo.app/Contents/MacOS/Pharo /Users/mariano/Pharo/images/Pharo-1.4.image startup.st

You execute the VM, pass the Pharo image as parameter and then the file. This will be executed during the startup. What’s the problem with this? If I want to always execute the startup in different images I always need to open the image this way, from command line. I just want to open an image with a double-click. Moreover, this file is hand-coded and not versioned in Monticello (what I would like). Besides, there are more limitations as I mention later in this blog.

2) Register your own class in Smalltalk startup list and implement #startUp message to do whatever you want to do. The problem is that my class is not present in the distributed images of Pharo. Therefore I need to manually first load my own code. Same problem: too much work.

New new tool

Remember my problem: I am downloading new images all the time. Having to manually set up my preferences is boring and time-consuming.

The new StartupLoader class searches for and executes .st files from certain locations.  To find these it searches for a ‘.config’ folder in the folder where the image file sits.  Then it looks in the next folder up, then up again and so on until reaching the root folder.  When a ‘.config’ folder is found, StartupLoader looks within this for a ‘pharo’ folder. This contains the startup scripts common to all versions of Pharo, and also optionally a folder per Pharo version holding startup scripts suitable for that version only.  So a typical directory layout might be…

.../some/folders/pharo/Content/Resources/pharo.image.
.../some/folders/pharo/Content/Resources/startup.st
.../some/folders/.config/pharo/author.st
.../some/folders/.config/pharo/useSharedCache.st
.../some/folders/.config/pharo/1.4/mystartupFor14only.st
.../some/folders/.config/pharo/2.0/mystartupFor20only.st
(**Note however that ‘.config’ is an invalid filename on Windows, so ‘..config’ is used instead)

IMPORTANT: I said that StartupLoader will search for a folder ‘.config’ starting from the image directory until the root of the filesystem. What happens if no folder is found? It creates ‘.config’ in the image folder. However, I recommend you create the ‘.config’ following the standard, that is, in the $HOME.

To know the real values for you…
Print the result of “FileDirectory preferencesGeneralFolder” which holds the startup scripts common to all versions of Pharo.
Print the result of “FileDirectory preferencesVersionFolder” which holds the startup scripts specific to the version of the current image.

The order of the search is from the most general to the most specific:

  1. General preferences folder: This is general for all Pharo versions This folder is shared for all the images you open. In my case (in MaxOSX) and Pharo 2.0, it is ‘/Users/mariano/.config/pharo/’. In this place, StartupLoader will load ALL existing .st files. This type of startup is useful when we have something to execute for all images of all Pharo versions.
  2. Preference version folder: This is a specific folder for a specific Pharo version. In my case it is  ‘/Users/mariano/.config/pharo/2.0/’. This type of startup is useful when we have something to execute for all images of a specific Pharo version.
  3. The image folder: The startup only searches for a file called ‘startup.st’. So if you have such a file in the same directory where the image is, then such script will be executed automatically. This type of startup is usually used to do something that is application-specific or something that only makes sense for the specific image you are using. Now you might ask why we don’t search the image folder for multiple .st files.  This is because it is normal for the image folder to contain .st files not related to started – such as from any file out.  Using one specific file ‘startup.st‘ avoids this while still allowing an image delivered to a client to run a script upon execution on a new system. Be careful if you already were sending your own ‘startup.st’ as parameter to the VM because it will be executed twice 😉

As you can see the order is from the most general to the most specific. Moreover, it does not stop when it finds files in any of them. So all are searched and executed. More specific scripts can even override stuff set in more general ones. It works more or less the same way as variables in UNIX with .bashrc /etc/envirorment, etc…

So you know where the system will search startup files. Now you can directly put your code there and it will be automatically executed during startup. Great!!! So that’s all?  we just write scripts there?  Of course not! 😉

StartupActions

Directly putting code in the files is easy, however, it is not the best choice. For example, what happens if there are certain scripts you want to execute only once on a certain image but some other code that you want to execute each time the image starts? To solve that, among other things, we have the reification of StartupAction. Let’s see how we use them:

| items |
items := OrderedCollection new.
items add: (StartupAction
name: 'Basic settings'
code: [
Author fullName: 'MarianoMartinezPeck'.
Debugger alwaysOpenFullDebugger: true.
]).
StartupLoader default addAtStartupInPreferenceVersionFolder: items named: 'basicSettings.st'.

What we do first is to create an instance of StartupAction using the message #name:code:. We pass as argument a name and the Smalltalk code we want to run inside a block closure. In this example, I just set my username and I put a setting to always open the debugger (no pre-debugger window). So far nothing weird.

The magic comes with the last line, the message #addAtStartupInPreferenceVersionFolder: items named: aFileName  receives a list of startup actions and a filename and stores the actions in a file with the passes argument. So in this case we have only one action called ‘Basic Settings’ and it will be placed in a file called ‘basicSettings.st’. But where? in which of the 3 folders described previously is it placed? well…it depends on the message. In this case, we used the #addAtStartupInPreferenceVersionFolder:named:  (notice the “InPreferenceVersionFolder”). So it put the files in 2). In addition, you can use the messages #addAtStartupInGeneralPreferenceFolder:named: which stores files in 1) and #addAtStartupInImageDirectory: which stores in 3). Notice that with the first two messages we can specify the file name but with the last one we can’t. Remember the last one is always called ‘startup.st’. If you are lazy and don’t want to think the name yourself, you can just use #addAtStartupInPreferenceVersionFolder: which creates a file called ‘startupPharoNN.st’ or #addAtStartupInGeneralPreferenceFolder: that creates a file named ‘startupPharo.st’.

StartupLoader

We saw that when executing the message #addAtStartupInPreferenceVersionFolder: items named:aFilename or any of its variant, a file is created with the code we want to evaluate. Then, when the system starts it will find our file and execute our code. But, how is the resulting file? Exactly as the code we provided? No! Look how our example file ‘/Users/mariano/.config/pharo/2.0/basicSettings.st’ is generated:

StartupLoader default executeAtomicItems: {
StartupAction name: 'Basic settings' code: [Author fullName: 'MarianoMartinezPeck'.
Debugger alwaysOpenFullDebugger: true].
}.

So as you can see, the file is generated by sending a collection of actions to “StartupLoader default executeAtomicItems:”. In this example the collection has only one action, but it would have more if our example has more. So now the StartupLoader will execute all the actions found in the file. Do we execute all actions? No! Actions can be built with a property of “runOnce”. So if an action has already been executed in the current image before the last save, it is not executed again. Executed actions are stored in the singleton instance #default of StartupLoader. Therefore, you have to save the image. If an action generates an error the action is NOT registered as executed. In addition, errors are also stored in the singleton of StartupLoader so you can query them after starting the image by inspecting the result of “StartupLoader default errors”.

Advanced example

As an advanced example, I want to show you the script I am using for my images. For that, I have this Smalltalk code:

setPersonalStartUpPrefernces
"self setPersonalStartUpPrefernces"
| items |
items := OrderedCollection new.

items add: (StartupAction name: 'General Preferences for all Pharo versions' code: [
FileStream stdout lf; nextPutAll: 'Setting general preferences for all Pharo versions'; lf.
Author fullName: 'MarianoMartinezPeck'.
FileStream stdout lf; nextPutAll: 'Finished'; lf.
]).
StartupLoader default addAtStartupInGeneralPreferenceFolder: items named: 'generalSettings.st'.

items add: (StartupAction name: 'Settings' code: [
FileStream stdout lf; nextPutAll: 'Setting general preferences'; lf.
UITheme currentSettings fastDragging: true.
CodeHolder showAnnotationPane: true.
MCCodeTool showAnnotationPane: true.
Deprecation raiseWarning: true.
Debugger alwaysOpenFullDebugger: true.
Parser warningAllowed: false.
FileStream stdout lf; nextPutAll: 'Finished'; lf.
]).
StartupLoader default addAtStartupInPreferenceVersionFolder: items named: 'settings.st'.

items removeAll.
items add: (StartupAction name: 'Nautilus' code: [
FileStream stdout lf; nextPutAll: 'Executing Nautilus related stuff'; lf.
Nautilus pluginClasses add: { NautilusBreadcrumbsPlugin. #top }.
Nautilus pluginClasses add: { AnnotationPanePlugin. #middle }.
FileStream stdout lf; nextPutAll: 'Finished'; lf.
] runOnce: true).
StartupLoader default addAtStartupInPreferenceVersionFolder: items named: 'nautilus.st'.

items removeAll.
items add: (StartupAction name: 'Monticello related stuff' code: [
| sharedPackageCacheDirectory |
FileStream stdout lf; nextPutAll: 'Executing Monticello related stuff'; lf.
sharedPackageCacheDirectory := (FileDirectory on: '/Users/mariano/Pharo/localRepo/')
assureExistence;
yourself.
MCCacheRepository default directory: sharedPackageCacheDirectory.
MCDirectoryRepository defaultDirectoryName: '/Users/mariano/Pharo/localRepo/'.
(MCRepositoryGroup default  repositories
select: [:each | (each isKindOf: MCHttpRepository)
and: [((each locationWithTrailingSlash includesSubString: 'www.squeaksource.com')
or: [each locationWithTrailingSlash includesSubString: 'http://ss3.gemstone.com/ss/'])]
]) do: [:each |
each
user: 'MMP';
password: ((FileDirectory default oldFileNamed: '/Users/mariano/Pharo/repositoriesPassword.txt') contents).
].
FileStream stdout lf; nextPutAll: 'Finished'; lf.
]).
StartupLoader default addAtStartupInPreferenceVersionFolder: items named: 'monticello.st'.

Basically, I have 4 files to customize stuff: 1) general settings;  2) settings for Pharo 2.0; 3) nautilus and 4) monticello related stuff. 1) is for all Pharo versions. So far I am just setting my username. 2) 3) and 4) are for Pharo 2.0 (just because I know they work in Pharo 2.0 and I am not sure if they work in other versions). For nautilus, I don’t want to add the plugins each time (because it would add the plugin several times) so I create a StartupAction using the message #name: nameOfItem code: code runOnce: aBoolean  passing a true to aBoolean.

Using the tool

How to split your stuff in files and actions?

So you may have noticed that: a) #addAtStartupInPreferenceVersionFolder: and friends expect a list of actions; b) you can have multiples files. So, how do you split your code? From what I can see in the framework, there is no restriction. You can have as many actions per files and as many files as you wish. An action has a block of closure that can contain as much code as you want.

I found that one way of splitting your code is when some actions need to be executed only once and some other each time. Another reason may be some code which may be expected to fail for some reason. If it fails, the code after the line that generated the error won’t be executed. Hence, you may want to split that code to a separate action.

How to version and work with this tool?

The way I found to work with this stuff is to have my own class GeneralImageBuilder (put whatever name you want). In such class I have the mentioned method #setPersonalStartUpPrefernces (from the advanced example). So I use Monticello to save and load that project. Then, whenever I want to create the script files and add them to their correct directory, I just evaluate that method.

Be careful with the cache!

In order to support the “runOnce:”, actions are stored in the singleton instance of StartupLoader. After an action is executed (if executed correctly), the action is stored and marked as “executed”. It may happen that later on you modify the scripts by hand, or you change the rules and re-store them or some kind of black magic. So…if you change some of these, I recommend to do 2 things:

  • Remove all existing files from the preference directories (no script is removed automatically). Check methods #remove* in StartupLoader.
  • Remove the existing stored actions in StartupLoader. Check method #cleanSavedActionsAndErrors and #removeAllScriptsAndCleanSavedActions.

Conclusion

I think that the tool is very nice. It is just 3 classes and a very few methods. I have been improving it recently but still, there could be more improvements. Wanna help?  I wanted to summarize this post and write a better class comment, but I am running out of free time. In addition, it would be nice to have some tests 😉

I want to thank to Benjamin Van Ryseghem for doing the first version of the tool and to Ben Coman for fixing the preference folder in Windows and for discussing with me about the performed improvements.

Hope you like it!


10 thoughts on “StartupLoader: running startUp scripts in Pharo

  1. There are many great things happening in Pharo that are a bit under-the-hood so its not so obvious, but this is one that directly makes life a bit easier. For me just setting the author and shared cache is well worth it.

    Thanks for the detailed write-up

    A useful tip that I just discovered… I was going to ask, “Do you know how to programmatically turn off OCompletion of SmartCharacters,” but then noticed that browsing [World > System > Settings] to [Code browsing > Code Completion… > Smart Characters], then going [contextMenu > Browse] brings up the method that controls this setting. In this case (NECPreferences class >> settingsOn:) shows this to be #smartCharacters, and executing (NECPreferences smartCharacters: false) in Workspace does indeed turn it off. I assume this would work similar for most other settings.

    cheers -ben

    Like

    1. Hi Ben. Regarding the settings, I have in mind a post in which I will write down all the “Pharo tips” that I know that not everybody may know. I already have the list, I need to write them now hahah. One of the items was exactly that: how to get the script/code of a Setting from the setting browser in order to script your preferences. It looks you found it 🙂

      Like

Leave a reply to Ben Coman Cancel reply