Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add install and uninstall subcommands #42

Merged
merged 5 commits into from
Apr 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,24 @@ which is required by the automation voice.

npm install -g @bocoup/windows-sapi-tts-engine-for-automation

2. Run the install command in a terminal:

at-driver install

If prompted for system administration permission, grant permission.

2. Start the server by executing the following command in a terminal:
3. Start the server by executing the following command in a terminal:

at-driver
at-driver serve

The process will write a message to the standard error stream when the
WebSocket server is listening for connections. The `--help` flag will cause
the command to output advanced usage instructions (e.g. `at-driver --help`).

3. Configure any screen reader to use the synthesizer named "Microsoft Speech
4. Configure any screen reader to use the synthesizer named "Microsoft Speech
API version 5" and the text-to-speech voice named "Bocoup Automation Voice."

4. Use any WebSocket client to connect to the server. The protocol is described
5. Use any WebSocket client to connect to the server. The protocol is described
below. (The server will print protocol messages to its standard error stream
for diagnostic purposes only. Neither the format nor the availability of
this output is guaranteed, making it inappropriate for external use.)
Expand Down
83 changes: 11 additions & 72 deletions lib/cli.js
Original file line number Diff line number Diff line change
@@ -1,83 +1,22 @@
'use strict';

const fs = require('fs/promises');

const yargs = require('yargs/yargs');
const { hideBin } = require('yargs/helpers');

const createCommandServer = require('./create-command-server');
const createVoiceServer = require('./create-voice-server');
const WINDOWS_NAMED_PIPE = '\\\\?\\pipe\\my_pipe';
const MACOS_SYSTEM_DIR = '/usr/local/var/at_driver_generic';
const MACOS_SOCKET_UNIX_PATH = '/usr/local/var/at_driver_generic/driver.socket';
const DEFAULT_PORT = 4382;
const installCommand = require('./commands/install');
const serveCommand = require('./commands/serve');
const uninstallCommand = require('./commands/uninstall');

/**
* Print logging information to the process's standard error stream, annotated
* with a timestamp describing the moment that the message was emitted.
* @param {import('process')} process
*/
const log = (...args) => console.error(new Date().toISOString(), ...args);

module.exports = async process => {
const argv = await yargs(hideBin(process.argv))
.option('port', {
coerce(string) {
if (!/^(0|[1-9][0-9]*)$/.test(string)) {
throw new TypeError(
`"port" option: expected a non-negative integer value but received "${string}"`,
);
}
return Number(string);
},
default: DEFAULT_PORT,
describe: 'TCP port on which to listen for WebSocket connections',
// Do not use the `number` type provided by `yargs` because it tolerates
// JavaScript numeric literal forms which are likely typos in this
// context (e.g. `0xf` or `1e-0`).
type: 'string',
requiresArg: true,
})
await yargs(hideBin(process.argv))
.command(installCommand)
.command(uninstallCommand)
.command(serveCommand)
.demandCommand(1, 1)
.strict()
.help()
.parse();

const socketPath = await prepareSocketPath();

const [commandServer, voiceServer] = await Promise.all([
createCommandServer(argv.port),
createVoiceServer(socketPath),
]);

log(`listening on port ${argv.port}`);

commandServer.on('error', error => {
log(`error: ${error}`);
});

voiceServer.on('message', message => {
log(`voice server received message ${JSON.stringify(message)}`);
if (message.name == 'speech') {
commandServer.broadcast({
method: 'interaction.capturedOutput',
params: { data: message.data },
});
}
});

voiceServer.on('error', error => {
log(`error: ${error}`);
});
};

const prepareSocketPath = async () => {
if (process.platform === 'win32') {
return WINDOWS_NAMED_PIPE;
} else if (process.platform === 'darwin') {
await fs.mkdir(MACOS_SYSTEM_DIR, { recursive: true });
await fs.unlink(MACOS_SOCKET_UNIX_PATH).catch(error => {
if (!error || error.code !== 'ENOENT') {
throw error;
}
});
return MACOS_SOCKET_UNIX_PATH;
}
throw new Error(`unsupported host platform '${process.platform}'`);
};
15 changes: 15 additions & 0 deletions lib/commands/install.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use strict';

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() {
const installDelegate = loadOsModule('install', {
darwin: () => require('../install/macos'),
win32: () => require('../install/win32'),
});
await installDelegate.install();
},
});
85 changes: 85 additions & 0 deletions lib/commands/serve.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
'use strict';

const fs = require('fs/promises');

const createCommandServer = require('../create-command-server');
const createVoiceServer = require('../create-voice-server');

const WINDOWS_NAMED_PIPE = '\\\\?\\pipe\\my_pipe';
const MACOS_SYSTEM_DIR = '/usr/local/var/at_driver_generic';
const MACOS_SOCKET_UNIX_PATH = '/usr/local/var/at_driver_generic/driver.socket';
const DEFAULT_PORT = 4382;


/**
* Print logging information to the process's standard error stream, annotated
* with a timestamp describing the moment that the message was emitted.
*/
const log = (...args) => console.error(new Date().toISOString(), ...args);

