Skip to content
This repository has been archived by the owner on Mar 3, 2023. It is now read-only.

File Recovery Service #11828

Merged
merged 34 commits into from
May 27, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
ef3ab03
Emit {will,did}SavePath on `ipcMain` before/after a buffer is saved
May 23, 2016
049321a
:bug: :fire: Remove double subscription to the same buffer
May 23, 2016
de599f9
Make dialog asynchronous when a renderer process crashes
May 23, 2016
b58ce49
Create `FileRecoveryService` to restore corrupted files after a crash
May 23, 2016
770199c
:art: event.sender -> window
May 23, 2016
ca32c13
:fire: Unnecessary variable assignments
May 23, 2016
57195d7
:white_check_mark: Write specs for FileRecoveryService
May 23, 2016
97dd25d
:green_heart:
May 24, 2016
25fece4
:art: file-recovery-service-spec.js -> file-recovery-service.spec.js
May 24, 2016
858740c
:memo: Better description on spec
May 24, 2016
c7f4b33
Emit informative warning when a file can't be recovered
May 24, 2016
4f1efe6
:shirt: Fix linter errors
May 24, 2016
7a12984
:fire: Unused requires on specs
May 24, 2016
152c28a
Merge branch 'master' into as-file-recovery-service
May 24, 2016
e57b35f
Merge branch 'master' into as-file-recovery-service
May 24, 2016
b84feeb
Emit {will,did}SavePath events synchronously
May 24, 2016
3b4c101
Forget window when it gets closed
May 25, 2016
c8fae11
Handle recovery when many windows save the same file simultaneously
May 25, 2016
3ce7d0a
Merge branch 'master' into as-file-recovery-service
May 25, 2016
3030723
:shirt: Fix linting issues
May 25, 2016
a2a734a
Generate readable recovery filenames
May 25, 2016
c6a87b9
Add sinon
May 25, 2016
1a7858c
Log a more informative message when cannot recover a file
May 25, 2016
c2b01d5
Make coupling looser between the recovery service and the windows
May 25, 2016
8ba275a
:art: Move RecoveryFile down
May 25, 2016
49a603a
Show also a message box when recovery is not successful
May 25, 2016
3f8f3c9
:fire: Remove extra comma
May 25, 2016
8733b52
:fire: Extra imports
May 25, 2016
d8564ad
Be a little more defensive when retaining/releasing recovery files
May 25, 2016
aefcbcd
:fire: Remove unneeded WeakSet
May 25, 2016
6c34844
:bug: Don't try to recover the same file twice
May 26, 2016
df263a2
Return early when a recovery file can't be stored
May 26, 2016
5e0e65b
Merge branch 'master' into as-file-recovery-service
May 26, 2016
8355e7f
Use fs.copyFileSync for buffered copy
May 26, 2016
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"event-kit": "^1.5.0",
"find-parent-dir": "^0.3.0",
"first-mate": "^5.1.1",
"fs-plus": "^2.8.0",
"fs-plus": "2.9.1",
"fstream": "0.1.24",
"fuzzaldrin": "^2.1",
"git-utils": "^4.1.2",
Expand Down Expand Up @@ -55,6 +55,7 @@
"season": "^5.3",
"semver": "^4.3.3",
"service-hub": "^0.7.0",
"sinon": "1.17.4",
"source-map-support": "^0.3.2",
"temp": "0.8.1",
"text-buffer": "9.1.0",
Expand Down
2 changes: 1 addition & 1 deletion spec/git-repository-async-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,7 @@ describe('GitRepositoryAsync', () => {
it('subscribes to all the serialized buffers in the project', async () => {
await atom.workspace.open('file.txt')

project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm, applicationDelegate: atom.applicationDelegate})
project2.deserialize(atom.project.serialize({isUnloading: true}))

const repo = project2.getRepositories()[0].async
Expand Down
2 changes: 1 addition & 1 deletion spec/git-spec.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ describe "GitRepository", ->
atom.workspace.open('file.txt')

runs ->
project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm, applicationDelegate: atom.applicationDelegate})
project2.deserialize(atom.project.serialize({isUnloading: false}))
buffer = project2.getBuffers()[0]

