aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2022-04-26 02:36:02 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2022-05-16 00:55:02 +0200
commit71082348b5d7105945bda90b097aef0a58ea35d9 (patch)
treee4f0115905cfbf4d76a1e0bc03d7815d0925b318
parentchore(deps): remove ms to reduce dependency count (diff)
downloadsophie-71082348b5d7105945bda90b097aef0a58ea35d9.tar.gz
sophie-71082348b5d7105945bda90b097aef0a58ea35d9.tar.zst
sophie-71082348b5d7105945bda90b097aef0a58ea35d9.zip
refactor: remove json5 dependency
Use a more standard config file format and reduce the amount of external code running in the security-sensitive context of the main process. Signed-off-by: Kristóf Marussy <kristof@marussy.com>
-rw-r--r--docs/architecture.md2
-rw-r--r--packages/main/package.json1
-rw-r--r--packages/main/src/infrastructure/config/impl/ConfigFile.ts64
-rw-r--r--packages/main/src/reactions/synchronizeConfig.ts2
-rw-r--r--yarn.lock1
5 files changed, 31 insertions, 39 deletions
diff --git a/docs/architecture.md b/docs/architecture.md
index b76b2ff..8b61288 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -69,7 +69,7 @@ Within the `MainStore`, changes to the [`SharedStore`](https://gitlab.com/say-hi
69Thus it can hold application state relevant to displaying the UI. 69Thus it can hold application state relevant to displaying the UI.
70 70
71The [`Config`](https://gitlab.com/say-hi-to-sophie/sophie/-/blob/main/packages/shared/src/stores/Config.ts) in the `SharedStore` holds the application configuration, including the list of services to be displayed. 71The [`Config`](https://gitlab.com/say-hi-to-sophie/sophie/-/blob/main/packages/shared/src/stores/Config.ts) in the `SharedStore` holds the application configuration, including the list of services to be displayed.
72It is synchronized with the `config.json5` file in user data directory, which should be human-readable and -editable to facilitate debugging and other advanced use cases. 72It is synchronized with the `settings.json` file in user data directory, which should be human-readable and -editable to facilitate debugging and other advanced use cases.
73 73
74In the UI renderer process, the [`RendererStore`](https://gitlab.com/say-hi-to-sophie/sophie/-/blob/main/packages/renderer/src/stores/RendererStore.ts) hold the UI sate. 74In the UI renderer process, the [`RendererStore`](https://gitlab.com/say-hi-to-sophie/sophie/-/blob/main/packages/renderer/src/stores/RendererStore.ts) hold the UI sate.
75It contains a read-only copy of the `SharedStore`. 75It contains a read-only copy of the `SharedStore`.
diff --git a/packages/main/package.json b/packages/main/package.json
index 112c358..fceb392 100644
--- a/packages/main/package.json
+++ b/packages/main/package.json
@@ -15,7 +15,6 @@
15 "electron": "^19.0.0-alpha.1", 15 "electron": "^19.0.0-alpha.1",
16 "fs-extra": "^10.1.0", 16 "fs-extra": "^10.1.0",
17 "i18next": "^21.6.16", 17 "i18next": "^21.6.16",
18 "json5": "^2.2.1",
19 "lodash-es": "^4.17.21", 18 "lodash-es": "^4.17.21",
20 "loglevel": "^1.8.0", 19 "loglevel": "^1.8.0",
21 "loglevel-plugin-prefix": "^0.8.4", 20 "loglevel-plugin-prefix": "^0.8.4",
diff --git a/packages/main/src/infrastructure/config/impl/ConfigFile.ts b/packages/main/src/infrastructure/config/impl/ConfigFile.ts
index a817717..c4e6d22 100644
--- a/packages/main/src/infrastructure/config/impl/ConfigFile.ts
+++ b/packages/main/src/infrastructure/config/impl/ConfigFile.ts
@@ -22,7 +22,6 @@ import { watch } from 'node:fs';
22import { readFile, stat, writeFile } from 'node:fs/promises'; 22import { readFile, stat, writeFile } from 'node:fs/promises';
23import path from 'node:path'; 23import path from 'node:path';
24 24
25import JSON5 from 'json5';
26import { throttle } from 'lodash-es'; 25import { throttle } from 'lodash-es';
27 26
28import type Config from '../../../stores/config/Config'; 27import type Config from '../../../stores/config/Config';
@@ -35,70 +34,65 @@ import type { ReadConfigResult } from '../ConfigRepository';
35const log = getLogger('ConfigFile'); 34const log = getLogger('ConfigFile');
36 35
37export default class ConfigFile implements ConfigRepository { 36export default class ConfigFile implements ConfigRepository {
38 readonly #userDataDir: string; 37 private readonly configFilePath: string;
39 38
40 readonly #configFileName: string; 39 private writingConfig = false;
41 40
42 readonly #configFilePath: string; 41 private timeLastWritten: Date | undefined;
43 42
44 #writingConfig = false; 43 constructor(
45 44 private readonly userDataDir: string,
46 #timeLastWritten: Date | undefined; 45 private readonly configFileName = 'settings.json',
47 46 ) {
48 constructor(userDataDir: string, configFileName = 'config.json5') { 47 this.configFilePath = path.join(userDataDir, configFileName);
49 this.#userDataDir = userDataDir;
50 this.#configFileName = configFileName;
51 this.#configFilePath = path.join(userDataDir, configFileName);
52 } 48 }
53 49
54 async readConfig(): Promise<ReadConfigResult> { 50 async readConfig(): Promise<ReadConfigResult> {
55 let configStr: string; 51 let configStr: string;
56 try { 52 try {
57 configStr = await readFile(this.#configFilePath, 'utf8'); 53 configStr = await readFile(this.configFilePath, 'utf8');
58 } catch (error) { 54 } catch (error) {
59 if (isErrno(error, 'ENOENT')) { 55 if (isErrno(error, 'ENOENT')) {
60 log.debug('Config file', this.#configFilePath, 'was not found'); 56 log.debug('Config file', this.configFilePath, 'was not found');
61 return { found: false }; 57 return { found: false };
62 } 58 }
63 throw error; 59 throw error;
64 } 60 }
65 log.info('Read config file', this.#configFilePath); 61 log.info('Read config file', this.configFilePath);
66 return { 62 return {
67 found: true, 63 found: true,
68 data: JSON5.parse(configStr), 64 data: JSON.parse(configStr),
69 }; 65 };
70 } 66 }
71 67
72 async writeConfig(configSnapshot: Config): Promise<void> { 68 async writeConfig(configSnapshot: Config): Promise<void> {
73 const configJson = JSON5.stringify(configSnapshot, { 69 const configJson = JSON.stringify(configSnapshot, undefined, 2);
74 space: 2, 70 this.writingConfig = true;
75 });
76 this.#writingConfig = true;
77 try { 71 try {
78 await writeFile(this.#configFilePath, configJson, 'utf8'); 72 await writeFile(this.configFilePath, configJson, 'utf8');
79 const { mtime } = await stat(this.#configFilePath); 73 const { mtime } = await stat(this.configFilePath);
80 log.trace('Config file', this.#configFilePath, 'last written at', mtime); 74 log.trace('Config file', this.configFilePath, 'last written at', mtime);
81 this.#timeLastWritten = mtime; 75 this.timeLastWritten = mtime;
82 } finally { 76 } finally {
83 this.#writingConfig = false; 77 this.writingConfig = false;
84 } 78 }
85 log.debug('Wrote config file', this.#configFilePath); 79 log.debug('Wrote config file', this.configFilePath);
86 } 80 }
87 81
88 watchConfig(callback: () => Promise<void>, throttleMs: number): Disposer { 82 watchConfig(callback: () => Promise<void>, throttleMs: number): Disposer {
89 log.debug('Installing watcher for', this.#userDataDir); 83 log.debug('Installing watcher for', this.userDataDir);
90 84
91 const configChanged = throttle(async () => { 85 const configChanged = throttle(async () => {
92 let mtime: Date; 86 let mtime: Date;
93 try { 87 try {
94 const stats = await stat(this.#configFilePath); 88 const stats = await stat(this.configFilePath);
95 mtime = stats.mtime; 89 mtime = stats.mtime;
96 log.trace('Config file last modified at', mtime); 90 log.trace('Config file last modified at', mtime);
97 } catch (error) { 91 } catch (error) {
98 if (isErrno(error, 'ENOENT')) { 92 if (isErrno(error, 'ENOENT')) {
99 log.debug( 93 log.debug(
100 'Config file', 94 'Config file',
101 this.#configFilePath, 95 this.configFilePath,
102 'was deleted after being changed', 96 'was deleted after being changed',
103 ); 97 );
104 return; 98 return;
@@ -106,27 +100,27 @@ export default class ConfigFile implements ConfigRepository {
106 throw error; 100 throw error;
107 } 101 }
108 if ( 102 if (
109 !this.#writingConfig && 103 !this.writingConfig &&
110 (this.#timeLastWritten === undefined || mtime > this.#timeLastWritten) 104 (this.timeLastWritten === undefined || mtime > this.timeLastWritten)
111 ) { 105 ) {
112 log.debug( 106 log.debug(
113 'Found a config file modified at', 107 'Found a config file modified at',
114 mtime, 108 mtime,
115 'which is newer than last written', 109 'which is newer than last written',
116 this.#timeLastWritten, 110 this.timeLastWritten,
117 ); 111 );
118 await callback(); 112 await callback();
119 } 113 }
120 }, throttleMs); 114 }, throttleMs);
121 115
122 const watcher = watch(this.#userDataDir, { 116 const watcher = watch(this.userDataDir, {
123 persistent: false, 117 persistent: false,
124 }); 118 });
125 119
126 watcher.on('change', (eventType, filename) => { 120 watcher.on('change', (eventType, filename) => {
127 if ( 121 if (
128 eventType === 'change' && 122 eventType === 'change' &&
129 (filename === this.#configFileName || filename === null) 123 (filename === this.configFileName || filename === null)
130 ) { 124 ) {
131 configChanged()?.catch((err) => { 125 configChanged()?.catch((err) => {
132 log.error('Unhandled error while listening for config changes', err); 126 log.error('Unhandled error while listening for config changes', err);
@@ -135,7 +129,7 @@ export default class ConfigFile implements ConfigRepository {
135 }); 129 });
136 130
137 return () => { 131 return () => {
138 log.trace('Removing watcher for', this.#configFilePath); 132 log.trace('Removing watcher for', this.configFilePath);
139 watcher.close(); 133 watcher.close();
140 }; 134 };
141 } 135 }
diff --git a/packages/main/src/reactions/synchronizeConfig.ts b/packages/main/src/reactions/synchronizeConfig.ts
index adbe712..7e366e2 100644
--- a/packages/main/src/reactions/synchronizeConfig.ts
+++ b/packages/main/src/reactions/synchronizeConfig.ts
@@ -59,7 +59,7 @@ export default async function synchronizeConfig(
59 lastConfigOnDisk = sharedStore.config; 59 lastConfigOnDisk = sharedStore.config;
60 // We can't use `comparer.structural` from `mobx`, because 60 // We can't use `comparer.structural` from `mobx`, because
61 // it handles missing values and `undefined` values differently, 61 // it handles missing values and `undefined` values differently,
62 // but JSON5 is unable to distinguish them. 62 // but JSON is unable to distinguish them.
63 if (!deepEqual(result.data, lastConfigOnDisk, { strict: true })) { 63 if (!deepEqual(result.data, lastConfigOnDisk, { strict: true })) {
64 await writeConfig(); 64 await writeConfig();
65 } 65 }
diff --git a/yarn.lock b/yarn.lock
index b934e68..0c26d14 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1280,7 +1280,6 @@ __metadata:
1280 i18next: ^21.6.16 1280 i18next: ^21.6.16
1281 jest: ^27.5.1 1281 jest: ^27.5.1
1282 jest-mock: ^27.5.1 1282 jest-mock: ^27.5.1
1283 json5: ^2.2.1
1284 lodash-es: ^4.17.21 1283 lodash-es: ^4.17.21
1285 loglevel: ^1.8.0 1284 loglevel: ^1.8.0
1286 loglevel-plugin-prefix: ^0.8.4 1285 loglevel-plugin-prefix: ^0.8.4