Skip to content

Commit

Permalink
Merge pull request #42 from w3c/install-commands
Browse files Browse the repository at this point in the history
Add install and uninstall subcommands
  • Loading branch information
jugglinmike authored Apr 1, 2024
2 parents 3bab7ad + 7434022 commit fde47d5
Show file tree
Hide file tree
Showing 9 changed files with 177 additions and 86 deletions.
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

0 comments on commit fde47d5

Please sign in to comment.