→ Automate ArcGIS Installs: Download Script Here ←
This is a controversial opinion, I know, but I’m just going to say it: I hate installing software.
More to the point — I hate waiting for software to install.
The Truth about ArcGIS Installs
Unfortunately, a common scenario I find is that I need to spin up a brand new machine for development, which means I’ll need to install — at minimum — ArcGIS for Desktop, a database client, an IDE and any custom code we have written for the client in the past.
This flurry of installation will then be followed by applying updates to all the software I’ve installed. In addition, this will be followed by installing enhancements and add-ons to various programs, configuring licensing, creating database connections in both the database client and ArcCatalog …
… and other boring, repetitive actions that really seem better suited to some sort of automaton.
Like, say, it’s a computer! So why to automate installs?
Enter Pywinauto for ArcGIS Installs
Pywinauto is a python package that allows you to manipulte and automate GUI controls in Windows programattically. Anything you can do with a mouse and keyboard, you can automate with pywinauto. This will help us automate ArcGIS installs. We can just let our fancy script run and spend that extra time on something more important, like looking at cat pictures on the internet getting more work done. It’s a brave new world, and we’re the people in it!
Recommended Prerequisites to automate ArcGIS installs:
Software:
- Windows 10
- Python 3.5.2
- ArcGIS Desktop 10.2.1.3497 installer
Python Packages:
I’ll be attempting to demonstrate the power of python for automating environment setup by doing three things: extracting the ArcGIS Desktop installation files from their compressed form; running the installer; and pointing ArcGIS Administrator to a license server.
Setting Up the Project to Automate ArcGIS Installs
The first thing to do to automate ArcGIS installs is set everything up before we get down to business. You’ll probably want to be working in an IDE while coding – I like JetBrains PyCharm Community Edition, which is a free, fully featured IDE.
I’ll be assuming the availability of IDE features throughout this article, so it would be helpful. You should also probably create a new VirtualEnv for this project, and install the requisite python packages as outlined in “Recommended Prerequisites” above. We’ll want to add on to these environmental setup procedures in the future, so we want to keep the code modular and generalizable.
So, we’re going to create two python files. These are “main.py” and “arcmap_installer.py.”
The main.py file will kick everything off and call everything in order. Right now, that will just be our ArcMap installer, but in the future we might add things like installing and configuring an Oracle client. We could add pulling down custom code from a source control server.
The arcmap_installer.py file, meanwhile, will do all the actual work.
Here’s main.py:
import arcmap_installer
arcmap_installer.run()
And here’s the skeleton of arcmap_installer.py:
import os import sys import pywinauto import psutil import subprocess
from pywinauto import timings
def run():
Extracting the Installer
Okay, we are one step closer to automating ArcGIS installs. Next, let’s extract the installer.
ArcGIS Desktop comes in a compressed format that must be extracted before it can be installed. Sounds boring to me, so let’s automate it!
First we’ll get a copy of the compressed ArcGIS installer file – mine’s called “ArcGIS_Desktop_1021_139036.exe.”
We’ll put it in the same directory as main.py and arcmap_installer.py so it will be easy to get to. Add the file’s name as a private variable in the file so we can reference it.
from pywinauto import timings
_desktopExtractorExeName = r’ArcGIS_Desktop_1021_139036.exe’
def run():
Then we’ll make a function that will extract the installation files, and call it from the run() function:
def run(): os.chdir(os.path.join(os.getcwd(), os.path.dirname(sys.argv[0]))) desktop_installer_full_path = os.getcwd() + r'\Desktop\SetupFiles\setup.msi' if not os.path.isfile(desktop_installer_full_path): extract(_desktopExtractorExeName)
def extract(exe_name):
extractor = pywinauto.Application().Start(exe_name)
return
We want to do all our boring work out of the way of the rest of the files on the computer. So, the first line in run() sets the default directory for this program to the directory where the program files reside.
The next two lines check if the installer MSI file already exists in its expected location, so we can skip this step if we have already extracted the files.
The first line in extract() is where the business that lets us automate ArcGIS installs really begins. The pywinauto.Application().Start() will call the specified executable and return an object representing the resulting application.
The next line — “return” — exists only so we can place a breakpoint after the first line but before we leave the scope of extract().
At this point, we run into a problem. We want to manipulate the extractor GUI, but we don’t know what the GUI looks like, or what the controls on it are called. We’ll need to do a test run to figure these things out, so we’ll put a breakpoint on “return” and run the script.
The script open the extractor application and its associated window, and then stop on our breakpoint.
This screen is asking where we’d like to extract our files to. But we want to extract them to the same directory our program is running in. Meanwhile, the default is set to the Documents folder where it will be nothing but clutter.
So we want to do two things here: We want to type the full path of our current directory, and we want to click the OK button. So we need to find out what the names of the textbox and the OK button are.
To resolve this mystery, we’ll want to bring up the python interpreter that is connected to our running script.
Most IDEs come with such an interpreter built-in, so open that up (Alt-F8 in PyCharm). You should be able to access the extractor application by typing this:
>>> extractor
And then hit enter. But this doesn’t tell us much other than that the application object exists. What we really want is the current application window, so we’ll try this:
>>> extractor.top_window_().Exists()
… which will return a boolean value indicating if the window exists or not. Assuming this evaluated to “true,” let’s get what we came for:
>>> extractor.top_window_().print_control_identifiers()
This method will print a list of the control identifiers for the window to the output console.
Each control looks something like this:
obj_BUTTON - 'b'&OK'' (L1014, T572, R1095, B597) 'b'&OK'' 'b'&OKobj_BUTTON'' 'b'obj_BUTTON'' 'b'obj_BUTTON0'' 'b'obj_BUTTON1''
The first line lists the type of control found (in this case, a button). It also contains the text found in the control (‘OK’ – &’s seem to be inserted randomly). And finally, the first line has the location of the control.
Subsequent lines list string options for identifying the control.
Pywinauto provides two methods for accessing a control object. If the control name is simple, like “obj_BUTTON”, it can be accessed as a property of the Window object, like this:
extractor.top_window_().obj_BUTTON
Alternatively, for more difficult names (for example, containing spaces or starting with a non-alphabetical character), the control can be accessed as a dictionary item, like this:
extractor.top_window_()[r'&OKobj_BUTTON']
So, using the list of control identifiers we got, we can add two lines to extract() like this:
def extract(exe_name): extractor = pywinauto.Application().Start(exe_name) extractor.top_window_().obj_Edit.TypeKeys(_selectAllCmd + os.getcwd()) extractor.top_window_().obj_BUTTON.Click()
The first new line uses the “TypeKeys()” method which, unsurprisingly, emulates typing keys on the keyboard.
Since there is text loaded by default into the textbox we are manipulating, we want to select it all first so that it will be overwritten when we type other keys.
Since this seems like a pretty common thing that we’ll want to do, we can put the keystrokes necessary to select the text in a variable at the top of the file:
_desktopExtractorExeName = r'ArcGIS_Desktop_1021_139036.exe' _selectAllCmd = r'^a'
def run():
Now this line will select all the text in the textbox defined by the control name ‘obj_Edit’ and replace it with the full path of the current directory we are in.
The final line, also unsurprisingly, clicks the ‘OK’ button, which will bring us to the next screen.
This next screen doesn’t require much of us. However, it presents its own challenge.
Namely, we need to figure out how long to wait until we can start working again. We don’t get much time to think of how to resolve this issue until the next screen presents itself.
Calling “extractor.top_window_().print_control_identifiers()” again gets us the names of all the controls on the final screen.
Now we have what we need to know how long to wait. Let’s make a new function:
def _top_window_contains_control(app, control_name): try: return app.top_window_()[control_name].Exists() except: return False
This function takes an application object and a control name as arguments. It then returns a boolean indicating if the top window of the application contains a control matching the control name.
Then we can use this function in the WaitUntil() function of pywinauto’s timings module.
timings.WaitUntil(60, 1, lambda: _top_window_contains_control(extractor, r'Launch the setup program.'))
WaitUntil() takes a boolean function or method – in our case, _top_window_contains_control(). It then tests this periodically for a given period of time.
Here, we are testing our condition every second until we reach 60 seconds. Once the function returns “true,” WaitUntil() will stop waiting and program execution will continue.
If the function doesn’t return “true” before our 60 seconds are up, however, it will throw an error and alert us that something has gone wrong (or is taking an unexpectedly long time).
Finally, we want to close out the final screen.
By default, the “Launch the setup program” checkbox is checked, but we want some more control here, so we’ll uncheck it before closing the form.
timings.WaitUntil(60, 1, lambda: _top_window_contains_control(extractor, r'Launch the setup program.'))
extractor.top_window_()[r’Launch the setup program.’].Click()
extractor.top_window_()[‘&Close’].Click()
Running the Installer
Great! Your installer is ready to run. The next task to automate ArcGIS installs is to actually run our installer. So let’s make a new function to do that, and call that function from run().
def install(msi_full_path):
Once again, we’ll pick a file we expect to exist after this function completes in order to determine if we should actually run it. The ArcMap executable should work, so we’ll use that.
_selectAllCmd = r'^a' _desktopExeFullPath = r'C:\Program Files (x86)\ArcGIS\Desktop10.2\bin\ArcMap.exe'
def run():
os.chdir(os.path.join(os.getcwd(), os.path.dirname(sys.argv[0])))
desktop_installer_full_path = os.getcwd() + r’\Desktop\SetupFiles\setup.msi’
if not os.path.isfile(desktop_installer_full_path):
extract(_desktopExtractorExeName)
if not os.path.isfile(_desktopExeFullPath):
install(desktop_installer_full_path)
Now, to save ourselves time, we’re going to diverge from the coding-heavy process of working through the installer GUI screens. Instead we will use some built-in Windows functionality to install ArcMap without showing any screens at all.
def install(msi_full_path): cmd = r"msiexec.exe /qn /i " + msi_full_path + " /l* desktop.log" subprocess.Popen(cmd)
The msiexec you see is a tool for giving users extra control over MSI installers.
Here, we’re telling msiexec to run the MSI file defined by “msi_full_path” and also that we want it to install (/i). Furthermore, we are telling msiexec that we want it to run without a GUI (/qn), and that we want it to log all its actions to a file named “desktop.log” (/l* desktop.log). Then we call this command in Python using “subprocess.Popen().”
When we run this code, though, we run into another issue. Normally, Python would track a process called via Popen(), and we would be able to wait for the process to complete by doing something like this:
installer = subprocess.Popen(cmd) installer.wait()
However, if we try this out, we find that “installer.wait()” returns surprisingly quickly. Taking a quick glance at the Task Manager, we find that msiexec.exe (aka “Windows Installer”) is still running in the background, and will continue to run for quite a while, as the ArcGIS Desktop installer is apt to do.
It appears that msiexec is detaching itself from our humble script, and is running completely on its own. We want to know when the installer has finished running so that we can finish setting up our environment, so we’ll pull in some functionality from psutil package.
"msiexec.exe" in [psutil.Process(i).name() for i in psutil.pids()]
This line of code will check if there are any processes named “msiexec.exe” currently running.
Then, we can use “timings.WaitUntil()” again to wait until the process no longer exists.
timings.WaitUntil(1200, 60, lambda: not ("msiexec.exe" in [psutil.Process(i).name() for i in psutil.pids()]))
The ArcGIS Desktop installer takes a while, so we’ll just check on it every minute for 20 minutes. Once it’s done, we can move on to pointing to our license.
Almost Done: Pointing to the License Server
Finally, we’d like to be able to run ArcMap without needing to configure anything – another task for pywinauto.
We’ll create another function, and a variable to point the program to the ArcGIS Administrator application.
_licenseServerName = '127.0.0.1' _desktopProductName = 'Advanced (ArcInfo) Concurrent Use'
_desktopExeFullPath = r’C:\Program Files (x86)\ArcGIS\Desktop10.2\bin\ArcMap.exe’
_arcAdminFullPath = r’C:\Program Files (x86)\Common Files\ArcGIS\bin\ArcGISAdmin.exe’
_selectAllCmd = r’^a’
def run():
os.chdir(os.path.join(os.getcwd(), os.path.dirname(sys.argv[0])))
desktop_installer_full_path = os.getcwd() + r’\Desktop\SetupFiles\setup.msi’
if not os.path.isfile(desktop_installer_full_path):
extract(_desktopExtractorExeName)
if not os.path.isfile(_desktopExeFullPath):
install(desktop_installer_full_path)
point_to_license(_licenseServerName, _desktopProductName)
def point_to_license(license_server, desktop_product): arc_admin = pywinauto.Application().Start(_arcAdminFullPath)
The first time we open ArcGIS Administrator, it is in wizard mode, which looks like this:
We want to select the type of license we have, type the name of our license server, and then click OK. Using the same method used in “Extracting The Installer”, we can flesh out the point_to_license() function:
def point_to_license(license_server, desktop_product): arc_admin = pywinauto.Application().Start(_arcAdminFullPath)
arc_admin.top_window_()[desktop_product].Click()
arc_admin.top_window_()[r’Define a License Manager Now:RadioButton’].Click()
arc_admin.top_window_()[r’Define a License Manager now:Edit’].TypeKeys(_selectAllCmd + license_server)
arc_admin.top_window_()[r’OKButton’].Click()
Grand Finale
And just like that …
ArcMap is up and running. We have been able to automate ArcGIS installs.
Full Code
main.py:
import arcmap_installer arcmap_installer.run()
arcmap_installer.py:
import os import sys import pywinauto import psutil import subprocess from pywinauto import timings # these would be better put in a config file, # but this article is already long enough _desktopExtractorExeName = r'ArcGIS_Desktop_1021_139036.exe' _licenseServerName = '127.0.0.1' _desktopProductName = 'Advanced (ArcInfo) Concurrent Use' _desktopExeFullPath = r'C:\Program Files (x86)\ArcGIS\Desktop10.2\bin\ArcMap.exe' _arcAdminFullPath = r'C:\Program Files (x86)\Common Files\ArcGIS\bin\ArcGISAdmin.exe' _selectAllCmd = r'^a' def run(): os.chdir(os.path.join(os.getcwd(), os.path.dirname(sys.argv[0]))) desktop_installer_full_path = os.getcwd() + r'\Desktop\SetupFiles\setup.msi' if not os.path.isfile(desktop_installer_full_path): extract(_desktopExtractorExeName) if not os.path.isfile(_desktopExeFullPath): install(desktop_installer_full_path) point_to_license(_licenseServerName, _desktopProductName) def extract(exe_name): extractor = pywinauto.Application().Start(exe_name) extractor.top_window_().obj_Edit.TypeKeys(_selectAllCmd + os.getcwd()) extractor.top_window_().obj_BUTTON.Click() # Wait a maximum of 60 seconds for extraction to complete timings.WaitUntil(5000, 1, lambda: _top_window_contains_control(extractor, 'Launch the setup program.')) # Checkbox is checked by default. Uncheck - :func:'install' extractor.top_window_()[r'Launch the setup program.'].Click() extractor.top_window_()['&Close'].Click() def install(msi_full_path): # call msiexec.exe # /qn - quiet install - don't show the user interface # /i - do and install # /l* - log everything cmd = r"msiexec.exe /qn /i " + msi_full_path + " /l* desktop.log" subprocess.Popen(cmd) # msiexec detaches and runs as an independent process. # The ArcMap installer takes a while, so check every minute to see if the installation is complete # Put it in a try block in case the process ends while we are iterating through pids and processes try: timings.WaitUntil(1200, 60, lambda: not ("msiexec.exe" in [psutil.Process(i).name() for i in psutil.pids()])) except psutil.NoSuchProcess: return def point_to_license(license_server, desktop_product): arc_admin = pywinauto.Application().Start(_arcAdminFullPath) arc_admin.top_window_()[desktop_product].Click() arc_admin.top_window_()[r'Define a License Manager Now:RadioButton'].Click() arc_admin.top_window_()[r'Define a License Manager now:Edit'].TypeKeys(_selectAllCmd + license_server) arc_admin.top_window_()[r'OKButton'].Click() def _top_window_contains_control(app, control_name): try: returnapp.top_window_()[control_name].Exists() except: return False
What do you think?