const prepareSocketPath = async () => {
if (process.platform === 'win32') {
return WINDOWS_NAMED_PIPE;
} else if (process.platform === 'darwin') {
await fs.mkdir(MACOS_SYSTEM_DIR, { recursive: true });
await fs.unlink(MACOS_SOCKET_UNIX_PATH).catch(error => {
if (!error || error.code !== 'ENOENT') {
throw error;
}
});
return MACOS_SOCKET_UNIX_PATH;
}
throw new Error(`unsupported host platform '${process.platform}'`);
};

module.exports = /** @type {import('yargs').CommandModule} */ ({
command: 'serve',
describe: 'Run at-driver server',
builder(yargs) {
return yargs.option('port', {
coerce(string) {
if (!/^(0|[1-9][0-9]*)$/.test(string)) {
throw new TypeError(
`"port" option: expected a non-negative integer value but received "${string}"`,
);
}
return Number(string);
},
default: DEFAULT_PORT,
describe: 'TCP port on which to listen for WebSocket connections',
// Do not use the `number` type provided by `yargs` because it tolerates
// JavaScript numeric literal forms which are likely typos in this
// context (e.g. `0xf` or `1e-0`).
type: 'string',
requiresArg: true,
});
},
async handler(argv) {
const socketPath = await prepareSocketPath();

const [commandServer, voiceServer] = await Promise.all([
createCommandServer(argv.port),
createVoiceServer(socketPath),
]);

log(`listening on port ${argv.port}`);

commandServer.on('error', error => {
log(`error: ${error}`);
});

voiceServer.on('message', message => {
log(`voice server received message ${JSON.stringify(message)}`);
if (message.name == 'speech') {
commandServer.broadcast({
method: 'interaction.capturedOutput',
params: { data: message.data },
});
}
});

voiceServer.on('error', error => {
log(`error: ${error}`);
});
},
});
15 changes: 15 additions & 0 deletions lib/commands/uninstall.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use strict';

const { loadOsModule } = require('../helpers/load-os-module');

module.exports = /** @type {import('yargs').CommandModule} */ ({
command: 'uninstall',
describe: 'Uninstall text to speech extension and other support',
async handler() {
const installDelegate = loadOsModule('install', {
darwin: () => require('../install/macos'),
win32: () => require('../install/win32'),
});
await installDelegate.uninstall();
},
});
9 changes: 9 additions & 0 deletions lib/install/macos.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict';

exports.install = async function() {
throw new Error('macos install not implemented');
};

exports.uninstall = async function() {
throw new Error('macos uninstall not implemented');
};
26 changes: 26 additions & 0 deletions lib/install/win32.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'use strict';

const { exec: _exec } = require('child_process');
const { resolve } = require('path');
const { promisify } = require('util');

const exec = promisify(_exec);

const MAKE_VOICE_EXE = 'MakeVoice.exe';

exports.install = async function () {
await exec(`${MAKE_VOICE_EXE}`, await getExecOptions());
};

exports.uninstall = async function () {
await exec(`${MAKE_VOICE_EXE} /u`, await getExecOptions());
};

/**
* @returns {Promise<import('child_process').ExecOptions>}
*/
async function getExecOptions() {
return {
cwd: resolve(__dirname, '../../Release'),
};
}
2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
"at-driver": "./bin/at-driver"
},
"scripts": {
"postinstall": "Release\\MakeVoice.exe",
"postuninstall": "Release\\MakeVoice.exe /u",
"prettier": "prettier --write lib test",
"test": "prettier --check lib test && npm run test-types && npm run test-unit",
"test-unit": "mocha --ui tdd test/**/*.js",
Expand Down
16 changes: 8 additions & 8 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,37 +56,37 @@ suite('at-driver', () => {
});

test('WebSocket server on default port', async () => {
const { whenClosed } = await run([]);
const { whenClosed } = await run(['serve']);
return Promise.race([whenClosed, connect(4382)]);
});

test('WebSocket server on custom port', async () => {
const { whenClosed } = await run(['--port', '6543']);
const { whenClosed } = await run(['serve', '--port', '6543']);
return Promise.race([whenClosed, connect(6543)]);
});

test('rejects invalid port values: unspecified', async () => {
const { whenClosed } = await run(['--port']);
const { whenClosed } = await run(['serve', '--port']);
return invert(whenClosed);
});

test('rejects invalid port values: non-numeric', async () => {
const { whenClosed } = await run(['--port', 'seven']);
const { whenClosed } = await run(['serve', '--port', 'seven']);
return invert(whenClosed);
});

test('rejects invalid port values: negative', async () => {
const { whenClosed } = await run(['--port', '-8000']);
const { whenClosed } = await run(['serve', '--port', '-8000']);
return invert(whenClosed);
});

test('rejects invalid port values: non-integer', async () => {
const { whenClosed } = await run(['--port', '2004.3']);
const { whenClosed } = await run(['serve', '--port', '2004.3']);
return invert(whenClosed);
});

test('rejects invalid port values: non-decimal', async () => {
const { whenClosed } = await run(['--port', '0x1000']);
const { whenClosed } = await run(['serve', '--port', '0x1000']);
return invert(whenClosed);
});

Expand All @@ -106,7 +106,7 @@ suite('at-driver', () => {
};

setup(async () => {
({ whenClosed } = await run([]));
({ whenClosed } = await run(['serve']));

websocket = await Promise.race([whenClosed, connect(4382)]);
});
Expand Down