UNPKG

23.3 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const crypto = require("crypto");
4const _ = require("underscore");
5const os = require("os");
6const path = require("path");
7const request = require("request-promise");
8const docker_cli_js_1 = require("docker-cli-js");
9const cron_1 = require("cron");
10const uuid = require("uuid/v5");
11const sbase = require("@nodeswork/sbase");
12const logger = require("@nodeswork/logger");
13const utils_1 = require("@nodeswork/utils");
14const applet = require("@nodeswork/applet");
15const errors = require("./errors");
16const utils_2 = require("./utils");
17const server_1 = require("./server");
18const socket_1 = require("./socket");
19const paths_1 = require("./paths");
20const compareVersion = require("compare-version");
21const latestVersion = require('latest-version');
22const LOG = logger.getLogger();
23const APPLET_MANAGER_KEY = 'appletManager';
24const containerVersion = require('../package.json').version;
25const isRunning = require('is-running');
26const machineId = require('node-machine-id').machineIdSync;
27const UUID_NAMESPACE = '5daabcd8-f17e-568c-aa6f-da9d92c7032c';
28const NAME_REGEX = /^na-(\w+)-([0-9.]+)-([\w-]+)_([0-9.]+)-(\w+)$/;
29class AppletManager {
30 constructor(options) {
31 this.options = options;
32 this.docker = new docker_cli_js_1.Docker();
33 this.cronJobs = [];
34 if (this.options.debug) {
35 LOG.level = 'debug';
36 }
37 const configPath = path.join(options.appPath, 'config.json');
38 LOG.debug('Load configuration from configPath', configPath);
39 this.ls = utils_2.localStorage(configPath);
40 let amOptions = this.ls.getItemSync(APPLET_MANAGER_KEY);
41 let running = false;
42 if (amOptions == null) {
43 amOptions = this.options;
44 LOG.debug('Initialize Applet Manager Options to local:', amOptions);
45 this.ls.setItemSync(APPLET_MANAGER_KEY, amOptions);
46 }
47 else {
48 LOG.debug('Got Applet Manager Options from local:', amOptions);
49 if (amOptions.pid) {
50 running = isRunning(amOptions.pid);
51 }
52 this.options.token = amOptions.token;
53 }
54 if (running && (this.options.appPath !== amOptions.appPath ||
55 this.options.nodesworkServer !== amOptions.nodesworkServer ||
56 this.options.port !== amOptions.port)) {
57 throw new utils_1.NodesworkError('Configuration does not match with existing Applet Manager', {
58 newOption: this.options,
59 runningOption: _.pick(amOptions, 'appPath', 'nodesworkServer', 'port'),
60 });
61 }
62 if (running) {
63 this.options.pid = amOptions.pid;
64 this.options.token = amOptions.token;
65 }
66 else {
67 this.options.pid = null;
68 }
69 }
70 authenticated() {
71 return this.options.token != null;
72 }
73 /**
74 * Authenticate the container by email and password.
75 *
76 * @throws UNAUTHENTICATED_ERROR
77 */
78 async authenticate(options) {
79 try {
80 const resp = await request.post({
81 baseUrl: this.options.nodesworkServer,
82 uri: '/v1/u/user/login',
83 body: {
84 email: options.email,
85 password: options.password,
86 },
87 json: true,
88 jar: true,
89 });
90 LOG.debug('Login successfully');
91 }
92 catch (e) {
93 if (e.name === 'RequestError') {
94 throw new utils_1.NodesworkError('Server is not available', {
95 path: this.options.nodesworkServer + '/v1/u/user/login',
96 }, e);
97 }
98 else if (e.statusCode === 401) {
99 throw new utils_1.NodesworkError('Wrong password');
100 }
101 else if (e.statusCode === 422) {
102 throw new utils_1.NodesworkError('Wrong email');
103 }
104 else {
105 throw e;
106 }
107 }
108 try {
109 const mid = machineId();
110 LOG.debug('Got machine id:', mid);
111 const deviceIdentifier = crypto.createHash('md5')
112 .update(mid)
113 .update(options.email)
114 .digest('hex');
115 let operatingSystem = {
116 Darwin: 'MacOS',
117 Windows_NT: 'Windows',
118 Linux: 'Linux',
119 }[os.type()];
120 const device = {
121 deviceType: 'UserDevice',
122 deviceIdentifier,
123 os: operatingSystem,
124 osVersion: os.release(),
125 containerVersion,
126 name: options.deviceName,
127 };
128 LOG.debug('Collected device information:', device);
129 const resp = await request.post({
130 baseUrl: this.options.nodesworkServer,
131 uri: '/v1/u/devices',
132 body: device,
133 json: true,
134 jar: true,
135 });
136 LOG.debug('Device registered successfully', resp);
137 this.options.token = resp.token;
138 this.ls.setItemSync(APPLET_MANAGER_KEY, this.options);
139 LOG.debug('Save token to local', this.options);
140 await this.updateDevice();
141 }
142 catch (e) {
143 if (e.name === 'RequestError') {
144 throw new utils_1.NodesworkError('Server is not available', {
145 path: this.options.nodesworkServer + '/v1/u/user/login',
146 }, e);
147 }
148 else {
149 throw e;
150 }
151 }
152 return;
153 }
154 isStarted() {
155 return this.options.pid != null;
156 }
157 /**
158 * Start the container.
159 *
160 * @throws UNAUTHENTICATED_ERROR
161 */
162 async startServer() {
163 if (this.options.pid != null) {
164 console.log('daemon has already started');
165 return;
166 }
167 if (this.options.token == null) {
168 throw errors.UNAUTHENTICATED_ERROR;
169 }
170 await this.checkEnvironment();
171 // Start the applet manager.
172 this.options.pid = process.pid;
173 this.ls.setItemSync(APPLET_MANAGER_KEY, this.options);
174 server_1.app.appletManager = this;
175 server_1.server.listen(this.options.port);
176 socket_1.connectSocket(this.options.nodesworkServer, this.options.token, this);
177 LOG.info(`Server is started at http://localhost:${this.options.port}`);
178 await this.updateDevice();
179 }
180 /**
181 * Stop the container.
182 *
183 * @throws UNAUTHENTICATED_ERROR
184 */
185 async stopServer() {
186 // Stop the applet manager.
187 if (this.options.pid) {
188 process.kill(this.options.pid);
189 this.options.pid = null;
190 await utils_2.sleep(1000);
191 }
192 }
193 async install(options) {
194 const docker = new docker_cli_js_1.Docker();
195 const cmd = `build -t ${imageName(options)} --build-arg package=${options.packageName} --build-arg version=${options.version} docker/${options.naType}/${options.naVersion}`;
196 LOG.debug('Execute command to install applet', { cmd });
197 try {
198 const result = await docker.command(cmd);
199 LOG.debug('Execute build command log', result);
200 await this.updateDevice();
201 }
202 catch (e) {
203 throw e;
204 }
205 }
206 async images() {
207 const docker = new docker_cli_js_1.Docker();
208 const images = await docker.command('images');
209 return _.chain(images.images)
210 .filter((image) => {
211 const [na, naType, naVersion, ...others] = image.repository.split('-');
212 return na === 'na' && (naType === 'npm') && (naVersion === '8.3.0');
213 })
214 .map((image) => {
215 const [na, naType, naVersion, ...others] = image.repository.split('-');
216 return {
217 naType,
218 naVersion,
219 packageName: others.join('-'),
220 version: image.tag,
221 };
222 })
223 .value();
224 }
225 async run(options) {
226 await this.checkEnvironment();
227 const uniqueName = this.name(options);
228 const rmCmd = `rm ${uniqueName}`;
229 LOG.debug('Execute command to rm applet', { cmd: rmCmd });
230 try {
231 const docker = new docker_cli_js_1.Docker();
232 const result = await docker.command(rmCmd);
233 LOG.debug('Execute build command log', result);
234 }
235 catch (e) {
236 LOG.debug('Container does not exist');
237 }
238 const image = imageName(options);
239 const cmd = `run --name ${uniqueName} --network nodeswork -d -e ${applet.constants.environmentKeys.APPLET_ID}=${options.appletId} -e ${applet.constants.environmentKeys.APPLET_TOKEN}=${options.appletToken} ${image}`;
240 LOG.debug('Execute command to run applet', { cmd });
241 try {
242 const docker = new docker_cli_js_1.Docker();
243 const result = await docker.command(cmd);
244 LOG.debug('Execute run command result', result);
245 await this.updateDevice();
246 }
247 catch (e) {
248 LOG.error('Execute run command failed', e);
249 throw e;
250 }
251 }
252 async kill(options) {
253 const uniqueName = `na-npm-${options.packageName}_${options.version}`;
254 const cmd = `stop ${uniqueName}`;
255 LOG.debug('Execute command to run applet', { cmd });
256 try {
257 const docker = new docker_cli_js_1.Docker();
258 const result = await docker.command(cmd);
259 LOG.debug('Execute build command log', result);
260 await this.updateDevice();
261 }
262 catch (e) {
263 throw e;
264 }
265 }
266 async ps() {
267 await this.checkEnvironment();
268 const psResult = await this.docker.command('ps');
269 const psApplets = _.chain(psResult.containerList)
270 .map((container) => {
271 const image = parseAppletImage(container.names);
272 if (image == null) {
273 return null;
274 }
275 const port = parseMappingPort(container.ports) || 28900;
276 return _.extend(image, { port, status: container.status });
277 })
278 .filter(_.identity)
279 .value();
280 const networkResults = Object.values((await this.docker.command('network inspect nodeswork')).object[0].Containers);
281 return _.filter(psApplets, (psApplet) => {
282 const appletName = this.name(psApplet);
283 const networkResult = _.find(networkResults, (result) => {
284 return result.Name === appletName;
285 });
286 if (networkResult == null) {
287 LOG.warn(`Applet ${appletName} is running but not in the correct network`);
288 return false;
289 }
290 psApplet.ip = networkResult.IPv4Address.split('/')[0];
291 return true;
292 });
293 }
294 async refreshWorkerCrons() {
295 const self = this;
296 try {
297 const userApplets = await request.get({
298 headers: {
299 'device-token': this.options.token,
300 },
301 baseUrl: this.options.nodesworkServer,
302 uri: '/v1/d/user-applets',
303 json: true,
304 jar: true,
305 });
306 const newJobs = _
307 .chain(userApplets)
308 .map((ua) => {
309 const appletConfig = ua.config.appletConfig;
310 const image = {
311 naType: appletConfig.naType,
312 naVersion: appletConfig.naVersion,
313 packageName: appletConfig.packageName,
314 version: appletConfig.version,
315 };
316 return _.map(appletConfig.workers, (workerConfig) => {
317 const worker = {
318 handler: workerConfig.handler,
319 name: workerConfig.name,
320 };
321 return {
322 jobUUID: uuid([
323 ua.applet._id,
324 ua._id,
325 image.naType,
326 image.naVersion,
327 image.packageName,
328 image.version,
329 worker.handler,
330 worker.name,
331 ].join(':'), UUID_NAMESPACE),
332 appletId: ua.applet._id,
333 userApplet: ua._id,
334 image,
335 worker,
336 schedule: workerConfig.schedule,
337 };
338 });
339 })
340 .flatten()
341 .filter((x) => x.schedule != null)
342 .value();
343 LOG.debug('Fetch applets for current device successfully');
344 for (const cron of this.cronJobs) {
345 const u = _.find(newJobs, (newJob) => newJob.jobUUID === cron.jobUUID);
346 if (u == null) {
347 cron.cronJob.stop();
348 LOG.info('Stop cron job successfully', _.omit(cron, 'cronJob'));
349 }
350 }
351 for (const newJob of newJobs) {
352 const cron = _.find(this.cronJobs, (c) => newJob.jobUUID === c.jobUUID);
353 if (cron == null) {
354 const cronJob = (function (c) {
355 try {
356 c.cronJob = new cron_1.CronJob({
357 cronTime: c.schedule,
358 onTick: async () => {
359 LOG.debug('Run cron job', _.omit(c, 'cronJob'));
360 try {
361 await self.executeCronJob(c);
362 LOG.info('Run cron job successfully', _.omit(c, 'cronJob'));
363 }
364 catch (e) {
365 LOG.error('Run cron job failed', e, _.omit(c, 'cronJob'));
366 }
367 },
368 start: true,
369 });
370 }
371 catch (e) {
372 LOG.error('Create cron job failed', _.omit(c, 'cronJob'));
373 }
374 LOG.info('Create cron job successfully', _.omit(c, 'cronJob'));
375 return c;
376 })(newJob);
377 this.cronJobs.push(cronJob);
378 }
379 }
380 }
381 catch (e) {
382 throw e;
383 }
384 }
385 async executeCronJob(job) {
386 try {
387 const accounts = await request.get({
388 headers: {
389 'device-token': this.options.token,
390 },
391 baseUrl: this.options.nodesworkServer,
392 uri: `/v1/d/user-applets/${job.userApplet}/accounts`,
393 json: true,
394 jar: true,
395 });
396 LOG.debug('Fetch accounts successfully', accounts);
397 const payload = {
398 accounts,
399 };
400 const result = await this.work({
401 route: {
402 appletId: job.appletId,
403 naType: job.image.naType,
404 naVersion: job.image.naVersion,
405 packageName: job.image.packageName,
406 version: job.image.version,
407 },
408 worker: job.worker,
409 payload,
410 });
411 LOG.info('Execute cron job successfully.', {
412 job: _.omit(job, 'cronJob'),
413 result,
414 });
415 }
416 catch (e) {
417 throw e;
418 }
419 }
420 async work(options) {
421 LOG.debug('Get work request', options);
422 const requestOptions = {
423 appletId: options.route.appletId,
424 naType: options.route.naType,
425 naVersion: options.route.naVersion,
426 packageName: options.route.packageName,
427 version: options.route.version,
428 uri: `/workers/${options.worker.handler}/${options.worker.name}`,
429 method: 'POST',
430 body: options.payload,
431 };
432 return await this.request(requestOptions);
433 }
434 async request(options) {
435 LOG.info('Get request', { options });
436 const routeAddress = await this.route(options);
437 if (routeAddress == null) {
438 throw new utils_1.NodesworkError('Applet is not running');
439 }
440 const headers = _.extend({}, options.headers);
441 headers[sbase.constants.headers.request.NODESWORK_FORWARDED_TO] = (routeAddress.route);
442 const requestOptions = {
443 uri: routeAddress.target + options.uri,
444 method: options.method,
445 proxy: routeAddress.target,
446 body: options.body,
447 headers,
448 json: true,
449 };
450 LOG.debug('Request options', requestOptions);
451 const resp = await request(requestOptions);
452 LOG.debug('Request response', resp);
453 return resp;
454 }
455 async operateAccount(options) {
456 const requestOptions = {
457 uri: `/v1/d/applets/${options.appletId}/accounts/${options.accountId}/operate`,
458 baseUrl: this.options.nodesworkServer,
459 body: options.body,
460 headers: {
461 'device-token': this.options.token,
462 },
463 json: true,
464 jar: true,
465 };
466 return await request.post(requestOptions);
467 }
468 async route(options) {
469 if (this.options.dev) {
470 try {
471 const devServer = await request({
472 uri: 'http://localhost:28900/sstats',
473 json: true,
474 });
475 if (devServer.applet &&
476 devServer.applet.packageName === options.packageName &&
477 compareVersion(devServer.applet.packageVersion, options.version) >= 0) {
478 return {
479 route: 'localhost:28900',
480 target: 'http://localhost:28900',
481 };
482 }
483 }
484 catch (e) {
485 // Fallback
486 }
487 }
488 return {
489 route: `${this.name(options)}:28900`,
490 target: paths_1.containerProxyUrl,
491 };
492 }
493 async updateDevice() {
494 if (this.options.token == null) {
495 throw errors.UNAUTHENTICATED_ERROR;
496 }
497 const installedApplets = await this.images();
498 const runningApplets = await this.ps();
499 try {
500 const resp = await request.post({
501 headers: {
502 'device-token': this.options.token,
503 },
504 baseUrl: this.options.nodesworkServer,
505 uri: '/v1/d/devices',
506 body: {
507 installedApplets,
508 runningApplets,
509 },
510 json: true,
511 jar: true,
512 });
513 LOG.debug('Update device successfully');
514 }
515 catch (e) {
516 throw e;
517 }
518 await this.refreshWorkerCrons();
519 }
520 async checkEnvironment() {
521 // Step 1: Check network configuration
522 const networks = await this.docker.command('network ls');
523 const targetNetwork = _.find(networks.network, (c) => c.name === 'nodeswork');
524 if (targetNetwork == null) {
525 LOG.debug('network is not setup, creating');
526 await this.docker.command('network create nodeswork');
527 }
528 const inspect = await this.docker.command('network inspect nodeswork');
529 LOG.debug('inspecting network', inspect.object[0]);
530 const IPAMConfig = inspect.object[0].IPAM.Config[0];
531 this.network = {
532 subnet: IPAMConfig.Subnet,
533 gateway: IPAMConfig.Gateway,
534 containers: inspect.object[0].Containers,
535 };
536 LOG.debug('Network configuration', this.network);
537 // Step 2: Check pre installed containers
538 // Step 2.1: Check proxy container
539 const containers = await this.docker.command('ps');
540 const proxyContainer = _.find(containers.containerList, (container) => container.names === 'nodeswork-container-proxy');
541 if (proxyContainer == null) {
542 LOG.debug('Container proxy is not running, starting');
543 await this.installContainerProxy();
544 }
545 else {
546 const version = proxyContainer.image.split(':')[1];
547 const lVersion = await latestVersion('@nodeswork/container-proxy');
548 this.containerProxy = {
549 version,
550 latestVersion: lVersion,
551 };
552 }
553 const proxyInNetwork = _.find(this.network.containers, (container) => {
554 return container.Name === 'nodeswork-container-proxy';
555 });
556 if (proxyInNetwork == null) {
557 LOG.debug('Proxy container is not in network');
558 await this.docker.command(`network connect nodeswork nodeswork-container-proxy`);
559 }
560 LOG.debug('Container Proxy configuration', this.containerProxy);
561 LOG.debug('Environment setup correctly');
562 }
563 name(options) {
564 return `na-${options.naType}-${options.naVersion}-${options.packageName}_${options.version}-${options.appletId}`;
565 }
566 async installContainerProxy() {
567 const version = await latestVersion('@nodeswork/container-proxy');
568 LOG.debug('Fetched latest version container-proxy', { version });
569 const output = await this.docker.command(`build -t nodeswork-container-proxy:${version} docker/container-proxy --build-arg version=${version}`);
570 LOG.debug('Building container proxy', output);
571 try {
572 await this.docker.command(`rm nodeswork-container-proxy`);
573 }
574 catch (e) {
575 LOG.debug('Remove container proxy error', e);
576 }
577 try {
578 // sudo ifconfig lo0 alias 172.16.222.111
579 await this.docker.command(`run --name nodeswork-container-proxy -d -e NAM_HOST=172.16.222.111:28310 -e SUB_NET=${this.network.subnet} -p 28320:28320 nodeswork-container-proxy:${version}`);
580 }
581 catch (e) {
582 LOG.debug('Remove container proxy error', e);
583 }
584 this.containerProxy = { version, latestVersion: version };
585 }
586}
587exports.AppletManager = AppletManager;
588function imageName(image) {
589 return `na-${image.naType}-${image.naVersion}-${image.packageName}\
590:${image.version}`;
591}
592function parseAppletImage(name) {
593 const result = NAME_REGEX.exec(name);
594 if (result == null) {
595 return null;
596 }
597 return {
598 naType: result[1],
599 naVersion: result[2],
600 packageName: result[3],
601 version: result[4],
602 appletId: result[5],
603 };
604}
605function parseMappingPort(ports) {
606 return parseInt(ports.split(':')[1]);
607}
608
609//# sourceMappingURL=applet-manager.js.map