From f77391b158007c2ef32da5edb58fd23799ef2174 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 2 Aug 2018 10:33:40 -0700 Subject: [PATCH] Write config file atomically (write and replace) --- src/config-file.js | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/src/config-file.js b/src/config-file.js index 35f3b5c38..f8cba37ef 100644 --- a/src/config-file.js +++ b/src/config-file.js @@ -6,6 +6,7 @@ const {watchPath} = require('./path-watcher') const CSON = require('season') const Path = require('path') const async = require('async') +const temp = require('temp') const EVENT_TYPES = new Set([ 'created', @@ -37,9 +38,11 @@ class ConfigFile { this.reloadCallbacks = [] // Use a queue to prevent multiple concurrent write to the same file. - const writeQueue = async.queue((data, callback) => - CSON.writeFile(this.path, data, error => { - if (error) { + const writeQueue = async.queue((data, callback) => { + (async () => { + try { + await writeCSONFileAtomically(this.path, data) + } catch (error) { this.emitter.emit('did-error', dedent ` Failed to write \`${Path.basename(this.path)}\`. @@ -47,8 +50,8 @@ class ConfigFile { `) } callback() - }) - ) + })() + }) this.requestLoad = _.debounce(() => this.reload(), 200) this.requestSave = _.debounce((data) => writeQueue.push(data), 200) @@ -116,3 +119,27 @@ class ConfigFile { }) } } + +function writeCSONFile (path, data) { + return new Promise((resolve, reject) => { + CSON.writeFile(path, data, error => { + if (error) reject(error) + else resolve() + }) + }) +} + +async function writeCSONFileAtomically (path, data) { + const tempPath = temp.path() + await writeCSONFile(tempPath, data) + await rename(tempPath, path) +} + +function rename (oldPath, newPath) { + return new Promise((resolve, reject) => { + fs.rename(oldPath, newPath, error => { + if (error) reject(error) + else resolve() + }) + }) +}