|
@@ -1,6 +1,20 @@
|
|
|
-/** @module interfaces */
|
|
|
+/**
|
|
|
+ * @fileOverview INTERFACE BACKEND
|
|
|
+ * @author <a href="tcve@u-blox.com">Tomi Cvetic</a>
|
|
|
+ *
|
|
|
+ * Communication with workers (for now only Python)
|
|
|
+ * The workers are implemented as REPL clients that
|
|
|
+ * communicate via JSON messages through STDIO.
|
|
|
+ *
|
|
|
+ * Convention:
|
|
|
+ * - Interface: Communication interface to instruments or DUTs (e.g. serial)
|
|
|
+ * - Worker: Python program that implements a REPL client (e.g. serial_worker.py)
|
|
|
+ * - Port: Port of an interface (e.g. COM1)
|
|
|
+ * - Connection: Process running the Python worker.
|
|
|
+ */
|
|
|
|
|
|
-let fs = require('fs')
|
|
|
+// Module imports
|
|
|
+const fs = require('fs')
|
|
|
const os = require('os')
|
|
|
const md5 = require('md5')
|
|
|
const { promisify } = require('util')
|
|
@@ -9,28 +23,23 @@ const PythonWorker = require('./pythonWorker')
|
|
|
const readdir = promisify(fs.readdir)
|
|
|
const stat = promisify(fs.stat)
|
|
|
|
|
|
-/**
|
|
|
- * INTERFACE BACKEND
|
|
|
- *
|
|
|
- * Communication with workers (for now only Python)
|
|
|
- * The workers are implemented as REPL clients that
|
|
|
- * communicate with JSON.
|
|
|
- *
|
|
|
- * Names:
|
|
|
- * * Worker: a Python file that implements a REPL client (e.g. serial)
|
|
|
- * * Port:
|
|
|
- * * Connection:
|
|
|
- */
|
|
|
-
|
|
|
-/** Directory with the Python worker scripts */
|
|
|
+/** @type {string} Directory with worker files */
|
|
|
const WORKER_DIR = `${__dirname}/python_workers`
|
|
|
|
|
|
-/** Hostname is used to identify ports across machines */
|
|
|
+/** @type {string} Hostname is used to identify ports across machines */
|
|
|
const HOST = os.hostname()
|
|
|
|
|
|
-/** Local state for the interface module */
|
|
|
+/**
|
|
|
+ * Local state for the interface module
|
|
|
+ * @typedef {object} State
|
|
|
+ * @property {Interface[]} interfaces - Array of found interfaces
|
|
|
+ * @property {Port[]} ports - Array of found ports
|
|
|
+ * @property {Connection[]} connections - Array of established connections
|
|
|
+ * @property {object} lastScan - Time of the last scan (per property)
|
|
|
+ */
|
|
|
+/** @type {State} Local state of the modules. */
|
|
|
const state = {
|
|
|
- interfaces: [],
|
|
|
+ interfaces: [], // @type {array}
|
|
|
ports: [],
|
|
|
connections: [],
|
|
|
lastScan: {
|
|
@@ -40,7 +49,9 @@ const state = {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-/** GraphQL types for the interface module */
|
|
|
+async function setState (content) {}
|
|
|
+
|
|
|
+/** @type {string} GraphQL types for the interface module */
|
|
|
const typeDefs = `
|
|
|
type Interface {
|
|
|
id: ID!
|
|
@@ -111,6 +122,7 @@ const typeDefs = `
|
|
|
sendCommand(connectionId: ID!, command: ConnectionCommand!): String!
|
|
|
}
|
|
|
`
|
|
|
+
|
|
|
/**
|
|
|
* Find worker files in a directory based on filename convention
|
|
|
* (ending in _worker.py).
|
|
@@ -127,11 +139,15 @@ async function findWorkers (directory) {
|
|
|
|
|
|
/**
|
|
|
* Fetch options from Python worker process.
|
|
|
- * @param {Object} workerProcess - Worker process to use for fetching the options
|
|
|
+ * @typedef {object} workerProcess
|
|
|
+ * @param {workerProcess} workerProcess - Worker process to use for fetching the options
|
|
|
+ * @return {array} Option array
|
|
|
*/
|
|
|
async function getOptions (workerProcess) {
|
|
|
// Check that workerProcess has a send method.
|
|
|
- if (!workerProcess || !workerProcess.send) throw new Error('workerProcess not configured properly.')
|
|
|
+ if (!workerProcess || !workerProcess.send) {
|
|
|
+ throw new Error('workerProcess not configured properly.')
|
|
|
+ }
|
|
|
const { data, error } = await workerProcess.send({ type: 'options' })
|
|
|
if (error) throw new Error(error)
|
|
|
return data
|
|
@@ -143,8 +159,10 @@ async function getOptions (workerProcess) {
|
|
|
* process.
|
|
|
* @param {string} directory - Python worker directory
|
|
|
* @param {string} workerFile - Python worker file
|
|
|
+ * @param {Object} state - State with already connected interfaces
|
|
|
+ * @return {Object} Updated state
|
|
|
*/
|
|
|
-async function createInterface (directory, workerFile, state) {
|
|
|
+async function createInterface (directory, workerFile, interfaces) {
|
|
|
// Assert that arguments are strings.
|
|
|
if (!(typeof directory === 'string')) {
|
|
|
throw new Error('Directory argument must be a string.')
|
|
@@ -158,16 +176,25 @@ async function createInterface (directory, workerFile, state) {
|
|
|
const { mtime } = await stat(path)
|
|
|
const workerScript = { path, mtime, updated: null }
|
|
|
// 2. Check if the interface already exists
|
|
|
- console.log('bugu', state)
|
|
|
- const foundInterface = state.interfaces.find(
|
|
|
+ const ifaceIndex = interfaces.findIndex(
|
|
|
iface => iface.interfaceName === interfaceName
|
|
|
)
|
|
|
- if (foundInterface) {
|
|
|
+ if (ifaceIndex >= 0) {
|
|
|
// b. If it was modified, save the modification time.
|
|
|
- if (foundInterface.workerScript.mtime < mtime) {
|
|
|
- foundInterface.workerScript.updated = mtime
|
|
|
+ if (interfaces[ifaceIndex].workerScript.mtime < mtime) {
|
|
|
+ const iface = interfaces[ifaceIndex]
|
|
|
+ const newWorkerScript = { ...iface.workerScript, updated: mtime }
|
|
|
+ return [
|
|
|
+ ...interfaces.slice(0, ifaceIndex),
|
|
|
+ {
|
|
|
+ ...iface,
|
|
|
+ workerScript: newWorkerScript
|
|
|
+ },
|
|
|
+ ...interfaces.slice(ifaceIndex + 1)
|
|
|
+ ]
|
|
|
+ } else {
|
|
|
+ return interfaces
|
|
|
}
|
|
|
- return
|
|
|
}
|
|
|
// c. Spawn a new worker connection.
|
|
|
const workerProcess = new PythonWorker(workerScript)
|
|
@@ -216,13 +243,21 @@ async function iface (parent, { id }, context, info) {
|
|
|
if (!iface) {
|
|
|
throw new Error(`Worker id=${id}, interfaceName=${interfaceName} not found`)
|
|
|
}
|
|
|
- return {
|
|
|
- ...iface,
|
|
|
- workerProcess
|
|
|
- }
|
|
|
+ return iface
|
|
|
}
|
|
|
|
|
|
-function workerProcess (parent, args, context, info) {
|
|
|
+/**
|
|
|
+ * @typedef {object} workerProcess
|
|
|
+ * @property {string} pid - OS process ID
|
|
|
+ *
|
|
|
+ * Serialize a worker process for GraphQL output
|
|
|
+ * @param {*} parent
|
|
|
+ * @param {*} args
|
|
|
+ * @param {*} context
|
|
|
+ * @param {*} info
|
|
|
+ * @return {workerProcess} Serialized worker process
|
|
|
+ */
|
|
|
+function serializeWorkerProcess (parent, args, context, info) {
|
|
|
const {
|
|
|
killed,
|
|
|
exitCode,
|
|
@@ -399,10 +434,10 @@ const resolvers = {
|
|
|
Interface: {
|
|
|
ports,
|
|
|
connections,
|
|
|
- workerProcess
|
|
|
+ serializeWorkerProcess
|
|
|
},
|
|
|
Connection: {
|
|
|
- workerProcess
|
|
|
+ serializeWorkerProcess
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -410,7 +445,7 @@ const __test__ = {
|
|
|
findWorkers,
|
|
|
createInterface,
|
|
|
getOptions,
|
|
|
- workerProcess,
|
|
|
+ serializeWorkerProcess,
|
|
|
state
|
|
|
}
|
|
|
|