Expand Down
127 changes: 127 additions & 0 deletions spec/main-process/file-recovery-service.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
'use babel'

import {dialog} from 'electron'
import FileRecoveryService from '../../src/main-process/file-recovery-service'
import temp from 'temp'
import fs from 'fs-plus'
import sinon from 'sinon'

describe("FileRecoveryService", () => {
let recoveryService, recoveryDirectory

beforeEach(() => {
recoveryDirectory = temp.mkdirSync()
recoveryService = new FileRecoveryService(recoveryDirectory)
})

describe("when no crash happens during a save", () => {
it("creates a recovery file and deletes it after saving", () => {
const mockWindow = {}
const filePath = temp.path()

fs.writeFileSync(filePath, "some content")
recoveryService.willSavePath(mockWindow, filePath)
assert.equal(fs.listTreeSync(recoveryDirectory).length, 1)

fs.writeFileSync(filePath, "changed")
recoveryService.didSavePath(mockWindow, filePath)
assert.equal(fs.listTreeSync(recoveryDirectory).length, 0)
assert.equal(fs.readFileSync(filePath, 'utf8'), "changed")
})

it("creates only one recovery file when many windows attempt to save the same file, deleting it when the last one finishes saving it", () => {
const mockWindow = {}
const anotherMockWindow = {}
const filePath = temp.path()

fs.writeFileSync(filePath, "some content")
recoveryService.willSavePath(mockWindow, filePath)
recoveryService.willSavePath(anotherMockWindow, filePath)
assert.equal(fs.listTreeSync(recoveryDirectory).length, 1)

fs.writeFileSync(filePath, "changed")
recoveryService.didSavePath(mockWindow, filePath)
assert.equal(fs.listTreeSync(recoveryDirectory).length, 1)
assert.equal(fs.readFileSync(filePath, 'utf8'), "changed")

recoveryService.didSavePath(anotherMockWindow, filePath)
assert.equal(fs.listTreeSync(recoveryDirectory).length, 0)
assert.equal(fs.readFileSync(filePath, 'utf8'), "changed")
})
})

describe("when a crash happens during a save", () => {
it("restores the created recovery file and deletes it", () => {
const mockWindow = {}
const filePath = temp.path()

fs.writeFileSync(filePath, "some content")
recoveryService.willSavePath(mockWindow, filePath)
assert.equal(fs.listTreeSync(recoveryDirectory).length, 1)

fs.writeFileSync(filePath, "changed")
recoveryService.didCrashWindow(mockWindow)
assert.equal(fs.listTreeSync(recoveryDirectory).length, 0)
assert.equal(fs.readFileSync(filePath, 'utf8'), "some content")
})

it("restores the created recovery file when many windows attempt to save the same file and one of them crashes", () => {
const mockWindow = {}
const anotherMockWindow = {}
const filePath = temp.path()

fs.writeFileSync(filePath, "A")
recoveryService.willSavePath(mockWindow, filePath)
fs.writeFileSync(filePath, "B")
recoveryService.willSavePath(anotherMockWindow, filePath)
assert.equal(fs.listTreeSync(recoveryDirectory).length, 1)

fs.writeFileSync(filePath, "C")

recoveryService.didCrashWindow(mockWindow)
assert.equal(fs.readFileSync(filePath, 'utf8'), "A")
assert.equal(fs.listTreeSync(recoveryDirectory).length, 0)

fs.writeFileSync(filePath, "D")
recoveryService.willSavePath(mockWindow, filePath)
fs.writeFileSync(filePath, "E")
recoveryService.willSavePath(anotherMockWindow, filePath)
assert.equal(fs.listTreeSync(recoveryDirectory).length, 1)

fs.writeFileSync(filePath, "F")

recoveryService.didCrashWindow(anotherMockWindow)
assert.equal(fs.readFileSync(filePath, 'utf8'), "D")
assert.equal(fs.listTreeSync(recoveryDirectory).length, 0)
})

it("emits a warning when a file can't be recovered", sinon.test(function () {
const mockWindow = {}
const filePath = temp.path()
fs.writeFileSync(filePath, "content")
fs.chmodSync(filePath, 0444)

let logs = []
this.stub(console, 'log', (message) => logs.push(message))
this.stub(dialog, 'showMessageBox')

recoveryService.willSavePath(mockWindow, filePath)
recoveryService.didCrashWindow(mockWindow)
let recoveryFiles = fs.listTreeSync(recoveryDirectory)
assert.equal(recoveryFiles.length, 1)
assert.equal(logs.length, 1)
assert.match(logs[0], new RegExp(filePath))
assert.match(logs[0], new RegExp(recoveryFiles[0]))
}))
})

it("doesn't create a recovery file when the file that's being saved doesn't exist yet", () => {
const mockWindow = {}

recoveryService.willSavePath(mockWindow, "a-file-that-doesnt-exist")
assert.equal(fs.listTreeSync(recoveryDirectory).length, 0)

recoveryService.didSavePath(mockWindow, "a-file-that-doesnt-exist")
assert.equal(fs.listTreeSync(recoveryDirectory).length, 0)
})
})
22 changes: 22 additions & 0 deletions spec/project-spec.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,28 @@ describe "Project", ->
editor.saveAs(tempFile)
expect(atom.project.getPaths()[0]).toBe path.dirname(tempFile)

