From 34226e389523551d7fcddfa0453e6d76bca94ccb Mon Sep 17 00:00:00 2001 From: "Michael \"Z\" Goddard" Date: Thu, 22 Feb 2024 11:42:59 -0500 Subject: [PATCH 01/12] add macos install --- lib/install/macos.js | 99 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 95 insertions(+), 4 deletions(-) diff --git a/lib/install/macos.js b/lib/install/macos.js index 37c87c1..c093895 100644 --- a/lib/install/macos.js +++ b/lib/install/macos.js @@ -1,9 +1,100 @@ 'use strict'; -exports.install = async function() { - throw new Error('macos install not implemented'); +const { exec: _exec } = require('child_process'); +const { resolve } = require('path'); +const { promisify } = require('util'); + +const LSREGISTER_EXECUTABLE_PATH = + '/System/Library/Frameworks/CoreServices.framework/Versions/Current/Frameworks/LaunchServices.framework/Versions/Current/Support/lsregister'; +const APPLICATION_NAME = 'ATDriverGenericMacOS.app'; +const EXTENSION_IDENTIFIER = 'com.bocoup.ATDriverGenericMacOS.ATDriverGenericMacOSExtension'; +const VOICE_IDENTIFIER = + 'com.bocoup.ATDriverGenericMacOS.ATDriverGenericMacOSExtension.ATDriverGenericMacOSExtension'; +const SYSTEM_VOICE_IDENTIFIER = 'com.apple.Fred'; +const PLUGIN_TRIPLET_IDENTIFIER = 'ausp atdg BOCU'; + +const exec = promisify(_exec); + +/** @typedef {import('child_process').ExecOptions} ExecOptions */ + +/** + * @returns {Promise} + */ +exports.install = async function () { + const options = await getExecOptions(); + + if (await isInstalled()) { + throw new Error('Already installed'); + } + + await removeQuarantine(options); + await registerExtensions(options); + await enableExtension(); + await setSystemVoice(VOICE_IDENTIFIER); }; -exports.uninstall = async function() { - throw new Error('macos uninstall not implemented'); +/** + * @returns {Promise} + */ +exports.uninstall = async function () { + const options = await getExecOptions(); + + if (!(await isInstalled())) { + throw new Error('Not installed'); + } + + await setSystemVoice(SYSTEM_VOICE_IDENTIFIER); + await unregisterExtensions(options); }; + +const isInstalled = async function () { + const { stdout } = await exec(`auval -v ${PLUGIN_TRIPLET_IDENTIFIER}`); + return /ATDriverGenericMacOSExtension/.test(stdout); +}; + +/** + * @returns {Promise} + */ +const getExecOptions = async function () { + return { + cwd: resolve(__dirname, '../../Release/macos'), + }; +}; + +/** + * @param {ExecOptions} options + * @returns {Promise} + */ +async function removeQuarantine(options) { + await exec(`xattr -r -d com.apple.quarantine ${APPLICATION_NAME}`, options); +} + +/** + * @param {ExecOptions} options + * @returns {Promise} + */ +async function registerExtensions(options) { + await exec(`${LSREGISTER_EXECUTABLE_PATH} -f -R -trusted ${APPLICATION_NAME}`, options); +} + +/** + * @param {ExecOptions} options + * @returns {Promise} + */ +async function unregisterExtensions(options) { + await exec(`${LSREGISTER_EXECUTABLE_PATH} -f -R -trusted -u ${APPLICATION_NAME}`, options); +} + +async function enableExtension() { + await exec(`pluginkit -e use -i ${EXTENSION_IDENTIFIER}`); +} + +/** + * @param {string} voice + * @returns {Promise} + */ +async function setSystemVoice(voice) { + await exec( + `defaults write com.apple.Accessibility SpeechVoiceIdentifierForLanguage '{2 = {en = "${voice}";};}`, + ); +} From 5ca0350b7693e65b046d87fbafc6ecee2afbbc8a Mon Sep 17 00:00:00 2001 From: Mike Pennisi Date: Mon, 8 Apr 2024 19:49:45 -0400 Subject: [PATCH 02/12] Add note on interdependent values --- lib/install/macos.js | 8 +++++++ .../Model/AudioUnitHostModel.swift | 5 +++++ .../ATDriverGenericMacOSExtension/Info.plist | 21 +++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/lib/install/macos.js b/lib/install/macos.js index c093895..573ea67 100644 --- a/lib/install/macos.js +++ b/lib/install/macos.js @@ -11,6 +11,14 @@ const EXTENSION_IDENTIFIER = 'com.bocoup.ATDriverGenericMacOS.ATDriverGenericMac const VOICE_IDENTIFIER = 'com.bocoup.ATDriverGenericMacOS.ATDriverGenericMacOSExtension.ATDriverGenericMacOSExtension'; const SYSTEM_VOICE_IDENTIFIER = 'com.apple.Fred'; +/** + * This string comprises three tokens (the "type", "subtype", and + * "manufacturer" of the Audio Unit) which must be kept in-sync with other + * references in this project: + * + * - src/macos/ATDriverGenericMacOS/ATDriverGenericMacOS/Model/AudioUnitHostModel.swift + * - src/macos/ATDriverGenericMacOS/ATDriverGenericMacOSExtension/Info.plist + */ const PLUGIN_TRIPLET_IDENTIFIER = 'ausp atdg BOCU'; const exec = promisify(_exec); diff --git a/src/macos/ATDriverGenericMacOS/ATDriverGenericMacOS/Model/AudioUnitHostModel.swift b/src/macos/ATDriverGenericMacOS/ATDriverGenericMacOS/Model/AudioUnitHostModel.swift index 0f66e7e..b9f1a88 100644 --- a/src/macos/ATDriverGenericMacOS/ATDriverGenericMacOS/Model/AudioUnitHostModel.swift +++ b/src/macos/ATDriverGenericMacOS/ATDriverGenericMacOS/Model/AudioUnitHostModel.swift @@ -31,6 +31,11 @@ class AudioUnitHostModel: ObservableObject { let auValString: String + /// These values must be kept in-sync with other references in this + /// project: + /// + /// - lib/install/macos.js + /// - src/macos/ATDriverGenericMacOS/ATDriverGenericMacOSExtension/Info.plist init(type: String = "ausp", subType: String = "atdg", manufacturer: String = "BOCU") { self.type = type self.subType = subType diff --git a/src/macos/ATDriverGenericMacOS/ATDriverGenericMacOSExtension/Info.plist b/src/macos/ATDriverGenericMacOS/ATDriverGenericMacOSExtension/Info.plist index b5434e3..b07e34b 100644 --- a/src/macos/ATDriverGenericMacOS/ATDriverGenericMacOSExtension/Info.plist +++ b/src/macos/ATDriverGenericMacOS/ATDriverGenericMacOSExtension/Info.plist @@ -11,18 +11,39 @@ description ATDriverGenericMacOSExtension + manufacturer BOCU name Bocoup LLC: ATDriverGenericMacOSExtension sandboxSafe + subtype atdg tags Speech Synthesizer + type ausp version From 4873cbedb702939add93f2f2eba20b98af0fa2ee Mon Sep 17 00:00:00 2001 From: Mike Pennisi Date: Mon, 8 Apr 2024 20:35:02 -0400 Subject: [PATCH 03/12] Document the intent of utility function --- lib/install/macos.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/install/macos.js b/lib/install/macos.js index 573ea67..efbff4d 100644 --- a/lib/install/macos.js +++ b/lib/install/macos.js @@ -70,6 +70,11 @@ const getExecOptions = async function () { }; /** + * Remove the "quarantine" attribute which macOS uses to prevent accidental + * execution of code from unverified sources. + * + * https://support.apple.com/en-us/101987 + * * @param {ExecOptions} options * @returns {Promise} */ From e05b38ec25679b8863e0f9cff67f8a0baced6f87 Mon Sep 17 00:00:00 2001 From: Mike Pennisi Date: Mon, 22 Apr 2024 20:14:41 -0400 Subject: [PATCH 04/12] Surface requirement for key press automation --- lib/commands/install.js | 10 +++++-- lib/install/macos.js | 63 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/lib/commands/install.js b/lib/commands/install.js index aaec388..4ba182d 100644 --- a/lib/commands/install.js +++ b/lib/commands/install.js @@ -5,11 +5,17 @@ const { loadOsModule } = require('../helpers/load-os-module'); module.exports = /** @type {import('yargs').CommandModule} */ ({ command: 'install', describe: 'Install text to speech extension and other support', - async handler() { + builder(yargs) { + return yargs.option('unattended', { + desc: 'Fail if installation requires human intervention', + boolean: true, + }); + }, + async handler({ unattended }) { const installDelegate = loadOsModule('install', { darwin: () => require('../install/macos'), win32: () => require('../install/win32'), }); - await installDelegate.install(); + await installDelegate.install({ unattended }); }, }); diff --git a/lib/install/macos.js b/lib/install/macos.js index efbff4d..5637f4e 100644 --- a/lib/install/macos.js +++ b/lib/install/macos.js @@ -3,6 +3,7 @@ const { exec: _exec } = require('child_process'); const { resolve } = require('path'); const { promisify } = require('util'); +const { 'interaction.pressKeys': pressKeys } = require('../modules/macos/interaction'); const LSREGISTER_EXECUTABLE_PATH = '/System/Library/Frameworks/CoreServices.framework/Versions/Current/Frameworks/LaunchServices.framework/Versions/Current/Support/lsregister'; @@ -22,15 +23,60 @@ const SYSTEM_VOICE_IDENTIFIER = 'com.apple.Fred'; const PLUGIN_TRIPLET_IDENTIFIER = 'ausp atdg BOCU'; const exec = promisify(_exec); +const enableKeyAutomationPrompt = `This tool can only be installed on systems which allow automated key pressing. +Please allow the Terminal application to control your computer (the setting is +controlled in System Settings > Privacy & Security > Accessibility).`; /** @typedef {import('child_process').ExecOptions} ExecOptions */ /** + * Prompt the user to press any key. Resolves when the user presses a key. + * * @returns {Promise} */ -exports.install = async function () { +const promptForManualKeyPress = async () => { + process.stdout.write('Press any key to continue... '); + const wasRaw = process.stdin.isRaw; + process.stdin.setRawMode(true); + process.stdin.resume(); + const byteArray = await new Promise(resolve => { + process.stdin.once('data', data => resolve(Array.from(data))); + }); + + process.stdin.pause(); + process.stdin.setRawMode(wasRaw); + process.stdout.write('\n'); + + // Honor "Control + C" motion by exiting. + if (byteArray[0] === 3) { + process.exit(1); + } +}; + +/** + * @param {object} options + * @param {boolean} options.unattended - Whether installation should fail if + * human intervention is required + * + * @returns {Promise} + */ +exports.install = async function ({ unattended }) { const options = await getExecOptions(); + if (!(await canPressKeys())) { + if (unattended) { + throw new Error('The system cannot automate key pressing.'); + } else { + console.error(enableKeyAutomationPrompt); + + await promptForManualKeyPress(); + + if (!(await canPressKeys())) { + throw new Error('The system cannot automate key pressing.'); + } + } + } + if (await isInstalled()) { throw new Error('Already installed'); } @@ -55,6 +101,21 @@ exports.uninstall = async function () { await unregisterExtensions(options); }; +/** + * Experimentally determine whether the current system supports automated key + * pressing by attempting to press an arbitrary key. + * + * @returns {Promise} + */ +const canPressKeys = async () => { + try { + await pressKeys(null, { keys: ['shift'] }); + } catch ({}) { + return false; + } + return true; +}; + const isInstalled = async function () { const { stdout } = await exec(`auval -v ${PLUGIN_TRIPLET_IDENTIFIER}`); return /ATDriverGenericMacOSExtension/.test(stdout); From 76639d7999434cffb8fb3cbe3aefc35b237fa198 Mon Sep 17 00:00:00 2001 From: Mike Pennisi Date: Thu, 25 Apr 2024 15:46:30 -0400 Subject: [PATCH 05/12] Correct error in command --- lib/install/macos.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/install/macos.js b/lib/install/macos.js index 5637f4e..a73df4d 100644 --- a/lib/install/macos.js +++ b/lib/install/macos.js @@ -169,6 +169,6 @@ async function enableExtension() { */ async function setSystemVoice(voice) { await exec( - `defaults write com.apple.Accessibility SpeechVoiceIdentifierForLanguage '{2 = {en = "${voice}";};}`, + `defaults write com.apple.Accessibility SpeechVoiceIdentifierForLanguage '{2 = {en = "${voice}";};}'`, ); } From 452b09c99cb9fcfb232f63627f4cee962932bfc8 Mon Sep 17 00:00:00 2001 From: Mike Pennisi Date: Tue, 30 Apr 2024 17:11:41 -0400 Subject: [PATCH 06/12] Add debugging information --- lib/install/macos.js | 28 ++++++++++++--- package-lock.json | 35 +++++++++++++++---- package.json | 1 + .../ATDriverGenericMacOSExtension.xcscheme | 1 - 4 files changed, 53 insertions(+), 12 deletions(-) diff --git a/lib/install/macos.js b/lib/install/macos.js index a73df4d..25784c0 100644 --- a/lib/install/macos.js +++ b/lib/install/macos.js @@ -3,6 +3,9 @@ const { exec: _exec } = require('child_process'); const { resolve } = require('path'); const { promisify } = require('util'); + +const debug = require('debug')('install'); + const { 'interaction.pressKeys': pressKeys } = require('../modules/macos/interaction'); const LSREGISTER_EXECUTABLE_PATH = @@ -137,10 +140,12 @@ const getExecOptions = async function () { * https://support.apple.com/en-us/101987 * * @param {ExecOptions} options - * @returns {Promise} + * @returns {Promise} Whether a change took place */ async function removeQuarantine(options) { + debug('Removing macOS quarantine'); await exec(`xattr -r -d com.apple.quarantine ${APPLICATION_NAME}`, options); + return true; } /** @@ -148,6 +153,7 @@ async function removeQuarantine(options) { * @returns {Promise} */ async function registerExtensions(options) { + debug('Registering trusted macOS extension'); await exec(`${LSREGISTER_EXECUTABLE_PATH} -f -R -trusted ${APPLICATION_NAME}`, options); } @@ -156,19 +162,33 @@ async function registerExtensions(options) { * @returns {Promise} */ async function unregisterExtensions(options) { + debug('Unregistering trusted macOS extension'); await exec(`${LSREGISTER_EXECUTABLE_PATH} -f -R -trusted -u ${APPLICATION_NAME}`, options); } async function enableExtension() { + debug('Enabling macOS extension'); await exec(`pluginkit -e use -i ${EXTENSION_IDENTIFIER}`); } /** - * @param {string} voice + * @param {string} newValue the identifier for the voice to set * @returns {Promise} */ -async function setSystemVoice(voice) { +async function setSystemVoice(newValue) { + debug(`Setting macOS system voice to "${newValue}"`); + + const {stdout} = await exec('defaults read com.apple.Accessibility SpeechVoiceIdentifierForLanguage'); + const currentValue = stdout.replace(/[\s]/g, '').match(/2={en="([^"]+)";};/); + + debug(`Current value: ${currentValue ? JSON.stringify(currentValue[1]) : '(unset)'}`); + + if (currentValue && currentValue[1] === newValue) { + debug('Already set.'); + return; + } + await exec( - `defaults write com.apple.Accessibility SpeechVoiceIdentifierForLanguage '{2 = {en = "${voice}";};}'`, + `defaults write com.apple.Accessibility SpeechVoiceIdentifierForLanguage '{2 = {en = "${newValue}";};}'`, ); } diff --git a/package-lock.json b/package-lock.json index 54568bd..8aa436f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,9 +7,9 @@ "": { "name": "@bocoup/windows-sapi-tts-engine-for-automation", "version": "0.0.4", - "hasInstallScript": true, "license": "MIT", "dependencies": { + "debug": "^4.3.4", "robotjs": "^0.6.0", "uuid": "^9.0.1", "ws": "^8.2.3", @@ -362,10 +362,9 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, "node_modules/debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dependencies": { "ms": "2.1.2" }, @@ -381,8 +380,7 @@ "node_modules/debug/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/decamelize": { "version": "4.0.0", @@ -904,6 +902,29 @@ "url": "https://opencollective.com/mochajs" } }, + "node_modules/mocha/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mocha/node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, "node_modules/mocha/node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", diff --git a/package.json b/package.json index 46264cd..91b3458 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "typescript": "^5.3.3" }, "dependencies": { + "debug": "^4.3.4", "robotjs": "^0.6.0", "uuid": "^9.0.1", "ws": "^8.2.3", diff --git a/src/macos/ATDriverGenericMacOS/ATDriverGenericMacOS.xcodeproj/xcshareddata/xcschemes/ATDriverGenericMacOSExtension.xcscheme b/src/macos/ATDriverGenericMacOS/ATDriverGenericMacOS.xcodeproj/xcshareddata/xcschemes/ATDriverGenericMacOSExtension.xcscheme index 76c7396..12609e4 100644 --- a/src/macos/ATDriverGenericMacOS/ATDriverGenericMacOS.xcodeproj/xcshareddata/xcschemes/ATDriverGenericMacOSExtension.xcscheme +++ b/src/macos/ATDriverGenericMacOS/ATDriverGenericMacOS.xcodeproj/xcshareddata/xcschemes/ATDriverGenericMacOSExtension.xcscheme @@ -95,7 +95,6 @@ savedToolIdentifier = "" useCustomWorkingDirectory = "NO" debugDocumentVersioning = "YES" - askForAppToLaunch = "Yes" launchAutomaticallySubstyle = "2"> From 15539bfd10b539ab68d6e1b6384269566e069a4c Mon Sep 17 00:00:00 2001 From: Mike Pennisi Date: Tue, 30 Apr 2024 17:12:22 -0400 Subject: [PATCH 07/12] Include files built for macOS in release --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 91b3458..17f4b4d 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "lib", "Release/AutomationTtsEngine.dll", "Release/MakeVoice.exe", - "Release/Vocalizer.exe" + "Release/Vocalizer.exe", + "Release/macos" ], "author": "", "license": "MIT", From 92e6fe1c9b739d4496c5d194f74b88f031afbd5d Mon Sep 17 00:00:00 2001 From: Mike Pennisi Date: Tue, 30 Apr 2024 18:09:42 -0400 Subject: [PATCH 08/12] Improve detection algorithm --- lib/install/macos.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/install/macos.js b/lib/install/macos.js index 25784c0..deb6aa7 100644 --- a/lib/install/macos.js +++ b/lib/install/macos.js @@ -120,7 +120,17 @@ const canPressKeys = async () => { }; const isInstalled = async function () { - const { stdout } = await exec(`auval -v ${PLUGIN_TRIPLET_IDENTIFIER}`); + let stdout; + + try { + ({ stdout } = await exec(`auval -v ${PLUGIN_TRIPLET_IDENTIFIER}`)); + } catch (error) { + if (error.stdout && error.stdout.includes('didn\'t find the component')) { + return false; + } + throw error; + } + return /ATDriverGenericMacOSExtension/.test(stdout); }; From 99387bafb75c2ea699520e17d5e9f4e7b7264b28 Mon Sep 17 00:00:00 2001 From: Mike Pennisi Date: Tue, 30 Apr 2024 22:08:34 -0400 Subject: [PATCH 09/12] Support environment with no prior setting --- lib/install/macos.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/install/macos.js b/lib/install/macos.js index deb6aa7..3ee9851 100644 --- a/lib/install/macos.js +++ b/lib/install/macos.js @@ -187,9 +187,19 @@ async function enableExtension() { */ async function setSystemVoice(newValue) { debug(`Setting macOS system voice to "${newValue}"`); + let stdout; + + try { + ({stdout} = await exec('defaults read com.apple.Accessibility SpeechVoiceIdentifierForLanguage')); + } catch (error) { + if (!error || !error.stderr.includes('does not exist')) { + throw error; + } + } - const {stdout} = await exec('defaults read com.apple.Accessibility SpeechVoiceIdentifierForLanguage'); - const currentValue = stdout.replace(/[\s]/g, '').match(/2={en="([^"]+)";};/); + const currentValue = stdout ? + stdout.replace(/[\s]/g, '').match(/2={en="([^"]+)";};/) : + null; debug(`Current value: ${currentValue ? JSON.stringify(currentValue[1]) : '(unset)'}`); From bbd1383cba9a4bbc53e9714aa5f1faa5bee7f685 Mon Sep 17 00:00:00 2001 From: Mike Pennisi Date: Tue, 14 May 2024 14:54:37 -0400 Subject: [PATCH 10/12] Add support for "enter" key --- lib/helpers/macos/KeyCode.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/helpers/macos/KeyCode.js b/lib/helpers/macos/KeyCode.js index ab30f14..e4ce91b 100644 --- a/lib/helpers/macos/KeyCode.js +++ b/lib/helpers/macos/KeyCode.js @@ -70,6 +70,7 @@ exports.KeyCode = { numpadAdd: 69, numpadClear: 71, numpadDivide: 75, + enter: 76, numpadSubtract: 78, numpadEqual: 81, numpad0: 82, From ea80e6ab6de49541b95caf50c22db176339de989 Mon Sep 17 00:00:00 2001 From: Mike Pennisi Date: Tue, 14 May 2024 15:16:44 -0400 Subject: [PATCH 11/12] Retrieve VoiceOver version (via macOS version) --- lib/modules/macos/session.js | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/lib/modules/macos/session.js b/lib/modules/macos/session.js index e761706..e94419a 100644 --- a/lib/modules/macos/session.js +++ b/lib/modules/macos/session.js @@ -2,18 +2,38 @@ 'use strict'; +const child_process = require('child_process'); + const { v4: uuid } = require('uuid'); +/** + * @returns {Promise} + */ +const getMacOSVersion = async () => { + return new Promise((resolve, reject) => { + child_process.exec('sw_vers -productVersion', (error, stdout, stderr) => { + if (error) { + reject(new Error(stderr)); + return; + } + resolve(stdout.trim()); + }); + }); +}; + const newSession = /** @type {ATDriverModules.SessionNewSession} */ ( - (websocket, params) => { + async (websocket, params) => { // TODO: match requested capabilities // const { capabilities } = params; websocket.sessionId = uuid(); + return { sessionId: websocket.sessionId, capabilities: { atName: 'Voiceover', - atVersion: 'TODO', + // The ARIA-AT Community Group considers the MacOS version identifier + // to be an accurate identifier for the VoiceOver screen reader. + atVersion: await getMacOSVersion(), platformName: 'macos', }, }; From 48bc03234d55d78f038d96c1c7698c41423dc08a Mon Sep 17 00:00:00 2001 From: Mike Pennisi Date: Tue, 14 May 2024 15:18:24 -0400 Subject: [PATCH 12/12] Reformat --- lib/install/macos.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/install/macos.js b/lib/install/macos.js index 3ee9851..13e9752 100644 --- a/lib/install/macos.js +++ b/lib/install/macos.js @@ -125,7 +125,7 @@ const isInstalled = async function () { try { ({ stdout } = await exec(`auval -v ${PLUGIN_TRIPLET_IDENTIFIER}`)); } catch (error) { - if (error.stdout && error.stdout.includes('didn\'t find the component')) { + if (error.stdout && error.stdout.includes("didn't find the component")) { return false; } throw error; @@ -190,16 +190,16 @@ async function setSystemVoice(newValue) { let stdout; try { - ({stdout} = await exec('defaults read com.apple.Accessibility SpeechVoiceIdentifierForLanguage')); + ({ stdout } = await exec( + 'defaults read com.apple.Accessibility SpeechVoiceIdentifierForLanguage', + )); } catch (error) { if (!error || !error.stderr.includes('does not exist')) { throw error; } } - const currentValue = stdout ? - stdout.replace(/[\s]/g, '').match(/2={en="([^"]+)";};/) : - null; + const currentValue = stdout ? stdout.replace(/[\s]/g, '').match(/2={en="([^"]+)";};/) : null; debug(`Current value: ${currentValue ? JSON.stringify(currentValue[1]) : '(unset)'}`);