2

I am trying to create an installer for a program that uses the pywin32 bindings to edit some excel spreadsheets. I have created an executable using py2exe and everything works when running the executable out of a folder on the desktop. However, I wish to be able to distribute a single installer file that will install the program into C:\Program Files\ or equivalent folder on whatever system. I have also succeeded in this, however, when the pywin32 bindings are used they create temporary files wherever the working directory is.

This is highly problematic as newer versions of windows have made it so only administrators have permission to write to these directories. Because of this when the app is run from these directories it fails with the error:

WindowsError: [Error 5] Access is denied: 'C:\\Program Files (x86)\\DataPlotter\\.\\win32com\\gen_py\
\00020813-0000-0000-C000-000000000046x0x1x6'

Changing the app to run with administrator permissions is a bad solution as it can introduce vulnerabilities.

Does anybody know of a fix to this problem or how to change the location that the pywin32 bindings use as a temporary file location.

Aaron S
  • 711
  • 2
  • 10
  • 23
  • You might be able to change the temp file location by setting the TEMP and TMP environment variables. – martineau Aug 20 '12 at 20:39
  • It is trying to write to `C:\Program Files(x86)\{Program Name}\.\win32com\gen_py\. I took a look at the source code and as far as I can tell it just creates a folder wherever the current working directory is rather than writing to any temp file. (Perhaps if I tried changing the working directory just before calling pywin32? I'm going to try this and report back.) – Aaron S Aug 20 '12 at 20:50
  • It really should be using the win32 [`GetTempPath`](http://msdn.microsoft.com/en-us/library/windows/desktop/aa364992%28v=vs.85%29.aspx) function which _is_ affected by the value of environment variables. Since that's not the case, your workaround may be the only alternative (if it works). You could change the cwd to the value of the TEMP environment variable. ` – martineau Aug 20 '12 at 21:00
  • 1
    I know, but whoever wrote it decided that they knew better. Anyway the workaround works. I'll write it up and submit it as an answer. – Aaron S Aug 20 '12 at 21:06
  • Good to hear/know. Actually you should call `GetTempPath` yourself and set the current working directory to the value returned. – martineau Aug 20 '12 at 21:08
  • I did. It seemed by far the most prudent thing to do. – Aaron S Aug 20 '12 at 21:13

2 Answers2

1

The way I described in my comment to your answer about how to turn your code into a context manager using the contextlib.contextmanager decorator was slightly oversimplified if you want the previous current directory restored even when an unhandled exception occurs. To make that happen requires also adding a try...finally clause around the yield (see below).

Also, I think it would be even better to make it a standalone function rather than a method of some class as well as pass it the directory to which to switch as an argument -- both of which make it more generic and readily reused. I've called it pushd() because of the similarities it has with the Windows and Unix shell command of the same name.

from contextlib import contextmanager
import os
import tempfile

@contextmanager
def pushd(dir=None):
    """ Context manager which saves the current working directory before
        changing either to the one passed or the default folder for temporary
        files and then restores the former after the controlled suite of
        statements have executed. This will happened even if an unhandled
        exception occurs within the block. """
    cwd = os.getcwd()
    os.chdir(dir if dir is not None else tempfile.gettempdir())
    try:
        yield
    finally:
        os.chdir(cwd)

# sample usage

with pushd():
    self.xl = win32com.client.gencache.EnsureDispatch("Excel.Application")

It might also be useful to do a yield cwd instead of nothing which would allow any optional as variable to receive a value indicating the previous current working directory for possible reference inside the block.

martineau
  • 119,623
  • 25
  • 170
  • 301
  • I see what you mean but I don't see anywhere in the code that would require this code to be reused with another directory. (At least at the moment) I see how the try catch block is useful. The documentation was misleading and seemed to imply that the __exit__ function of a context-manager would be passed the exception before exiting, allowing it to execute. – Aaron S Aug 21 '12 at 19:46
  • @Aaron S: I see no problem with making the temp files directory a default. As for the documentation, indeed the part about the decorator needs to be parsed very carefully to be grasped fully. Reading the related PEP 0343 and seeing some examples helps, too ;-). I assume it was done this way to allow the generator function to handle any exceptions -- or lack of them -- exactly as it wishes. – martineau Aug 21 '12 at 23:30
0

It is a stupid hack-y solution but this problem can be avoided by doing a runaround on pywin32.

By switching the current working directory to one that is guaranteed to be safe to write to such as the temp directory it is possible to avoid the problem.

#Save the current working directory and then switch back once
#excel has been started. This is so pywin32 does not ruin everything
#by trying to write in the current directory without the proper 
#permission. This mainly happens (for me) when the program is installed in 
#program files which requires administrator permissions to write to.

import os
import tempfile
import win32com.client

cwd = os.getcwd()
tdir =  tempfile.gettempdir()
os.chdir(tdir)
self.xl = win32com.client.gencache.EnsureDispatch("Excel.Application") 
os.chdir(cwd)

N.B. the switch at the end back to the original working directory is not necessary but it will work if you need it (as I did).

martineau suggested a more robust way of doing this below using contextmanagers:

from contextlib import contextmanager

@contextmanager
def tempManager(self):
    cwd = os.getcwd()
    tdir =  tempfile.gettempdir()
    os.chdir(tdir)
    yield
    os.chdir(cwd)

with self.tempManager():
    self.xl = win32com.client.gencache.EnsureDispatch("Excel.Application")
Aaron S
  • 711
  • 2
  • 10
  • 23
  • You should make a context manager that saves the cwd, sets a new one, and the restores the original at the end of the `with`. Using the `contextlib.contextmanager` decorator would make creating one really easy -- just `yield` after changing to the new directory and then restore the original in the next statement. – martineau Aug 20 '12 at 21:18
  • You don't think that that is a bit much for such a simple operation? What extra would be gained besides perhaps a line or two of code size? – Aaron S Aug 20 '12 at 21:45
  • 1
    It really wouldn't take much more code that you already have if you create it with the decorator. The biggest difference besides codifying the whole idea of what you're doing is that you'd have something that would automatically restore the working directory even if an exception happens somewhere in the block. It would also be more readily reusable by yourself as well as others who see it in the answer you've posted. – martineau Aug 20 '12 at 23:26
  • That looks good...except for one major thing -- see the answer I just added. – martineau Aug 21 '12 at 17:21