ASpellWorker.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. // TODO: This file was created by bulk-decaffeinate.
  2. // Sanity-check the conversion and remove this comment.
  3. /*
  4. * decaffeinate suggestions:
  5. * DS102: Remove unnecessary code created because of implicit returns
  6. * DS205: Consider reworking code to avoid use of IIFEs
  7. * DS207: Consider shorter variations of null checks
  8. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  9. */
  10. const childProcess = require('child_process')
  11. const logger = require('logger-sharelatex')
  12. const metrics = require('@overleaf/metrics')
  13. const _ = require('underscore')
  14. const OError = require('@overleaf/o-error')
  15. const BATCH_SIZE = 100
  16. class ASpellWorker {
  17. constructor(language) {
  18. this.language = language
  19. this.count = 0
  20. this.closeReason = ''
  21. this.pipe = childProcess.spawn('aspell', [
  22. 'pipe',
  23. '-t',
  24. '--encoding=utf-8',
  25. '-d',
  26. language
  27. ])
  28. logger.info(
  29. { process: this.pipe.pid, lang: this.language },
  30. 'starting new aspell worker'
  31. )
  32. metrics.inc('aspellWorker', 1, { status: 'start', method: this.language })
  33. this.pipe.on('exit', () => {
  34. this.state = 'killed'
  35. logger.info(
  36. { process: this.pipe.pid, lang: this.language },
  37. 'aspell worker has exited'
  38. )
  39. metrics.inc('aspellWorker', 1, {
  40. status: 'exit',
  41. method: this.language
  42. })
  43. })
  44. this.pipe.on('close', () => {
  45. const previousWorkerState = this.state
  46. if (this.state !== 'killed') {
  47. this.state = 'closed'
  48. }
  49. if (this.callback != null) {
  50. const err = new OError(
  51. 'aspell worker closed output streams with uncalled callback',
  52. {
  53. process: this.pipe.pid,
  54. lang: this.language,
  55. stdout: output.slice(-1024),
  56. stderr: error.slice(-1024),
  57. workerState: this.state,
  58. previousWorkerState,
  59. closeReason: this.closeReason
  60. }
  61. )
  62. this.callback(err, [])
  63. this.callback = null
  64. }
  65. })
  66. this.pipe.on('error', (err) => {
  67. const previousWorkerState = this.state
  68. if (this.state !== 'killed') {
  69. this.state = 'error'
  70. }
  71. OError.tag(err, 'aspell worker error', {
  72. process: this.pipe.pid,
  73. stdout: output.slice(-1024),
  74. stderr: error.slice(-1024),
  75. lang: this.language,
  76. workerState: this.state,
  77. previousWorkerState,
  78. closeReason: this.closeReason
  79. })
  80. if (this.callback != null) {
  81. this.callback(err, [])
  82. this.callback = null
  83. } else {
  84. logger.warn(err)
  85. }
  86. })
  87. this.pipe.stdin.on('error', (err) => {
  88. const previousWorkerState = this.state
  89. if (this.state !== 'killed') {
  90. this.state = 'error'
  91. }
  92. OError.tag(err, 'aspell worker error on stdin', {
  93. process: this.pipe.pid,
  94. stdout: output.slice(-1024),
  95. stderr: error.slice(-1024),
  96. lang: this.language,
  97. workerState: this.state,
  98. previousWorkerState,
  99. closeReason: this.closeReason
  100. })
  101. if (this.callback != null) {
  102. this.callback(err, [])
  103. this.callback = null
  104. } else {
  105. logger.warn(err)
  106. }
  107. })
  108. this.pipe.stdout.setEncoding('utf8') // ensure utf8 output is handled correctly
  109. var output = ''
  110. const endMarkerRegex = new RegExp('^[a-z][a-z]', 'gm')
  111. this.pipe.stdout.on('data', (data) => {
  112. // We receive the language code from Aspell as the end of data marker in
  113. // the data. The input is a utf8 encoded string.
  114. const oldPos = output.length
  115. output = output + data
  116. // The end marker may cross the end of a chunk, so we optimise the search
  117. // using the regex lastIndex property.
  118. endMarkerRegex.lastIndex = oldPos > 2 ? oldPos - 2 : 0
  119. if (endMarkerRegex.test(output)) {
  120. if (this.callback != null) {
  121. this.callback(null, output.slice())
  122. this.callback = null // only allow one callback in use
  123. } else {
  124. logger.err(
  125. new OError(
  126. 'end of data marker received when callback already used',
  127. {
  128. process: this.pipe.pid,
  129. lang: this.language,
  130. workerState: this.state
  131. }
  132. )
  133. )
  134. }
  135. this.state = 'ready'
  136. output = ''
  137. }
  138. })
  139. var error = ''
  140. this.pipe.stderr.on('data', (chunk) => {
  141. return (error = error + chunk)
  142. })
  143. this.pipe.stdout.on('end', () => {
  144. // process has ended
  145. return (this.state = 'end')
  146. })
  147. }
  148. isReady() {
  149. return this.state === 'ready'
  150. }
  151. check(words, callback) {
  152. // we will now send data to aspell, and be ready again when we
  153. // receive the end of data marker
  154. this.state = 'busy'
  155. if (this.callback != null) {
  156. // only allow one callback in use
  157. return this.callback(
  158. new OError('Aspell callback already in use - SHOULD NOT HAPPEN', {
  159. process: this.pipe.pid,
  160. lang: this.language,
  161. workerState: this.state
  162. })
  163. )
  164. }
  165. this.callback = _.once(callback) // extra defence against double callback
  166. this.setTerseMode()
  167. this.write(words)
  168. return this.flush()
  169. }
  170. write(words) {
  171. let i = 0
  172. return (() => {
  173. const result = []
  174. while (i < words.length) {
  175. // batch up the words to check for efficiency
  176. const batch = words.slice(i, i + BATCH_SIZE)
  177. this.sendWords(batch)
  178. result.push((i += BATCH_SIZE))
  179. }
  180. return result
  181. })()
  182. }
  183. flush() {
  184. // get aspell to send an end of data marker "*" when ready
  185. // @sendCommand("%") # take the aspell pipe out of terse mode so we can look for a '*'
  186. // @sendCommand("^ENDOFSTREAMMARKER") # send our marker which will generate a '*'
  187. // @sendCommand("!") # go back into terse mode
  188. return this.sendCommand('$$l')
  189. }
  190. shutdown(reason) {
  191. logger.info({ process: this.pipe.pid, reason }, 'shutting down')
  192. this.state = 'closing'
  193. this.closeReason = reason
  194. return this.pipe.stdin.end()
  195. }
  196. kill(reason) {
  197. logger.info({ process: this.pipe.pid, reason }, 'killing')
  198. this.closeReason = reason
  199. if (this.state === 'killed') {
  200. return
  201. }
  202. return this.pipe.kill('SIGKILL')
  203. }
  204. setTerseMode() {
  205. return this.sendCommand('!')
  206. }
  207. sendWord(word) {
  208. return this.sendCommand(`^${word}`)
  209. }
  210. sendWords(words) {
  211. // Aspell accepts multiple words to check on the same line
  212. // ^word1 word2 word3 ...
  213. // See aspell.info, writing programs to use Aspell Through A Pipe
  214. this.sendCommand(`^${words.join(' ')}`)
  215. return this.count++
  216. }
  217. sendCommand(command) {
  218. return this.pipe.stdin.write(command + '\n')
  219. }
  220. }
  221. module.exports = ASpellWorker