describe "before and after saving a buffer", ->
[buffer] = []
beforeEach ->
waitsForPromise ->
atom.project.bufferForPath(path.join(__dirname, 'fixtures', 'sample.js')).then (o) ->
buffer = o
buffer.retain()

afterEach ->
buffer.release()

it "emits save events on the main process", ->
spyOn(atom.project.applicationDelegate, 'emitDidSavePath')
spyOn(atom.project.applicationDelegate, 'emitWillSavePath')

buffer.save()

expect(atom.project.applicationDelegate.emitDidSavePath.calls.length).toBe(1)
expect(atom.project.applicationDelegate.emitDidSavePath).toHaveBeenCalledWith(buffer.getPath())
expect(atom.project.applicationDelegate.emitWillSavePath.calls.length).toBe(1)
expect(atom.project.applicationDelegate.emitWillSavePath).toHaveBeenCalledWith(buffer.getPath())

describe "when a watch error is thrown from the TextBuffer", ->
editor = null
beforeEach ->
Expand Down
2 changes: 1 addition & 1 deletion spec/workspace-spec.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe "Workspace", ->
projectState = atom.project.serialize({isUnloading: true})
atom.workspace.destroy()
atom.project.destroy()
atom.project = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm.bind(atom)})
atom.project = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm.bind(atom), applicationDelegate: atom.applicationDelegate})
atom.project.deserialize(projectState)
atom.workspace = new Workspace({
config: atom.config, project: atom.project, packageManager: atom.packages,
Expand Down
6 changes: 6 additions & 0 deletions src/application-delegate.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -266,3 +266,9 @@ class ApplicationDelegate

getAutoUpdateManagerErrorMessage: ->
ipcRenderer.sendSync('get-auto-update-manager-error')

emitWillSavePath: (path) ->
ipcRenderer.sendSync('will-save-path', path)

emitDidSavePath: (path) ->
ipcRenderer.sendSync('did-save-path', path)
2 changes: 1 addition & 1 deletion src/atom-environment.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ class AtomEnvironment extends Model

@clipboard = new Clipboard()

@project = new Project({notificationManager: @notifications, packageManager: @packages, @config})
@project = new Project({notificationManager: @notifications, packageManager: @packages, @config, @applicationDelegate})

@commandInstaller = new CommandInstaller(@getVersion(), @applicationDelegate)

Expand Down
18 changes: 14 additions & 4 deletions src/main-process/atom-application.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ AtomProtocolHandler = require './atom-protocol-handler'
AutoUpdateManager = require './auto-update-manager'
StorageFolder = require '../storage-folder'
Config = require '../config'
FileRecoveryService = require './file-recovery-service'
ipcHelpers = require '../ipc-helpers'
{BrowserWindow, Menu, app, dialog, ipcMain, shell} = require 'electron'
fs = require 'fs-plus'
Expand Down Expand Up @@ -78,6 +79,7 @@ class AtomApplication
@autoUpdateManager = new AutoUpdateManager(@version, options.test, @resourcePath, @config)
@applicationMenu = new ApplicationMenu(@version, @autoUpdateManager)
@atomProtocolHandler = new AtomProtocolHandler(@resourcePath, @safeMode)
@fileRecoveryService = new FileRecoveryService(path.join(process.env.ATOM_HOME, "recovery"))

@listenForArgumentsFromNewProcess()
@setupJavaScriptArguments()
Expand Down Expand Up @@ -242,7 +244,7 @@ class AtomApplication
options.window = window
@openPaths(options)
else
new AtomWindow(options)
new AtomWindow(@fileRecoveryService, options)
else
@promptForPathToOpen('all', {window})

Expand Down Expand Up @@ -325,6 +327,14 @@ class AtomApplication
ipcMain.on 'get-auto-update-manager-error', (event) =>
event.returnValue = @autoUpdateManager.getErrorMessage()

ipcMain.on 'will-save-path', (event, path) =>
@fileRecoveryService.willSavePath(@windowForEvent(event), path)
event.returnValue = true

ipcMain.on 'did-save-path', (event, path) =>
@fileRecoveryService.didSavePath(@windowForEvent(event), path)
event.returnValue = true

setupDockMenu: ->
if process.platform is 'darwin'
dockMenu = Menu.buildFromTemplate [
Expand Down Expand Up @@ -485,7 +495,7 @@ class AtomApplication
windowInitializationScript ?= require.resolve('../initialize-application-window')
resourcePath ?= @resourcePath
windowDimensions ?= @getDimensionsForNewWindow()
openedWindow = new AtomWindow({initialPaths, locationsToOpen, windowInitializationScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup, clearWindowState, env})
openedWindow = new AtomWindow(@fileRecoveryService, {initialPaths, locationsToOpen, windowInitializationScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup, clearWindowState, env})

if pidToKillWhenClosed?
@pidsToOpenWindows[pidToKillWhenClosed] = openedWindow
Expand Down Expand Up @@ -564,7 +574,7 @@ class AtomApplication
packagePath = @packages.resolvePackagePath(packageName)
windowInitializationScript = path.resolve(packagePath, pack.urlMain)
windowDimensions = @getDimensionsForNewWindow()
new AtomWindow({windowInitializationScript, @resourcePath, devMode, safeMode, urlToOpen, windowDimensions, env})
new AtomWindow(@fileRecoveryService, {windowInitializationScript, @resourcePath, devMode, safeMode, urlToOpen, windowDimensions, env})
else
console.log "Package '#{pack.name}' does not have a url main: #{urlToOpen}"
else
Expand Down Expand Up @@ -609,7 +619,7 @@ class AtomApplication
devMode = true
isSpec = true
safeMode ?= false
new AtomWindow({windowInitializationScript, resourcePath, headless, isSpec, devMode, testRunnerPath, legacyTestRunnerPath, testPaths, logFile, safeMode, env})
new AtomWindow(@fileRecoveryService, {windowInitializationScript, resourcePath, headless, isSpec, devMode, testRunnerPath, legacyTestRunnerPath, testPaths, logFile, safeMode, env})

resolveTestRunnerPath: (testPath) ->
FindParentDir ?= require 'find-parent-dir'
Expand Down
4 changes: 3 additions & 1 deletion src/main-process/atom-window.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class AtomWindow
loaded: null
isSpec: null

constructor: (settings={}) ->
constructor: (@fileRecoveryService, settings={}) ->
{@resourcePath, initialPaths, pathToOpen, locationsToOpen, @isSpec, @headless, @safeMode, @devMode} = settings
locationsToOpen ?= [{pathToOpen}] if pathToOpen
locationsToOpen ?= []
Expand Down Expand Up @@ -125,6 +125,7 @@ class AtomWindow
global.atomApplication.saveState(false)

@browserWindow.on 'closed', =>
@fileRecoveryService.didCloseWindow(this)
global.atomApplication.removeWindow(this)

@browserWindow.on 'unresponsive', =>
Expand All @@ -140,6 +141,7 @@ class AtomWindow
@browserWindow.webContents.on 'crashed', =>
global.atomApplication.exit(100) if @headless

@fileRecoveryService.didCrashWindow(this)
chosen = dialog.showMessageBox @browserWindow,
type: 'warning'
buttons: ['Close Window', 'Reload', 'Keep It Open']
Expand Down
Loading