Skip to content

Commit cae6696

Browse files
committed
Fix a race condition in TexManager.make_dvi & make_png.
Previously, a race condition could occur if, while a process had called make_tex (generating the tex file in the global cache) and was going to call the latex subprocess (to generate the dvi file), another process also called make_tex for the same tex string and started rewriting the tex source. In that case, the latex subprocess could see a partially written (invalid) tex source. Fix that by generating the tex source in a process-private temporary directory, where the latex process is already going to run anyways. (This is cheap compared to the latex subprocess invocation.) Apply a similar strategy for make_png as well.
1 parent 780e66c commit cae6696

File tree

1 file changed

+27
-26
lines changed

1 file changed

+27
-26
lines changed

lib/matplotlib/texmanager.py

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -281,11 +281,9 @@ def make_dvi(cls, tex, fontsize):
281281
282282
Return the file name.
283283
"""
284-
basefile = cls.get_basefile(tex, fontsize)
285-
dvifile = '%s.dvi' % basefile
286-
if not os.path.exists(dvifile):
287-
texfile = Path(cls.make_tex(tex, fontsize))
288-
# Generate the dvi in a temporary directory to avoid race
284+
dvifile = Path(cls.get_basefile(tex, fontsize)).with_suffix(".dvi")
285+
if not dvifile.exists():
286+
# Generate the tex and dvi in a temporary directory to avoid race
289287
# conditions e.g. if multiple processes try to process the same tex
290288
# string at the same time. Having tmpdir be a subdirectory of the
291289
# final output dir ensures that they are on the same filesystem,
@@ -294,15 +292,17 @@ def make_dvi(cls, tex, fontsize):
294292
# the absolute path may contain characters (e.g. ~) that TeX does
295293
# not support; n.b. relative paths cannot traverse parents, or it
296294
# will be blocked when `openin_any = p` in texmf.cnf).
297-
cwd = Path(dvifile).parent
298-
with TemporaryDirectory(dir=cwd) as tmpdir:
299-
tmppath = Path(tmpdir)
295+
with TemporaryDirectory(dir=dvifile.parent) as tmpdir:
296+
Path(tmpdir, "file.tex").write_text(
297+
cls._get_tex_source(tex, fontsize), encoding='utf-8')
300298
cls._run_checked_subprocess(
301299
["latex", "-interaction=nonstopmode", "--halt-on-error",
302-
f"--output-directory={tmppath.name}",
303-
f"{texfile.name}"], tex, cwd=cwd)
304-
(tmppath / Path(dvifile).name).replace(dvifile)
305-
return dvifile
300+
"file.tex"], tex, cwd=tmpdir)
301+
Path(tmpdir, "file.dvi").replace(dvifile)
302+
# Also move the tex source to the main cache directory, but
303+
# only for backcompat.
304+
Path(tmpdir, "file.tex").replace(dvifile.with_suffix(".tex"))
305+
return str(dvifile)
306306

307307
@classmethod
308308
def make_png(cls, tex, fontsize, dpi):
@@ -311,22 +311,23 @@ def make_png(cls, tex, fontsize, dpi):
311311
312312
Return the file name.
313313
"""
314-
basefile = cls.get_basefile(tex, fontsize, dpi)
315-
pngfile = '%s.png' % basefile
314+
pngfile = Path(cls.get_basefile(tex, fontsize)).with_suffix(".png")
316315
# see get_rgba for a discussion of the background
317-
if not os.path.exists(pngfile):
316+
if not pngfile.exists():
318317
dvifile = cls.make_dvi(tex, fontsize)
319-
cmd = ["dvipng", "-bg", "Transparent", "-D", str(dpi),
320-
"-T", "tight", "-o", pngfile, dvifile]
321-
# When testing, disable FreeType rendering for reproducibility; but
322-
# dvipng 1.16 has a bug (fixed in f3ff241) that breaks --freetype0
323-
# mode, so for it we keep FreeType enabled; the image will be
324-
# slightly off.
325-
if (getattr(mpl, "_called_from_pytest", False) and
326-
mpl._get_executable_info("dvipng").raw_version != "1.16"):
327-
cmd.insert(1, "--freetype0")
328-
cls._run_checked_subprocess(cmd, tex)
329-
return pngfile
318+
with TemporaryDirectory(dir=pngfile.parent) as tmpdir:
319+
cmd = ["dvipng", "-bg", "Transparent", "-D", str(dpi),
320+
"-T", "tight", "-o", "file.png", dvifile]
321+
# When testing, disable FreeType rendering for reproducibility;
322+
# but dvipng 1.16 has a bug (fixed in f3ff241) that breaks
323+
# --freetype0 mode, so for it we keep FreeType enabled; the
324+
# image will be slightly off.
325+
if (getattr(mpl, "_called_from_pytest", False) and
326+
mpl._get_executable_info("dvipng").raw_version != "1.16"):
327+
cmd.insert(1, "--freetype0")
328+
cls._run_checked_subprocess(cmd, tex, cwd=tmpdir)
329+
Path(tmpdir, "file.png").replace(pngfile)
330+
return str(pngfile)
330331

331332
@classmethod
332333
def get_grey(cls, tex, fontsize=None, dpi=None):

0 commit comments

Comments
 (0)