Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent race condition in genpy via named mutex #2118

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 22 additions & 2 deletions com/win32com/client/gencache.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

from __future__ import annotations

import contextlib
import glob
import os
import sys
Expand All @@ -34,6 +35,7 @@
import pywintypes
import win32com
import win32com.client
import win32event

from . import CLSIDToClass

Expand Down Expand Up @@ -133,6 +135,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 @@ -258,7 +276,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 @@ -730,7 +749,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 @@ -1037,35 +1036,15 @@ def open_writer(self, filename, encoding="utf-8"):

def finish_writer(self, filename, f, worked):
f.close()
try:
os.unlink(filename)
except OSError:
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 OSError:
# 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 OSError:
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 @@ -1356,7 +1335,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 @@ -371,7 +373,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