diff --git a/Lib/importlib/_bootstrap.py b/Lib/importlib/_bootstrap.py --- a/Lib/importlib/_bootstrap.py +++ b/Lib/importlib/_bootstrap.py @@ -80,6 +80,27 @@ def _path_absolute(path): return _path_join(_os.getcwd(), path) +def _write_atomic(path, data): + """Best-effort function to write data to a path atomically.""" + if not sys.platform.startswith('win'): + # On POSIX-like platforms, renaming is atomic + path_tmp = path + '.tmp' + try: + fd = _os.open(path_tmp, _os.O_EXCL | _os.O_CREAT | _os.O_WRONLY) + with _io.FileIO(fd, 'wb') as file: + file.write(data) + _os.rename(path_tmp, path) + except OSError: + try: + _os.unlink(path_tmp) + except OSError: + pass + raise + else: + with _io.FileIO(path, 'wb') as file: + file.write(data) + + def _wrap(new, old): """Simple substitute for functools.wraps.""" for replace in ['__module__', '__name__', '__doc__']: @@ -494,9 +515,8 @@ class _SourceFileLoader(_FileLoader, Sou else: raise try: - with _io.FileIO(path, 'wb') as file: - file.write(data) - except IOError as exc: + _write_atomic(path, data) + except OSError as exc: # Don't worry if you can't write bytecode. if exc.errno == errno.EACCES: return diff --git a/Python/import.c b/Python/import.c --- a/Python/import.c +++ b/Python/import.c @@ -1284,7 +1284,8 @@ write_compiled_module(PyCodeObject *co, #ifdef MS_WINDOWS int fd; #else - PyObject *cpathbytes; + PyObject *cpathbytes, *cpathbytes_tmp; + Py_ssize_t cpathbytes_len; #endif PyObject *dirname; Py_UCS4 *dirsep; @@ -1345,13 +1346,25 @@ write_compiled_module(PyCodeObject *co, else fp = NULL; #else + /* Under POSIX, we first write to a tmp file and then take advantage + of atomic renaming. */ cpathbytes = PyUnicode_EncodeFSDefault(cpathname); if (cpathbytes == NULL) { PyErr_Clear(); return; } - - fp = open_exclusive(PyBytes_AS_STRING(cpathbytes), mode); + cpathbytes_len = PyBytes_GET_SIZE(cpathbytes); + cpathbytes_tmp = PyBytes_FromStringAndSize(NULL, cpathbytes_len + 4); + if (cpathbytes_tmp == NULL) { + Py_DECREF(cpathbytes); + PyErr_Clear(); + return; + } + memcpy(PyBytes_AS_STRING(cpathbytes_tmp), PyBytes_AS_STRING(cpathbytes), + cpathbytes_len); + memcpy(PyBytes_AS_STRING(cpathbytes_tmp) + cpathbytes_len, ".tmp", 4); + + fp = open_exclusive(PyBytes_AS_STRING(cpathbytes_tmp), mode); #endif if (fp == NULL) { if (Py_VerboseFlag) @@ -1359,6 +1372,7 @@ write_compiled_module(PyCodeObject *co, "# can't create %R\n", cpathname); #ifndef MS_WINDOWS Py_DECREF(cpathbytes); + Py_DECREF(cpathbytes_tmp); #endif return; } @@ -1366,6 +1380,11 @@ write_compiled_module(PyCodeObject *co, /* First write a 0 for mtime */ PyMarshal_WriteLongToFile(0L, fp, Py_MARSHAL_VERSION); PyMarshal_WriteObjectToFile((PyObject *)co, fp, Py_MARSHAL_VERSION); + fflush(fp); + /* Now write the true mtime */ + fseek(fp, 4L, 0); + assert(mtime < LONG_MAX); + PyMarshal_WriteLongToFile((long)mtime, fp, Py_MARSHAL_VERSION); if (fflush(fp) != 0 || ferror(fp)) { if (Py_VerboseFlag) PySys_FormatStderr("# can't write %R\n", cpathname); @@ -1374,22 +1393,31 @@ write_compiled_module(PyCodeObject *co, #ifdef MS_WINDOWS (void)DeleteFileW(PyUnicode_AS_UNICODE(cpathname)); #else - (void) unlink(PyBytes_AS_STRING(cpathbytes)); + (void) unlink(PyBytes_AS_STRING(cpathbytes_tmp)); Py_DECREF(cpathbytes); + Py_DECREF(cpathbytes_tmp); #endif return; } + fclose(fp); + /* Under POSIX, do an atomic rename */ #ifndef MS_WINDOWS + if (rename(PyBytes_AS_STRING(cpathbytes_tmp), + PyBytes_AS_STRING(cpathbytes))) { + if (Py_VerboseFlag) + PySys_FormatStderr("# can't write %R\n", cpathname); + /* Don't keep tmp file */ + unlink(PyBytes_AS_STRING(cpathbytes_tmp)); + Py_DECREF(cpathbytes); + Py_DECREF(cpathbytes_tmp); + return; + } Py_DECREF(cpathbytes); + Py_DECREF(cpathbytes_tmp); #endif - /* Now write the true mtime */ - fseek(fp, 4L, 0); - assert(mtime < LONG_MAX); - PyMarshal_WriteLongToFile((long)mtime, fp, Py_MARSHAL_VERSION); - fflush(fp); - fclose(fp); if (Py_VerboseFlag) PySys_FormatStderr("# wrote %R\n", cpathname); + return; } static void