Skip to content

Commit 9c7d559

Browse files
committed
Revert "feat(config): move config to database"
This reverts commit a60816c.
1 parent 2f2cd92 commit 9c7d559

7 files changed

+251
-122
lines changed

spec/stores/account-store-spec.coffee

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,10 @@ xdescribe "AccountStore", ->
110110
account = (new Account).fromJSON(@json)
111111
expect(@instance._accounts.length).toBe 1
112112
expect(@instance._accounts[0]).toEqual account
113-
expect(NylasEnv.config.set.calls.length).toBe 2
113+
expect(NylasEnv.config.set.calls.length).toBe 3
114+
expect(NylasEnv.config.set.calls[0].args).toEqual(['nylas.accountTokens', null])
114115
# Version must be updated last since it will trigger other windows to load nylas.accounts
115-
expect(NylasEnv.config.set.calls[1].args).toEqual(['nylas.accountsVersion', 1])
116+
expect(NylasEnv.config.set.calls[2].args).toEqual(['nylas.accountsVersion', 1])
116117

117118
it "triggers", ->
118119
expect(@instance.trigger).toHaveBeenCalled()

src/browser/application.es6

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,23 @@ import path from 'path';
88
import proc from 'child_process'
99
import {EventEmitter} from 'events';
1010

11+
import SystemTrayManager from './system-tray-manager';
1112
import WindowManager from './window-manager';
1213
import FileListCache from './file-list-cache';
1314
import ApplicationMenu from './application-menu';
1415
import AutoUpdateManager from './auto-update-manager';
15-
import SystemTrayManager from './system-tray-manager';
1616
import PerformanceMonitor from './performance-monitor'
17-
import DefaultClientHelper from '../default-client-helper';
1817
import NylasProtocolHandler from './nylas-protocol-handler';
1918
import PackageMigrationManager from './package-migration-manager';
2019
import ConfigPersistenceManager from './config-persistence-manager';
20+
import DefaultClientHelper from '../default-client-helper';
2121

2222
let clipboard = null;
2323

