-
-
Notifications
You must be signed in to change notification settings - Fork 44
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: Update simulator preferences handling (#298)
- Loading branch information
1 parent
dd10637
commit 7f6b71d
Showing
7 changed files
with
289 additions
and
70 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
import _ from 'lodash'; | ||
import { DOMParser, XMLSerializer } from 'xmldom'; | ||
import { exec } from 'teen_process'; | ||
import B from 'bluebird'; | ||
import log from './logger'; | ||
|
||
/** | ||
* Serializes the given value to plist-compatible | ||
* XML representation, which is ready for further usage | ||
* with `defaults` command line tool arguments | ||
* | ||
* @param {*} value The value to be serialized | ||
* @param {boolean} serialize [true] Whether to serialize the resulting | ||
* XML to string or to return raw HTMLElement instance | ||
* @returns {HTMLElement|string} Either string or raw node representation of | ||
* the given value | ||
* @throws {TypeError} If it is not known how to serialize the given value | ||
*/ | ||
function toXmlArg (value, serialize = true) { | ||
let xmlDoc = null; | ||
|
||
if (_.isPlainObject(value)) { | ||
xmlDoc = new DOMParser().parseFromString('<dict></dict>', 'text/xml'); | ||
for (const [subKey, subValue] of _.toPairs(value)) { | ||
const keyEl = xmlDoc.createElement('key'); | ||
const keyTextEl = xmlDoc.createTextNode(subKey); | ||
keyEl.appendChild(keyTextEl); | ||
xmlDoc.documentElement.appendChild(keyEl); | ||
const subValueEl = xmlDoc.importNode(toXmlArg(subValue, false), true); | ||
xmlDoc.documentElement.appendChild(subValueEl); | ||
} | ||
} else if (_.isArray(value)) { | ||
xmlDoc = new DOMParser().parseFromString('<array></array>', 'text/xml'); | ||
for (const subValue of value) { | ||
const subValueEl = xmlDoc.importNode(toXmlArg(subValue, false), true); | ||
xmlDoc.documentElement.appendChild(subValueEl); | ||
} | ||
} else if (_.isBoolean(value)) { | ||
xmlDoc = new DOMParser().parseFromString(value ? '<true/>' : '<false/>', 'text/xml'); | ||
} else if (_.isInteger(value)) { | ||
xmlDoc = new DOMParser().parseFromString(`<integer>${value}</integer>`, 'text/xml'); | ||
} else if (_.isNumber(value)) { | ||
xmlDoc = new DOMParser().parseFromString(`<real>${value}</real>`, 'text/xml'); | ||
} else if (_.isString(value)) { | ||
xmlDoc = new DOMParser().parseFromString(`<string></string>`, 'text/xml'); | ||
const valueTextEl = xmlDoc.createTextNode(value); | ||
xmlDoc.documentElement.appendChild(valueTextEl); | ||
} | ||
|
||
if (!xmlDoc) { | ||
throw new TypeError(`The defaults value ${JSON.stringify(value)} cannot be written, ` + | ||
`because it is not known how to handle its type`); | ||
} | ||
|
||
return serialize | ||
? new XMLSerializer().serializeToString(xmlDoc.documentElement) | ||
: xmlDoc.documentElement; | ||
} | ||
|
||
/** | ||
* Generates command line args for the `defaults` | ||
* command line utility based on the given preference values mapping. | ||
* See https://shadowfile.inode.link/blog/2018/06/advanced-defaults1-usage/ | ||
* for more details. | ||
* | ||
* @param {Object} valuesMap Preferences mapping | ||
* @returns {Array<Array<string>>} Each item in the array | ||
* is the `defaults write <plist>` command suffix | ||
*/ | ||
function generateUpdateCommandArgs (valuesMap) { | ||
const resultArgs = []; | ||
for (const [key, value] of _.toPairs(valuesMap)) { | ||
try { | ||
if (_.isPlainObject(value)) { | ||
const dictArgs = [key, '-dict-add']; | ||
for (const [subKey, subValue] of _.toPairs(value)) { | ||
dictArgs.push(subKey, toXmlArg(subValue)); | ||
} | ||
resultArgs.push(dictArgs); | ||
} else if (_.isArray(value)) { | ||
const arrayArgs = [key, '-array-add']; | ||
for (const subValue of arrayArgs) { | ||
arrayArgs.push(toXmlArg(subValue)); | ||
} | ||
resultArgs.push(arrayArgs); | ||
} else { | ||
resultArgs.push([key, toXmlArg(value)]); | ||
} | ||
} catch (e) { | ||
if (e instanceof TypeError) { | ||
log.warn(e.message); | ||
} else { | ||
throw e; | ||
} | ||
} | ||
} | ||
return resultArgs; | ||
} | ||
|
||
|
||
class NSUserDefaults { | ||
constructor (plist) { | ||
this.plist = plist; | ||
} | ||
|
||
/** | ||
* Reads the content of the given plist file using plutil command line tool | ||
* and serializes it to a JSON representation | ||
* | ||
* @returns {Object} The serialized plist content | ||
* @throws {Error} If there was an error during serialization | ||
*/ | ||
async asJson () { | ||
try { | ||
const {stdout} = await exec('plutil', ['-convert', 'json', '-o', '-', this.plist]); | ||
return JSON.parse(stdout); | ||
} catch (e) { | ||
throw new Error(`'${this.plist}' cannot be converted to JSON. Original error: ${e.stderr || e.message}`); | ||
} | ||
} | ||
|
||
/** | ||
* Updates the content of the given plist file. | ||
* If the plist does not exist yet then it is going to be created. | ||
* | ||
* @param {Object} valuesMap Mapping of preference values to update. | ||
* If any of item values are of dictionary type then only the first level dictionary gets | ||
* updated. Everything below this level will be replaced. This is the known limitation | ||
* of the `defaults` command line tool. A workaround for it would be to read the current | ||
* preferences mapping first and merge it with this value. | ||
* @throws {Error} If there was an error while updating the plist | ||
*/ | ||
async update (valuesMap) { | ||
if (!_.isPlainObject(valuesMap)) { | ||
throw new TypeError(`plist values must be a map. '${valuesMap}' is given instead`); | ||
} | ||
if (_.isEmpty(valuesMap)) { | ||
return; | ||
} | ||
|
||
const commandArgs = generateUpdateCommandArgs(valuesMap); | ||
try { | ||
await B.all(commandArgs.map((args) => exec('defaults', ['write', this.plist, ...args]))); | ||
} catch (e) { | ||
throw new Error(`Could not write defaults into '${this.plist}'. Original error: ${e.stderr || e.message}`); | ||
} | ||
} | ||
} | ||
|
||
|
||
export { | ||
NSUserDefaults, | ||
toXmlArg, generateUpdateCommandArgs, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.