Skip to content

Commit

Permalink
Merge pull request atom#11828 from atom/as-file-recovery-service
Browse files Browse the repository at this point in the history
File Recovery Service
  • Loading branch information
Antonio Scandurra committed May 27, 2016
2 parents 685ef5c + 8355e7f commit 1c843e7
Show file tree
Hide file tree
Showing 13 changed files with 311 additions and 13 deletions.
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

0 comments on commit 1c843e7

Please sign in to comment.