UNPKG

28.3 kBJavaScriptView Raw
1'use strict';
2
3const ip = require('ip');
4const fs = require('fs-extra');
5const tar = require('tar-fs');
6const nas = require('./nas');
7const path = require('path');
8const debug = require('debug')('fun:local');
9const Docker = require('dockerode');
10const docker = new Docker();
11const dockerOpts = require('./docker-opts');
12const getVisitor = require('./visitor').getVisitor;
13const getProfile = require('./profile').getProfile;
14const { generatePwdFile } = require('./utils/passwd');
15
16const { blue, red, yellow } = require('colors');
17const { getRootBaseDir } = require('./tpl');
18const { parseArgsStringToArgv } = require('string-argv');
19const { extractNasMappingsFromNasYml } = require('./nas/support');
20const { addEnv, addInstallTargetEnv, resolveLibPathsFromLdConf } = require('./install/env');
21const { findPathsOutofSharedPaths } = require('./docker-support');
22const { processorTransformFactory } = require('./error-processor');
23
24const isWin = process.platform === 'win32';
25
26const _ = require('lodash');
27
28require('draftlog').into(console);
29
30var containers = new Set();
31
32const devnull = require('dev-null');
33
34// exit container, when use ctrl + c
35function waitingForContainerStopped() {
36 // see https://stackoverflow.com/questions/10021373/what-is-the-windows-equivalent-of-process-onsigint-in-node-js
37 const isRaw = process.isRaw;
38 const kpCallBack = (_char, key) => {
39 if (key & key.ctrl && key.name === 'c') {
40 process.emit('SIGINT');
41 }
42 };
43 if (process.platform === 'win32') {
44 if (process.stdin.isTTY) {
45 process.stdin.setRawMode(isRaw);
46 }
47 process.stdin.on('keypress', kpCallBack);
48 }
49
50 let stopping = false;
51
52 process.on('SIGINT', async () => {
53
54 debug('containers length: ', containers.length);
55
56 if (stopping) {
57 return;
58 }
59
60 // Just fix test on windows
61 // Because process.emit('SIGINT') in test/docker.test.js will not trigger rl.on('SIGINT')
62 // And when listening to stdin the process never finishes until you send a SIGINT signal explicitly.
63 process.stdin.destroy();
64
65 if (!containers.size) {
66 return;
67 }
68
69 stopping = true;
70
71 console.log(`\nreceived canncel request, stopping running containers.....`);
72
73 const jobs = [];
74
75 for (let container of containers) {
76 try {
77 if (container.destroy) { // container stream
78 container.destroy();
79 } else {
80 const c = docker.getContainer(container);
81 console.log(`stopping container ${container}`);
82
83 jobs.push(c.kill().catch(ex => debug('kill container instance error, error is', ex)));
84 }
85 } catch (error) {
86 debug('get container instance error, ignore container to stop, error is', error);
87 }
88 }
89
90 try {
91 await Promise.all(jobs);
92 console.log('all containers stopped');
93 } catch (error) {
94 console.error(error);
95 process.exit(-1); // eslint-disable-line
96 }
97 });
98
99 return () => {
100 process.stdin.removeListener('keypress', kpCallBack);
101 if (process.stdin.isTTY) {
102 process.stdin.setRawMode(isRaw);
103 }
104 };
105}
106
107const goThrough = waitingForContainerStopped();
108
109const {
110 generateVscodeDebugConfig, generateDebugEnv
111} = require('./debug');
112
113// todo: add options for pull latest image
114const skipPullImage = true;
115
116async function resolveNasConfigToMounts(baseDir, serviceName, nasConfig, nasBaseDir) {
117 const nasMappings = await nas.convertNasConfigToNasMappings(nasBaseDir, nasConfig, serviceName);
118 return convertNasMappingsToMounts(getRootBaseDir(baseDir), nasMappings);
119}
120
121async function resolveTmpDirToMount(absTmpDir) {
122 if (!absTmpDir) { return {}; }
123 return {
124 Type: 'bind',
125 Source: absTmpDir,
126 Target: '/tmp',
127 ReadOnly: false
128 };
129}
130
131async function resolveDebuggerPathToMount(debuggerPath) {
132 if (!debuggerPath) { return {}; }
133 const absDebuggerPath = path.resolve(debuggerPath);
134 return {
135 Type: 'bind',
136 Source: absDebuggerPath,
137 Target: '/tmp/debugger_files',
138 ReadOnly: false
139 };
140}
141
142// todo: 当前只支持目录以及 jar。code uri 还可能是 oss 地址、目录、jar、zip?
143async function resolveCodeUriToMount(absCodeUri, readOnly = true) {
144 let target = null;
145
146 const stats = await fs.lstat(absCodeUri);
147
148 if (stats.isDirectory()) {
149 target = '/code';
150 } else {
151 // could not use path.join('/code', xxx)
152 // in windows, it will be translate to \code\xxx, and will not be recorgnized as a valid path in linux container
153 target = path.posix.join('/code', path.basename(absCodeUri));
154 }
155
156 // Mount the code directory as read only
157 return {
158 Type: 'bind',
159 Source: absCodeUri,
160 Target: target,
161 ReadOnly: readOnly
162 };
163}
164
165async function resolvePasswdMount() {
166 if (process.platform === 'linux') {
167 return {
168 Type: 'bind',
169 Source: await generatePwdFile(),
170 Target: '/etc/passwd',
171 ReadOnly: true
172 };
173 }
174
175 return null;
176}
177
178function convertNasMappingsToMounts(baseDir, nasMappings) {
179 return nasMappings.map(nasMapping => {
180 console.log('mounting local nas mock dir %s into container %s\n', nasMapping.localNasDir, nasMapping.remoteNasDir);
181 return {
182 Type: 'bind',
183 Source: path.resolve(baseDir, nasMapping.localNasDir),
184 Target: nasMapping.remoteNasDir,
185 ReadOnly: false
186 };
187 });
188}
189
190async function resolveNasYmlToMount(baseDir, serviceName) {
191 const nasMappings = await extractNasMappingsFromNasYml(baseDir, serviceName);
192 return convertNasMappingsToMounts(getRootBaseDir(baseDir), nasMappings);
193}
194
195function conventInstallTargetsToMounts(installTargets) {
196
197 if (!installTargets) { return []; }
198
199 const mounts = [];
200
201 _.forEach(installTargets, (target) => {
202 const { hostPath, containerPath } = target;
203
204 if (!(fs.pathExistsSync(hostPath))) {
205 fs.ensureDirSync(hostPath);
206 }
207
208 mounts.push({
209 Type: 'bind',
210 Source: hostPath,
211 Target: containerPath,
212 ReadOnly: false
213 });
214 });
215
216 return mounts;
217}
218
219async function imageExist(imageName) {
220
221 const images = await docker.listImages({
222 filters: {
223 reference: [imageName]
224 }
225 });
226
227 return images.length > 0;
228}
229
230async function listContainers(options) {
231 return await docker.listContainers(options);
232}
233
234async function getContainer(containerId) {
235 return await docker.getContainer(containerId);
236}
237
238async function renameContainer(container, name) {
239 return await container.rename({
240 name
241 });
242}
243
244// dockerode exec 在 windows 上有问题,用 exec 的 stdin 传递事件,当调用 stream.end() 时,会直接导致 exec 退出,且 ExitCode 为 null
245function generateDockerCmd(functionProps, httpMode, invokeInitializer = true, event = null) {
246 const cmd = ['-h', functionProps.Handler];
247
248 // 如果提供了 event
249 if (event !== null) {
250 cmd.push('--event', Buffer.from(event).toString('base64'));
251 cmd.push('--event-decode');
252 } else {
253 // always pass event using stdin mode
254 cmd.push('--stdin');
255 }
256
257 if (httpMode) {
258 cmd.push('--http');
259 }
260
261 const initializer = functionProps.Initializer;
262
263 if (initializer && invokeInitializer) {
264 cmd.push('-i', initializer);
265 }
266
267 const initializationTimeout = functionProps.InitializationTimeout;
268
269 // initializationTimeout is defined as integer, see lib/validate/schema/function.js
270 if (initializationTimeout) {
271 cmd.push('--initializationTimeout', initializationTimeout.toString());
272 }
273
274 debug(`docker cmd: ${cmd}`);
275
276 return cmd;
277}
278
279
280function followProgress(stream, onFinished) {
281
282 const barLines = {};
283
284 const onProgress = (event) => {
285 let status = event.status;
286
287 if (event.progress) {
288 status = `${event.status} ${event.progress}`;
289 }
290
291 if (event.id) {
292 const id = event.id;
293
294 if (!barLines[id]) {
295 barLines[id] = console.draft();
296 }
297 barLines[id](id + ': ' + status);
298 } else {
299 if (_.has(event, 'aux.ID')) {
300 event.stream = event.aux.ID + '\n';
301 }
302 // If there is no id, the line should be wrapped manually.
303 const out = event.status ? event.status + '\n' : event.stream;
304 process.stdout.write(out);
305 }
306 };
307
308 docker.modem.followProgress(stream, onFinished, onProgress);
309}
310
311async function pullImage(imageName) {
312
313 const resolveImageName = await dockerOpts.resolveImageNameForPull(imageName);
314
315 // copied from lib/edge/container.js
316 const startTime = new Date();
317
318 const stream = await docker.pull(resolveImageName);
319
320 const visitor = await getVisitor();
321
322 visitor.event({
323 ec: 'image',
324 ea: 'pull',
325 el: 'start'
326 }).send();
327
328 const registry = await dockerOpts.resolveDockerRegistry();
329
330 return new Promise((resolve, reject) => {
331
332 console.log(`begin pulling image ${resolveImageName}, you can also use ` + yellow(`'docker pull ${resolveImageName}'`) + ' to pull image by yourself.');
333
334 const onFinished = async (err) => {
335
336 containers.delete(stream);
337
338 const pullDuration = parseInt((new Date() - startTime) / 1000);
339 if (err) {
340 visitor.event({
341 ec: 'image',
342 ea: 'pull',
343 el: 'error'
344 }).send();
345
346 visitor.event({
347 ec: 'image',
348 ea: `pull from ${registry}`,
349 el: 'error'
350 }).send();
351
352 visitor.event({
353 ec: `image pull from ${registry}`,
354 ea: `used ${pullDuration}`,
355 el: 'error'
356 }).send();
357 reject(err);
358 return;
359 }
360
361 visitor.event({
362 ec: 'image',
363 ea: `pull from ${registry}`,
364 el: 'success'
365 }).send();
366
367 visitor.event({
368 ec: 'image',
369 ea: 'pull',
370 el: 'success'
371 }).send();
372
373 visitor.event({
374 ec: `image pull from ${registry}`,
375 ea: `used ${pullDuration}`,
376 el: 'success'
377 }).send();
378
379 for (const r of dockerOpts.DOCKER_REGISTRIES) {
380 if (resolveImageName.indexOf(r) === 0) {
381 const image = await docker.getImage(resolveImageName);
382
383 const newImageName = resolveImageName.slice(r.length + 1);
384 const repoTag = newImageName.split(':');
385
386 // rename
387 await image.tag({
388 name: resolveImageName,
389 repo: _.first(repoTag),
390 tag: _.last(repoTag)
391 });
392 break;
393 }
394 }
395 resolve(resolveImageName);
396 };
397
398 containers.add(stream);
399 // pull image progress
400 followProgress(stream, onFinished);
401 });
402}
403
404function generateFunctionEnvs(functionProps) {
405 const environmentVariables = functionProps.EnvironmentVariables;
406
407 if (!environmentVariables) { return {}; }
408
409 return Object.assign({}, environmentVariables);
410}
411
412function generateRamdomContainerName() {
413 return `fun_local_${new Date().getTime()}_${Math.random().toString(36).substr(2, 7)}`;
414}
415
416async function generateDockerEnvs(baseDir, serviceName, serviceProps, functionName, functionProps, debugPort, httpParams, nasConfig, ishttpTrigger, debugIde, debugArgs) {
417 const envs = {};
418
419 if (httpParams) {
420 Object.assign(envs, {
421 'FC_HTTP_PARAMS': httpParams
422 });
423 }
424
425 const confEnv = await resolveLibPathsFromLdConf(baseDir, functionProps.CodeUri);
426
427 Object.assign(envs, confEnv);
428
429 const runtime = functionProps.Runtime;
430
431 if (debugPort && !debugArgs) {
432 const debugEnv = generateDebugEnv(runtime, debugPort, debugIde);
433
434 Object.assign(envs, debugEnv);
435 } else if (debugArgs) {
436 Object.assign(envs, {
437 DEBUG_OPTIONS: debugArgs
438 });
439 }
440
441 if (ishttpTrigger && runtime === 'java8') {
442 envs['fc_enable_new_java_ca'] = 'true';
443 }
444
445 Object.assign(envs, generateFunctionEnvs(functionProps));
446
447 const profile = await getProfile();
448
449 Object.assign(envs, {
450 'local': true,
451 'FC_ACCESS_KEY_ID': profile.accessKeyId,
452 'FC_ACCESS_KEY_SECRET': profile.accessKeySecret,
453 'FC_ACCOUND_ID': profile.accountId,
454 'FC_REGION': profile.defaultRegion,
455 'FC_FUNCTION_NAME': functionName,
456 'FC_HANDLER': functionProps.Handler,
457 'FC_MEMORY_SIZE': functionProps.MemorySize || 128,
458 'FC_TIMEOUT': functionProps.Timeout || 3,
459 'FC_INITIALIZER': functionProps.Initializer,
460 'FC_INITIALIZATIONIMEOUT': functionProps.InitializationTimeout || 3,
461 'FC_SERVICE_NAME': serviceName,
462 'FC_SERVICE_LOG_PROJECT': ((serviceProps || {}).LogConfig || {}).Project,
463 'FC_SERVICE_LOG_STORE': ((serviceProps || {}).LogConfig || {}).Logstore
464 });
465
466 return addEnv(envs, nasConfig);
467}
468
469async function pullImageIfNeed(imageName) {
470 const exist = await imageExist(imageName);
471
472 if (!exist || !skipPullImage) {
473
474 await pullImage(imageName);
475 } else {
476 debug(`skip pulling image ${imageName}...`);
477 console.log(`skip pulling image ${imageName}...`);
478 }
479}
480
481async function showDebugIdeTipsForVscode(serviceName, functionName, runtime, codeSource, debugPort) {
482 const vscodeDebugConfig = await generateVscodeDebugConfig(serviceName, functionName, runtime, codeSource, debugPort);
483
484 // todo: auto detect .vscode/launch.json in codeuri path.
485 console.log(blue('you can paste these config to .vscode/launch.json, and then attach to your running function'));
486 console.log('///////////////// config begin /////////////////');
487 console.log(JSON.stringify(vscodeDebugConfig, null, 4));
488 console.log('///////////////// config end /////////////////');
489}
490
491async function showDebugIdeTipsForPycharm(codeSource, debugPort) {
492
493 const stats = await fs.lstat(codeSource);
494
495 if (!stats.isDirectory()) {
496 codeSource = path.dirname(codeSource);
497 }
498
499 console.log(yellow(`\n========= Tips for PyCharm remote debug =========
500Local host name: ${ip.address()}
501Port : ${yellow(debugPort)}
502Path mappings : ${yellow(codeSource)}=/code
503
504Debug Code needed to copy to your function code:
505
506import pydevd
507pydevd.settrace('${ip.address()}', port=${debugPort}, stdoutToServer=True, stderrToServer=True)
508
509=========================================================================\n`));
510}
511
512function writeEventToStreamAndClose(stream, event) {
513
514 if (event) {
515 stream.write(event);
516 }
517
518 stream.end();
519}
520
521async function isDockerToolBoxAndEnsureDockerVersion() {
522
523 const dockerInfo = await docker.info();
524
525 await detectDockerVersion(dockerInfo.ServerVersion || '');
526
527 const obj = (dockerInfo.Labels || []).map(e => _.split(e, '=', 2))
528 .filter(e => e.length === 2)
529 .reduce((acc, cur) => (acc[cur[0]] = cur[1], acc), {});
530
531 return process.platform === 'win32' && obj.provider === 'virtualbox';
532}
533
534async function run(opts, event, outputStream, errorStream, context = {}) {
535
536 const container = await createContainer(opts);
537
538 const attachOpts = {
539 hijack: true,
540 stream: true,
541 stdin: true,
542 stdout: true,
543 stderr: true
544 };
545
546 const stream = await container.attach(attachOpts);
547
548 if (!outputStream) {
549 outputStream = process.stdout;
550 }
551
552 if (!errorStream) {
553 errorStream = process.stderr;
554 }
555
556 const errorTransform = processorTransformFactory({
557 serviceName: context.serviceName,
558 functionName: context.functionName,
559 errorStream: errorStream
560 });
561
562 if (!isWin) {
563 container.modem.demuxStream(stream, outputStream, errorTransform);
564 }
565
566 await container.start();
567
568 // dockerode bugs on windows. attach could not receive output and error
569 if (isWin) {
570 const logStream = await container.logs({
571 stdout: true,
572 stderr: true,
573 follow: true
574 });
575
576 container.modem.demuxStream(logStream, outputStream, errorTransform);
577 }
578
579 containers.add(container.id);
580
581 writeEventToStreamAndClose(stream, event);
582
583 // exitRs format: {"Error":null,"StatusCode":0}
584 // see https://docs.docker.com/engine/api/v1.37/#operation/ContainerWait
585 const exitRs = await container.wait();
586
587 containers.delete(container.id);
588
589 return exitRs;
590}
591
592async function createContainer(opts) {
593 const isWin = process.platform === 'win32';
594 const isMac = process.platform === 'darwin';
595
596 if (opts && isMac) {
597 if (opts.HostConfig) {
598 const pathsOutofSharedPaths = await findPathsOutofSharedPaths(opts.HostConfig.Mounts);
599 if (isMac && pathsOutofSharedPaths.length > 0) {
600 throw new Error(red(`Please add directory '${pathsOutofSharedPaths}' to Docker File sharing list, more information please refer to https://github.com/alibaba/funcraft/blob/master/docs/usage/faq-zh.md`));
601 }
602 }
603 }
604 const dockerToolBox = await isDockerToolBoxAndEnsureDockerVersion();
605
606 let container;
607 try {
608 // see https://github.com/apocas/dockerode/pull/38
609 container = await docker.createContainer(opts);
610 } catch (ex) {
611
612 if (ex.message.indexOf('invalid mount config for type') !== -1 && dockerToolBox) {
613 throw new Error(red(`The default host machine path for docker toolbox is under 'C:\\Users', Please make sure your project is in this directory. If you want to mount other disk paths, please refer to https://github.com/alibaba/funcraft/blob/master/docs/usage/faq-zh.md .`));
614 }
615 if (ex.message.indexOf('drive is not shared') !== -1 && isWin) {
616 throw new Error(red(`${ex.message}More information please refer to https://docs.docker.com/docker-for-windows/#shared-drives`));
617 }
618 throw ex;
619 }
620 return container;
621}
622
623async function createAndRunContainer(opts) {
624 const container = await createContainer(opts);
625 containers.add(container.id);
626 await container.start({});
627 return container;
628}
629
630async function execContainer(container, opts, outputStream, errorStream) {
631 outputStream = process.stdout;
632 errorStream = process.stderr;
633 const logStream = await container.logs({
634 stdout: true,
635 stderr: true,
636 follow: true,
637 since: (new Date().getTime() / 1000)
638 });
639 container.modem.demuxStream(logStream, outputStream, errorStream);
640 const exec = await container.exec(opts);
641 const stream = await exec.start();
642 // have to wait, otherwise stdin may not be readable
643 await new Promise(resolve => setTimeout(resolve, 30));
644 container.modem.demuxStream(stream, outputStream, errorStream);
645
646 await waitForExec(exec);
647 logStream.destroy();
648}
649
650async function waitForExec(exec) {
651 return await new Promise((resolve, reject) => {
652 // stream.on('end') could not receive end event on windows.
653 // so use inspect to check exec exit
654 function waitContainerExec() {
655 exec.inspect((err, data) => {
656 if (data.Running) {
657 setTimeout(waitContainerExec, 100);
658 return;
659 }
660 if (err) {
661 reject(err);
662 } else if (data.ExitCode !== 0) {
663 reject(`${data.ProcessConfig.entrypoint} exited with code ${data.ExitCode}`);
664 } else {
665 resolve(data.ExitCode);
666 }
667 });
668 }
669 waitContainerExec();
670 });
671}
672
673// outputStream, errorStream used for http invoke
674// because agent is started when container running and exec could not receive related logs
675async function startContainer(opts, outputStream, errorStream, context = {}) {
676
677 const container = await createContainer(opts);
678
679 containers.add(container.id);
680
681 try {
682 await container.start({});
683 } catch (err) {
684 console.error(err);
685 }
686
687 const logs = outputStream || errorStream;
688
689 if (logs) {
690 if (!outputStream) {
691 outputStream = devnull();
692 }
693
694 if (!errorStream) {
695 errorStream = devnull();
696 }
697
698 // dockerode bugs on windows. attach could not receive output and error, must use logs
699 const logStream = await container.logs({
700 stdout: true,
701 stderr: true,
702 follow: true
703 });
704
705 container.modem.demuxStream(logStream, outputStream, processorTransformFactory({
706 serviceName: context.serviceName,
707 functionName: context.functionName,
708 errorStream
709 }));
710 }
711
712 return {
713 stop: async () => {
714 await container.stop();
715 containers.delete(container.id);
716 },
717
718 exec: async (cmd, { cwd = '', env = {}, outputStream, errorStream, verbose = false, context = {}, event = null } = {}) => {
719 const stdin = event ? true : false;
720
721 const options = {
722 Cmd: cmd,
723 Env: dockerOpts.resolveDockerEnv(env),
724 Tty: false,
725 AttachStdin: stdin,
726 AttachStdout: true,
727 AttachStderr: true,
728 WorkingDir: cwd
729 };
730
731 // docker exec
732 debug('docker exec opts: ' + JSON.stringify(options, null, 4));
733
734 const exec = await container.exec(options);
735
736 const stream = await exec.start({ hijack: true, stdin });
737
738 // todo: have to wait, otherwise stdin may not be readable
739 await new Promise(resolve => setTimeout(resolve, 30));
740
741 if (event !== null) {
742 writeEventToStreamAndClose(stream, event);
743 }
744
745 if (!outputStream) {
746 outputStream = process.stdout;
747 }
748
749 if (!errorStream) {
750 errorStream = process.stderr;
751 }
752
753 if (verbose) {
754 container.modem.demuxStream(stream, outputStream, errorStream);
755 } else {
756 container.modem.demuxStream(stream, devnull(), errorStream);
757 }
758
759 return await waitForExec(exec);
760 }
761 };
762}
763
764async function startInstallationContainer({ runtime, imageName, codeUri, targets, context }) {
765 debug(`runtime: ${runtime}`);
766 debug(`codeUri: ${codeUri}`);
767
768 if (await isDockerToolBoxAndEnsureDockerVersion()) {
769 throw new Error(red(`\nWe detected that you are using docker toolbox. For a better experience, please upgrade 'docker for windows'.\nYou can refer to Chinese doc https://github.com/alibaba/funcraft/blob/master/docs/usage/installation-zh.md#windows-%E5%AE%89%E8%A3%85-docker or English doc https://github.com/alibaba/funcraft/blob/master/docs/usage/installation.md.`));
770 }
771
772 if (!imageName) {
773 imageName = await dockerOpts.resolveRuntimeToDockerImage(runtime, true);
774 if (!imageName) {
775 throw new Error(`invalid runtime name ${runtime}`);
776 }
777 }
778
779 const codeMount = await resolveCodeUriToMount(codeUri, false);
780
781 const installMounts = conventInstallTargetsToMounts(targets);
782 const passwdMount = await resolvePasswdMount();
783 const mounts = _.compact([codeMount, ...installMounts, passwdMount]);
784
785 await pullImageIfNeed(imageName);
786
787 const envs = addInstallTargetEnv({}, targets);
788 const opts = dockerOpts.generateInstallOpts(imageName, mounts, envs);
789
790 return await startContainer(opts);
791}
792
793function displaySboxTips(runtime) {
794 console.log(yellow(`\nWelcom to fun sbox environment.\n`));
795 console.log(yellow(`You can install system dependencies like this:`));
796 console.log(yellow(`fun-install apt-get install libxss1\n`));
797
798 switch (runtime) {
799 case 'nodejs6':
800 case 'nodejs8':
801 case 'nodejs10':
802 console.log(yellow(`You can install node modules like this:`));
803 console.log(yellow(`fun-install npm install puppeteer\n`));
804 break;
805 case 'python2.7':
806 case 'python3':
807 console.log(yellow(`You can install pip dependencies like this:`));
808 console.log(yellow(`fun-install pip install flask`));
809 break;
810 default:
811 break;
812 }
813 console.log(yellow('type \'fun-install --help\' for more help\n'));
814}
815
816async function startSboxContainer({
817 runtime, imageName,
818 mounts, cmd, envs,
819 isTty, isInteractive
820}) {
821 debug(`runtime: ${runtime}`);
822 debug(`mounts: ${mounts}`);
823 debug(`isTty: ${isTty}`);
824 debug(`isInteractive: ${isInteractive}`);
825
826 if (!imageName) {
827 imageName = await dockerOpts.resolveRuntimeToDockerImage(runtime, true);
828 if (!imageName) {
829 throw new Error(`invalid runtime name ${runtime}`);
830 }
831 }
832
833 debug(`cmd: ${parseArgsStringToArgv(cmd || '')}`);
834
835 const container = await createContainer(dockerOpts.generateSboxOpts({
836 imageName,
837 hostname: `fc-${runtime}`,
838 mounts,
839 envs,
840 cmd: parseArgsStringToArgv(cmd || ''),
841 isTty,
842 isInteractive
843 }));
844
845 containers.add(container.id);
846
847 await container.start();
848
849 const stream = await container.attach({
850 logs: true,
851 stream: true,
852 stdin: isInteractive,
853 stdout: true,
854 stderr: true
855 });
856
857 // show outputs
858 let logStream;
859 if (isTty) {
860 stream.pipe(process.stdout);
861 } else {
862 if (isInteractive || process.platform === 'win32') {
863 // 这种情况很诡异,收不到 stream 的 stdout,使用 log 绕过去。
864 logStream = await container.logs({
865 stdout: true,
866 stderr: true,
867 follow: true
868 });
869 container.modem.demuxStream(logStream, process.stdout, process.stderr);
870 } else {
871 container.modem.demuxStream(stream, process.stdout, process.stderr);
872 }
873
874 }
875
876 if (isInteractive) {
877 displaySboxTips(runtime);
878
879 // Connect stdin
880 process.stdin.pipe(stream);
881
882 let previousKey;
883 const CTRL_P = '\u0010', CTRL_Q = '\u0011';
884
885 process.stdin.on('data', (key) => {
886 // Detects it is detaching a running container
887 const keyStr = key.toString('ascii');
888 if (previousKey === CTRL_P && keyStr === CTRL_Q) {
889 container.stop(() => { });
890 }
891 previousKey = keyStr;
892 });
893
894 }
895
896 let resize;
897
898 const isRaw = process.isRaw;
899 if (isTty) {
900 // fix not exit process in windows
901 goThrough();
902
903 process.stdin.setRawMode(true);
904
905 resize = async () => {
906 const dimensions = {
907 h: process.stdout.rows,
908 w: process.stdout.columns
909 };
910
911 if (dimensions.h !== 0 && dimensions.w !== 0) {
912 await container.resize(dimensions);
913 }
914 };
915
916 await resize();
917 process.stdout.on('resize', resize);
918
919 // 在不加任何 cmd 的情况下 shell prompt 需要输出一些字符才会显示,
920 // 这里输入一个空格+退格,绕过这个怪异的问题。
921 stream.write(' \b');
922 }
923
924 await container.wait();
925
926 // cleanup
927 if (isTty) {
928 process.stdout.removeListener('resize', resize);
929 process.stdin.setRawMode(isRaw);
930 }
931
932 if (isInteractive) {
933 process.stdin.removeAllListeners();
934 process.stdin.unpipe(stream);
935
936 /**
937 * https://stackoverflow.com/questions/31716784/nodejs-process-never-ends-when-piping-the-stdin-to-a-child-process?rq=1
938 * https://github.com/nodejs/node/issues/2276
939 * */
940 process.stdin.destroy();
941 }
942
943 if (logStream) {
944 logStream.removeAllListeners();
945 }
946
947 stream.unpipe(process.stdout);
948
949 // fix not exit process in windows
950 // stream is hackji socks,so need to close
951 stream.destroy();
952
953 containers.delete(container.id);
954
955 if (!isTty) {
956 goThrough();
957 }
958}
959
960async function zipTo(archive, to) {
961
962 await fs.ensureDir(to);
963
964 await new Promise((resolve, reject) => {
965 archive.pipe(tar.extract(to)).on('error', reject).on('finish', resolve);
966 });
967}
968
969async function copyFromImage(imageName, from, to) {
970 const container = await docker.createContainer({
971 Image: imageName
972 });
973
974 const archive = await container.getArchive({
975 path: from
976 });
977
978 await zipTo(archive, to);
979
980 await container.remove();
981}
982
983function buildImage(dockerBuildDir, dockerfilePath, imageTag) {
984
985 return new Promise((resolve, reject) => {
986 var tarStream = tar.pack(dockerBuildDir);
987
988 docker.buildImage(tarStream, {
989 dockerfile: path.relative(dockerBuildDir, dockerfilePath),
990 t: imageTag
991 }, (error, stream) => {
992 containers.add(stream);
993
994 if (error) {
995 reject(error);
996 return;
997 }
998 stream.on('error', (e) => {
999 containers.delete(stream);
1000 reject(e);
1001 return;
1002 });
1003 stream.on('end', function () {
1004 containers.delete(stream);
1005 resolve(imageTag);
1006 return;
1007 });
1008
1009 followProgress(stream, (err, res) => err ? reject(err) : resolve(res));
1010 });
1011 });
1012}
1013
1014async function detectDockerVersion(serverVersion) {
1015 let cur = serverVersion.split('.');
1016 // 1.13.1
1017 if (Number.parseInt(cur[0]) === 1 && Number.parseInt(cur[1]) <= 13) {
1018 throw new Error(red(`\nWe detected that your docker version is ${serverVersion}, for a better experience, please upgrade the docker version.`));
1019 }
1020}
1021
1022module.exports = {
1023 imageExist, generateDockerCmd,
1024 pullImage,
1025 resolveCodeUriToMount, generateFunctionEnvs, run, generateRamdomContainerName,
1026 generateDockerEnvs, pullImageIfNeed,
1027 showDebugIdeTipsForVscode, resolveNasConfigToMounts,
1028 startInstallationContainer, startContainer, isDockerToolBoxAndEnsureDockerVersion,
1029 conventInstallTargetsToMounts, startSboxContainer, buildImage, copyFromImage,
1030 resolveTmpDirToMount, showDebugIdeTipsForPycharm, resolveDebuggerPathToMount,
1031 listContainers, getContainer, createAndRunContainer, execContainer,
1032 renameContainer, detectDockerVersion, resolveNasYmlToMount, resolvePasswdMount
1033};
\No newline at end of file