2424
// The application's singleton class.
2525
//
2626
export default class Application extends EventEmitter {
27-
async start(options) {
27+
start(options) {
2828
const {resourcePath, configDirPath, version, devMode, specMode, safeMode} = options;
2929

3030
// Normalize to make sure drive letter case is consistent on Windows
@@ -38,13 +38,12 @@ export default class Application extends EventEmitter {
3838
this.fileListCache = new FileListCache();
3939
this.nylasProtocolHandler = new NylasProtocolHandler(this.resourcePath, this.safeMode);
4040

41-
this.configPersistenceManager = new ConfigPersistenceManager({
42-
configDirPath, resourcePath, specMode});
43-
await this.configPersistenceManager.setup()
41+
this.temporaryMigrateConfig();
4442

4543
const Config = require('../config');
4644
const config = new Config();
4745
this.config = config;
46+
this.configPersistenceManager = new ConfigPersistenceManager({configDirPath, resourcePath});
4847
config.load();
4948

5049
this.packageMigrationManager = new PackageMigrationManager({config, configDirPath, version})
@@ -103,6 +102,17 @@ export default class Application extends EventEmitter {
103102
return this.quitting;
104103
}
105104

105+
temporaryMigrateConfig() {
106+
const oldConfigFilePath = fs.resolve(this.configDirPath, 'config.cson');
107+
const newConfigFilePath = path.join(this.configDirPath, 'config.json');
108+
if (oldConfigFilePath) {
109+
const CSON = require('season');
110+
const userConfig = CSON.readFileSync(oldConfigFilePath);
111+
fs.writeFileSync(newConfigFilePath, JSON.stringify(userConfig, null, 2));
112+
fs.unlinkSync(oldConfigFilePath);
113+
}
114+
}
115+
106116
// Opens a new window based on the options provided.
107117
handleLaunchOptions(options) {
108118
const {specMode, pathsToOpen, urlsToOpen} = options;
Lines changed: 176 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,209 @@
11
import path from 'path';
2+
import pathWatcher from 'pathwatcher';
23
import fs from 'fs-plus';
3-
import {setupDatabase, databasePath} from '../database-helpers'
4+
import {BrowserWindow, dialog, app} from 'electron';
5+
import {atomicWriteFileSync} from '../fs-utils'
6+
47
let _ = require('underscore');
58
_ = _.extend(_, require('../config-utils'));
69

10+
const RETRY_SAVES = 3
11+
12+
713
export default class ConfigPersistenceManager {
8-
constructor({configDirPath, resourcePath, specMode} = {}) {
9-
this.database = null;
10-
this.specMode = specMode
11-
this.resourcePath = resourcePath;
14+
constructor({configDirPath, resourcePath} = {}) {
1215
this.configDirPath = configDirPath;
16+
this.resourcePath = resourcePath;
17+
18+
this.userWantsToPreserveErrors = false
19+
this.saveRetries = 0
20+
this.configFilePath = path.join(this.configDirPath, 'config.json')
21+
this.settings = {};
22+
23+
this.initializeConfigDirectory();
24+
this.load();
25+
this.observe();
26+
}
27+
28+
initializeConfigDirectory() {
29+
if (!fs.existsSync(this.configDirPath)) {
30+
fs.makeTreeSync(this.configDirPath);
31+
const templateConfigDirPath = path.join(this.resourcePath, 'dot-nylas');
32+
fs.copySync(templateConfigDirPath, this.configDirPath);
33+
}
34+
35+
if (!fs.existsSync(this.configFilePath)) {
36+
this.writeTemplateConfigFile();
37+
}
1338
}
1439

15-
async setup() {
16-
await this._initializeDatabase();
17-
this._initializeConfigDirectory();
18-
this._migrateOldConfigs();
40+
writeTemplateConfigFile() {
41+
const templateConfigPath = path.join(this.resourcePath, 'dot-nylas', 'config.json');
42+
const templateConfig = fs.readFileSync(templateConfigPath);
43+
fs.writeFileSync(this.configFilePath, templateConfig);
1944
}
2045

21-
getRawValues() {
22-
if (!this._selectStatement) {
23-
const q = `SELECT * FROM \`config\` WHERE id = '*'`;
24-
this._selectStatement = this.database.prepare(q)
46+
load() {
47+
this.userWantsToPreserveErrors = false;
48+
49+
try {
50+
const json = JSON.parse(fs.readFileSync(this.configFilePath)) || {};
51+
this.settings = json['*'];
52+
this.emitChangeEvent();
53+
} catch (error) {
54+
global.errorLogger.reportError(error, {event: 'Failed to load config.json'})
55+
const message = `Failed to load "${path.basename(this.configFilePath)}"`;
56+
let detail = (error.location) ? error.stack : error.message;
57+
58+
if (error instanceof SyntaxError) {
59+
detail += `\n\nThe file ${this.configFilePath} has incorrect JSON formatting or is empty. Fix the formatting to resolve this error, or reset your settings to continue using N1.`
60+
} else {
61+
detail += `\n\nWe were unable to read the file ${this.configFilePath}. Make sure you have permissions to access this file, and check that the file is not open or being edited and try again.`
62+
}
63+
64+
const clickedIndex = dialog.showMessageBox({
65+
type: 'error',
66+
message,
67+
detail,
68+
buttons: ['Quit', 'Try Again', 'Reset Configuration'],
69+
});
70+
71+
if (clickedIndex === 0) {
72+
this.userWantsToPreserveErrors = true;
73+
app.quit();
74+
} else if (clickedIndex === 1) {
75+
this.load();
76+
} else {
77+
if (fs.existsSync(this.configFilePath)) {
78+
fs.unlinkSync(this.configFilePath);
79+
}
80+
this.writeTemplateConfigFile();
81+
this.load();
82+
}
2583
}
26-
const row = this._selectStatement.get();
27-
return JSON.parse(row.data)
2884
}
2985

30-
resetConfig(newConfig) {
31-
this._replace(newConfig);
86+
loadSoon = () => {
87+
this._loadDebounced = this._loadDebounced || _.debounce(this.load, 100);
88+
this._loadDebounced();
3289
}
3390

34-
setRawValue(keyPath, value) {
35-
const configData = this.getRawValues();
36-
if (!keyPath) {
37-
throw new Error("Must specify a keyPath to set the config")
91+
observe() {
92+
// watch the config file for edits. This observer needs to be
93+
// replaced if the config file is deleted.
94+
let watcher = null;
95+
const watchCurrentConfigFile = () => {
96+
try {
97+
if (watcher) {
98+
watcher.close();
99+
}
100+
watcher = pathWatcher.watch(this.configFilePath, (e) => {
101+
if (e === 'change') {
102+
this.loadSoon();
103+
}
104+
});
105+
} catch (error) {
106+
this.observeErrorOccurred(error);
107+
}
38108
}
109+
watchCurrentConfigFile();
110+
111+
// watch the config directory (non-recursive) to catch the config file
112+
// being deleted and replaced or atomically edited.
113+
try {
114+
let lastctime = null;
115+
pathWatcher.watch(this.configDirPath, () => {
116+
fs.stat(this.configFilePath, (err, stats) => {
117+
if (err) { return; }
39118

40-
// This edits in place
41-
_.setValueForKeyPath(configData, keyPath, value);
119+
const ctime = stats.ctime.getTime();
120+
if (ctime !== lastctime) {
121+
if (Math.abs(ctime - this.lastSaveTimestamp) > 2000) {
122+
this.loadSoon();
123+
}
124+
watchCurrentConfigFile();
125+
lastctime = ctime;
126+
}
127+
});
128+
})
129+
} catch (error) {
130+
this.observeErrorOccurred(error);
131+
}
132+
}
42133

43-
this._replace(configData);
44-
return configData
134+
observeErrorOccurred = (error) => {
135+
global.errorLogger.reportError(error)
136+
dialog.showMessageBox({
137+
type: 'error',
138+
message: 'Configuration Error',
139+
detail: `
140+
Unable to watch path: ${path.basename(this.configFilePath)}. Make sure you have permissions to
141+
${this.configFilePath}. On linux there are currently problems with watch
142+
sizes.
143+
`,
144+
buttons: ['Okay'],
145+
})
45146
}
46147

47-
_migrateOldConfigs() {
148+
save = () => {
149+
if (this.userWantsToPreserveErrors) {
150+
return;
151+
}
152+
const allSettings = {'*': this.settings};
153+
const allSettingsJSON = JSON.stringify(allSettings, null, 2);
154+
this.lastSaveTimestamp = Date.now();
155+
48156
try {
49-
const oldConfig = path.join(this.configDirPath, 'config.json');
50-
if (fs.existsSync(oldConfig)) {
51-
const configData = JSON.parse(fs.readFileSync(oldConfig))['*'];
52-
this._replace(configData)
53-
fs.unlinkSync(oldConfig)
157+
atomicWriteFileSync(this.configFilePath, allSettingsJSON)
158+
this.saveRetries = 0
159+
} catch (error) {
160+
if (this.saveRetries >= RETRY_SAVES) {
161+
global.errorLogger.reportError(error, {event: 'Failed to save config.json'})
162+
const clickedIndex = dialog.showMessageBox({
163+
type: 'error',
164+
message: `Failed to save "${path.basename(this.configFilePath)}"`,
165+
detail: `\n\nWe were unable to save the file ${this.configFilePath}. Make sure you have permissions to access this file, and check that the file is not open or being edited and try again.`,
166+
buttons: ['Okay', 'Try again'],
167+
})
168+
this.saveRetries = 0
169+
if (clickedIndex === 1) {
170+
this.saveSoon()
171+
}
172+
} else {
173+
this.saveRetries++
174+
this.saveSoon()
54175
}
55-
} catch (err) {
56-
global.errorLogger.reportError(err)
57176
}
58177
}
59178

60-
_replace(configData) {
61-
if (!this._replaceStatement) {
62-
const q = `REPLACE INTO \`config\` (id, data) VALUES (?,?)`;
63-
this._replaceStatement = this.database.prepare(q)
64-
}
65-
this._replaceStatement.run(['*', JSON.stringify(configData)])
179+
saveSoon = () => {
180+
this._saveThrottled = this._saveThrottled || _.throttle(this.save, 100);
181+
this._saveThrottled();
66182
}
67183

68-
async _initializeDatabase() {
69-
const dbPath = databasePath(this.configDirPath, this.specMode);
70-
this.database = await setupDatabase(dbPath)
71-
const setupQuery = `CREATE TABLE IF NOT EXISTS \`config\` (id TEXT PRIMARY KEY, data BLOB)`;
72-
this.database.prepare(setupQuery).run()
184+
getRawValuesString = () => {
185+
return JSON.stringify(this.settings);
73186
}
74187

75-
_initializeConfigDirectory() {
76-
if (!fs.existsSync(this.configDirPath)) {
77-
fs.makeTreeSync(this.configDirPath);
78-
const templateConfigDirPath = path.join(this.resourcePath, 'dot-nylas');
79-
fs.copySync(templateConfigDirPath, this.configDirPath);
188+
setRawValue = (keyPath, value, sourceWebcontentsId) => {
189+
if (keyPath) {
190+
_.setValueForKeyPath(this.settings, keyPath, value);
191+
} else {
192+
this.settings = value;
80193
}
194+
195+
this.emitChangeEvent({sourceWebcontentsId});
196+
this.saveSoon();
197+
return null;
198+
}
199+
200+
emitChangeEvent = ({sourceWebcontentsId} = {}) => {
201+
global.application.config.updateSettings(this.settings);
202+
203+
BrowserWindow.getAllWindows().forEach((win) => {
204+
if ((win.webContents) && (win.webContents.getId() !== sourceWebcontentsId)) {
205+
win.webContents.send('on-config-reloaded', this.settings);
206+
}
207+
});
81208
}
82209
}

src/config.coffee

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -321,9 +321,6 @@ class Config
321321

322322
# Created during initialization, available as `NylasEnv.config`
323323
constructor: ->
324-
# `app` exists in the remote browser process. We do this to use a
325-
# single DB connection for the Config
326-
@configPersistenceManager = app.configPersistenceManager
327324
@emitter = new Emitter
328325
@schema =
329326
type: 'object'
@@ -620,18 +617,20 @@ class Config
620617
@transact =>
621618
settings = @getRawValues()
622619
settings = @makeValueConformToSchema(null, settings, suppressException: true)
623-
@configPersistenceManager.resetConfig(settings)
624-
@load()
620+
@setRawValue(null, settings)
625621
return
626622

627623
emitChangeEvent: ->
628624
@emitter.emit 'did-change' unless @transactDepth > 0
629625

630626
getRawValues: ->
631-
return @configPersistenceManager.getRawValues()
627+
try
628+
return JSON.parse(app.configPersistenceManager.getRawValuesString())
629+
catch
630+
return {}
632631

633632
setRawValue: (keyPath, value) ->
634-
@configPersistenceManager.setRawValue(keyPath, value, webContentsId)
633+
app.configPersistenceManager.setRawValue(keyPath, value, webContentsId)
635634
@load()
636635

637636
# Base schema enforcers. These will coerce raw input into the specified type,

0 commit comments

Comments
 (0)