Skip to content

Commit

Permalink
Prevent race condition in genpy via named mutex
Browse files Browse the repository at this point in the history
  • Loading branch information
Chronial committed Sep 8, 2023
1 parent 2a7137f commit efb3e43
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 35 deletions.
24 changes: 22 additions & 2 deletions com/win32com/client/gencache.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
Currently just uses a pickled dictionary, but should used some sort of indexed file.
Maybe an OLE2 compound file, or a bsddb file?
"""
import contextlib
import glob
import os
import sys
Expand All @@ -29,6 +30,7 @@
import pywintypes
import win32com
import win32com.client
import win32event

from . import CLSIDToClass

Expand Down Expand Up @@ -127,6 +129,22 @@ def _LoadDicts():
f.close()


@contextlib.contextmanager
def ModuleMutex(module_name):
"""Given the output of GetGeneratedFilename, acquire a named mutex for that module
This is required so that writes (generation) don't interfere with each other and with reads (import)
"""
mutex = win32event.CreateMutex(None, False, module_name)
with contextlib.closing(mutex):
# acquire mutex
win32event.WaitForSingleObject(mutex, win32event.INFINITE)
try:
yield
finally:
win32event.ReleaseMutex(mutex)


def GetGeneratedFileName(clsid, lcid, major, minor):
"""Given the clsid, lcid, major and minor for a type lib, return
the file name (no extension) providing this support.
Expand Down Expand Up @@ -252,7 +270,8 @@ class which wraps the COM object.
if sub_mod is not None:
sub_mod_name = mod.__name__ + "." + sub_mod
try:
__import__(sub_mod_name)
with ModuleMutex(mod.__name__.split(".")[-1]):
__import__(sub_mod_name)
except ImportError:
info = typelibCLSID, lcid, major, minor
# Force the generation. If this typelibrary has explicitly been added,
Expand Down Expand Up @@ -723,7 +742,8 @@ def GetGeneratedInfos():
def _GetModule(fname):
"""Given the name of a module in the gen_py directory, import and return it."""
mod_name = "win32com.gen_py.%s" % fname
mod = __import__(mod_name)
with ModuleMutex(fname):
__import__(mod_name)
return sys.modules[mod_name]


Expand Down
36 changes: 8 additions & 28 deletions com/win32com/client/genpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@
import time

import pythoncom
import win32com

from . import build
from . import build, gencache

error = "makepy.error"
makepy_version = "0.5.01" # Written to generated file.
Expand Down Expand Up @@ -1040,35 +1039,15 @@ def open_writer(self, filename, encoding="mbcs"):

def finish_writer(self, filename, f, worked):
f.close()
try:
os.unlink(filename)
except os.error:
pass
temp_filename = self.get_temp_filename(filename)
if worked:
os.replace(temp_filename, filename)
else:
try:
os.rename(temp_filename, filename)
os.unlink(filename)
os.unlink(temp_filename)
except os.error:
# If we are really unlucky, another process may have written the
# file in between our calls to os.unlink and os.rename. So try
# again, but only once.
# There are still some race conditions, but they seem difficult to
# fix, and they probably occur much less frequently:
# * The os.rename failure could occur more than once if more than
# two processes are involved.
# * In between os.unlink and os.rename, another process could try
# to import the module, having seen that it already exists.
# * If another process starts a COM server while we are still
# generating __init__.py, that process sees that the folder
# already exists and assumes that __init__.py is already there
# as well.
try:
os.unlink(filename)
except os.error:
pass
os.rename(temp_filename, filename)
else:
os.unlink(temp_filename)
pass

def get_temp_filename(self, filename):
return "%s.%d.temp" % (filename, os.getpid())
Expand Down Expand Up @@ -1359,7 +1338,8 @@ def generate_child(self, child, dir):
self.progress.Tick()
worked = True
finally:
self.finish_writer(out_name, self.file, worked)
with gencache.ModuleMutex(self.base_mod_name.split(".")[-1]):
self.finish_writer(out_name, self.file, worked)
self.file = None
finally:
self.progress.Finished()
Expand Down
13 changes: 8 additions & 5 deletions com/win32com/client/makepy.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,10 +292,11 @@ def GenerateFromTypeLibSpec(
for typelib, info in typelibs:
gen = genpy.Generator(typelib, info.dll, progress, bBuildHidden=bBuildHidden)

this_name = gencache.GetGeneratedFileName(
info.clsid, info.lcid, info.major, info.minor
)

if file is None:
this_name = gencache.GetGeneratedFileName(
info.clsid, info.lcid, info.major, info.minor
)
full_name = os.path.join(gencache.GetGeneratePath(), this_name)
if bForDemand:
try:
Expand Down Expand Up @@ -326,7 +327,8 @@ def GenerateFromTypeLibSpec(
worked = True
finally:
if file is None:
gen.finish_writer(outputName, fileUse, worked)
with gencache.ModuleMutex(this_name):
gen.finish_writer(outputName, fileUse, worked)
importlib.invalidate_caches()
if bToGenDir:
progress.SetDescription("Importing module")
Expand Down Expand Up @@ -372,7 +374,8 @@ def GenerateChildFromTypeLibSpec(
gen.generate_child(child, dir_path_name)
progress.SetDescription("Importing module")
importlib.invalidate_caches()
__import__("win32com.gen_py." + dir_name + "." + child)
with gencache.ModuleMutex(dir_name):
__import__("win32com.gen_py." + dir_name + "." + child)
progress.Close()


Expand Down

0 comments on commit efb3e43

Please sign in to comment.