UNPKG

27.9 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 definition = require('./definition');
13const getVisitor = require('./visitor').getVisitor;
14const getProfile = require('./profile').getProfile;
15
16const { blue, red, yellow } = require('colors');
17const { parseArgsStringToArgv } = require('string-argv');
18const { extractNasMappingsFromNasYml } = require('./nas/support');
19const { addEnv, addInstallTargetEnv, resolveLibPathsFromLdConf } = require('./install/env');
20const { findPathsOutofSharedPaths } = require('./docker-support');
21const { processorTransformFactory } = require('./error-processor');
22
23const _ = require('lodash');
24
25require('draftlog').into(console);
26
27var containers = new Set();
28
29const devnull = require('dev-null');
30
31// exit container, when use ctrl + c
32function waitingForContainerStopped() {
33 // see https://stackoverflow.com/questions/10021373/what-is-the-windows-equivalent-of-process-onsigint-in-node-js
34 const isRaw = process.isRaw;
35 const kpCallBack = (_char, key) => {
36 if (key & key.ctrl && key.name === 'c') {
37 process.emit('SIGINT');
38 }
39 };
40 if (process.platform === 'win32') {
41 if (process.stdin.isTTY) {
42 process.stdin.setRawMode(isRaw);
43 }
44 process.stdin.on('keypress', kpCallBack);
45 }
46
47 let stopping = false;
48
49 process.on('SIGINT', async () => {
50
51 debug('containers length: ', containers.length);
52
53 if (stopping) {
54 return;
55 }
56
57 // Just fix test on windows
58 // Because process.emit('SIGINT') in test/docker.test.js will not trigger rl.on('SIGINT')
59 // And when listening to stdin the process never finishes until you send a SIGINT signal explicitly.
60 process.stdin.destroy();
61
62 if (!containers.size) {
63 return;
64 }
65
66 stopping = true;
67
68 console.log(`\nreceived canncel request, stopping running containers.....`);
69
70 const jobs = [];
71
72 for (let container of containers) {
73 try {
74 if (container.destroy) { // container stream
75 container.destroy();
76 } else {
77 const c = docker.getContainer(container);
78
79 await c.inspect();
80
81 console.log(`stopping container ${container}`);
82
83 jobs.push(c.stop());
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(serviceName, nasConfig, nasBaseDir) {
117 const nasMappings = await nas.convertNasConfigToNasMappings(nasBaseDir, nasConfig, serviceName);
118 return convertNasMappingsToMounts(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
165function convertNasMappingsToMounts(nasMappings) {
166 return nasMappings.map(nasMapping => {
167 debug('mounting local nas mock dir %s into container %s\n', nasMapping.localNasDir, nasMapping.remoteNasDir);
168 return {
169 Type: 'bind',
170 Source: nasMapping.localNasDir,
171 Target: nasMapping.remoteNasDir,
172 ReadOnly: false
173 };
174 });
175}
176
177async function resolveNasYmlToMount(baseDir, serviceName) {
178 const nasMappings = await extractNasMappingsFromNasYml(baseDir, serviceName);
179 return convertNasMappingsToMounts(nasMappings);
180}
181
182function conventInstallTargetsToMounts(installTargets) {
183
184 if (!installTargets) { return []; }
185
186 const mounts = [];
187
188 _.forEach(installTargets, (target) => {
189 const { hostPath, containerPath } = target;
190
191 if (!(fs.pathExistsSync(hostPath))) {
192 fs.ensureDirSync(hostPath);
193 }
194
195 mounts.push({
196 Type: 'bind',
197 Source: hostPath,
198 Target: containerPath,
199 ReadOnly: false
200 });
201 });
202
203 return mounts;
204}
205
206async function imageExist(imageName) {
207
208 const images = await docker.listImages({
209 filters: {
210 reference: [imageName]
211 }
212 });
213
214 return images.length > 0;
215}
216
217async function listContainers(options) {
218 return await docker.listContainers(options);
219}
220
221async function getContainer(containerId) {
222 return await docker.getContainer(containerId);
223}
224
225async function renameContainer(container, name) {
226 return await container.rename({
227 name
228 });
229}
230
231// dockerode exec 在 windows 上有问题,用 exec 的 stdin 传递事件,当调用 stream.end() 时,会直接导致 exec 退出,且 ExitCode 为 null
232function generateDockerCmd(functionProps, httpMode, invokeInitializer = true, event = null) {
233 const cmd = ['-h', functionProps.Handler];
234
235 // 如果提供了 event
236 if (event !== null) {
237 cmd.push('--event', Buffer.from(event).toString('base64'));
238 cmd.push('--event-decode');
239 } else {
240 // always pass event using stdin mode
241 cmd.push('--stdin');
242 }
243
244 if (httpMode) {
245 cmd.push('--http');
246 }
247
248 const initializer = functionProps.Initializer;
249
250 if (initializer && invokeInitializer) {
251 cmd.push('-i', initializer);
252 }
253
254 const initializationTimeout = functionProps.InitializationTimeout;
255
256 // initializationTimeout is defined as integer, see lib/validate/schema/function.js
257 if (initializationTimeout) {
258 cmd.push('--initializationTimeout', initializationTimeout.toString());
259 }
260
261 debug(`docker cmd: ${cmd}`);
262
263 return cmd;
264}
265
266
267function followProgress(stream, onFinished) {
268
269 const barLines = {};
270
271 const onProgress = (event) => {
272 let status = event.status;
273
274 if (event.progress) {
275 status = `${event.status} ${event.progress}`;
276 }
277
278 if (event.id) {
279 const id = event.id;
280
281 if (!barLines[id]) {
282 barLines[id] = console.draft();
283 }
284 barLines[id](id + ': ' + status);
285 } else {
286 if (_.has(event, 'aux.ID')) {
287 event.stream = event.aux.ID + '\n';
288 }
289 // If there is no id, the line should be wrapped manually.
290 const out = event.status ? event.status + '\n' : event.stream;
291 process.stdout.write(out);
292 }
293 };
294
295 docker.modem.followProgress(stream, onFinished, onProgress);
296}
297
298async function pullImage(imageName) {
299
300 const resolveImageName = await dockerOpts.resolveImageNameForPull(imageName);
301
302 // copied from lib/edge/container.js
303 const startTime = new Date();
304
305 const stream = await docker.pull(resolveImageName);
306
307 const visitor = await getVisitor();
308
309 visitor.event({
310 ec: 'image',
311 ea: 'pull',
312 el: 'start'
313 }).send();
314
315 const registry = await dockerOpts.resolveDockerRegistry();
316
317 return new Promise((resolve, reject) => {
318
319 console.log(`begin pulling image ${resolveImageName}, you can also use ` + yellow(`'docker pull ${resolveImageName}'`) + ' to pull image by yourself.');
320
321 const onFinished = async (err) => {
322
323 containers.delete(stream);
324
325 const pullDuration = parseInt((new Date() - startTime) / 1000);
326 if (err) {
327 visitor.event({
328 ec: 'image',
329 ea: 'pull',
330 el: 'error'
331 }).send();
332
333 visitor.event({
334 ec: 'image',
335 ea: `pull from ${registry}`,
336 el: 'error'
337 }).send();
338
339 visitor.event({
340 ec: `image pull from ${registry}`,
341 ea: `used ${pullDuration}`,
342 el: 'error'
343 }).send();
344 reject(err);
345 return;
346 }
347
348 visitor.event({
349 ec: 'image',
350 ea: `pull from ${registry}`,
351 el: 'success'
352 }).send();
353
354 visitor.event({
355 ec: 'image',
356 ea: 'pull',
357 el: 'success'
358 }).send();
359
360 visitor.event({
361 ec: `image pull from ${registry}`,
362 ea: `used ${pullDuration}`,
363 el: 'success'
364 }).send();
365
366 for (const r of dockerOpts.DOCKER_REGISTRIES) {
367 if (resolveImageName.indexOf(r) === 0) {
368 const image = await docker.getImage(resolveImageName);
369
370 const newImageName = resolveImageName.slice(r.length + 1);
371 const repoTag = newImageName.split(':');
372
373 // rename
374 await image.tag({
375 name: resolveImageName,
376 repo: _.first(repoTag),
377 tag: _.last(repoTag)
378 });
379 break;
380 }
381 }
382 resolve(resolveImageName);
383 };
384
385 containers.add(stream);
386 // pull image progress
387 followProgress(stream, onFinished);
388 });
389}
390
391function generateFunctionEnvs(functionProps) {
392 const environmentVariables = functionProps.EnvironmentVariables;
393
394 if (!environmentVariables) { return {}; }
395
396 return Object.assign({}, environmentVariables);
397}
398
399function generateRamdomContainerName() {
400 return `fun_local_${new Date().getTime()}_${Math.random().toString(36).substr(2, 7)}`;
401}
402
403async function generateDockerEnvs(baseDir, serviceName, serviceProps, functionName, functionProps, debugPort, httpParams, nasConfig, ishttpTrigger, debugIde, debugArgs) {
404
405 const envs = {};
406
407 if (httpParams) {
408 Object.assign(envs, {
409 'FC_HTTP_PARAMS': httpParams
410 });
411 }
412
413 const confEnv = await resolveLibPathsFromLdConf(baseDir, functionProps.CodeUri);
414
415 Object.assign(envs, confEnv);
416
417 const runtime = functionProps.Runtime;
418
419 if (debugPort && !debugArgs) {
420 const debugEnv = generateDebugEnv(runtime, debugPort, debugIde);
421
422 Object.assign(envs, debugEnv);
423 } else if (debugArgs) {
424 Object.assign(envs, {
425 DEBUG_OPTIONS: debugArgs
426 });
427 }
428
429 if (ishttpTrigger && runtime === 'java8') {
430 envs['fc_enable_new_java_ca'] = 'true';
431 }
432
433 Object.assign(envs, generateFunctionEnvs(functionProps));
434
435 const profile = await getProfile();
436
437 Object.assign(envs, {
438 'local': true,
439 'FC_ACCESS_KEY_ID': profile.accessKeyId,
440 'FC_ACCESS_KEY_SECRET': profile.accessKeySecret,
441 'FC_ACCOUND_ID': profile.accountId,
442 'FC_REGION': profile.defaultRegion,
443 'FC_FUNCTION_NAME': functionName,
444 'FC_HANDLER': functionProps.Handler,
445 'FC_MEMORY_SIZE': functionProps.MemorySize || 128,
446 'FC_TIMEOUT': functionProps.Timeout || 3,
447 'FC_INITIALIZER': functionProps.Initializer,
448 'FC_INITIALIZATIONIMEOUT': functionProps.InitializationTimeout || 3,
449 'FC_SERVICE_NAME': serviceName,
450 'FC_SERVICE_LOG_PROJECT': (serviceProps.LogConfig || {}).Project,
451 'FC_SERVICE_LOG_STORE': (serviceProps.LogConfig || {}).Logstore
452 });
453
454 return addEnv(envs, nasConfig);
455}
456
457async function pullImageIfNeed(imageName) {
458 const exist = await imageExist(imageName);
459
460 if (!exist || !skipPullImage) {
461
462 await pullImage(imageName);
463 } else {
464 debug(`skip pulling image ${imageName}...`);
465 console.log(`skip pulling image ${imageName}...`);
466 }
467}
468
469async function showDebugIdeTipsForVscode(serviceName, functionName, runtime, codeSource, debugPort) {
470 const vscodeDebugConfig = await generateVscodeDebugConfig(serviceName, functionName, runtime, codeSource, debugPort);
471
472 // todo: auto detect .vscode/launch.json in codeuri path.
473 console.log(blue('you can paste these config to .vscode/launch.json, and then attach to your running function'));
474 console.log('///////////////// config begin /////////////////');
475 console.log(JSON.stringify(vscodeDebugConfig, null, 4));
476 console.log('///////////////// config end /////////////////');
477}
478
479async function showDebugIdeTipsForPycharm(codeSource, debugPort) {
480
481 const stats = await fs.lstat(codeSource);
482
483 if (!stats.isDirectory()) {
484 codeSource = path.dirname(codeSource);
485 }
486
487 console.log(yellow(`\n========= Tips for PyCharm remote debug =========
488Local host name: ${ip.address()}
489Port : ${yellow(debugPort)}
490Path mappings : ${yellow(codeSource)}=/code
491
492Debug Code needed to copy to your function code:
493
494import pydevd
495pydevd.settrace('${ip.address()}', port=${debugPort}, stdoutToServer=True, stderrToServer=True)
496
497=========================================================================\n`));
498}
499
500function writeEventToStreamAndClose(stream, event) {
501
502 if (event) {
503 stream.write(event);
504 }
505
506 stream.end();
507}
508
509async function isDockerToolBoxAndEnsureDockerVersion() {
510
511 const dockerInfo = await docker.info();
512
513 await detectDockerVersion(dockerInfo.ServerVersion || '');
514
515 const obj = (dockerInfo.Labels || []).map(e => _.split(e, '=', 2))
516 .filter(e => e.length === 2)
517 .reduce((acc, cur) => (acc[cur[0]] = cur[1], acc), {});
518
519 return process.platform === 'win32' && obj.provider === 'virtualbox';
520}
521
522async function run(opts, event, outputStream, errorStream, context = {}) {
523
524 const container = await createContainer(opts);
525
526 const attachOpts = {
527 hijack: true,
528 stream: true,
529 stdin: true,
530 stdout: true,
531 stderr: true
532 };
533
534 const stream = await container.attach(attachOpts);
535
536 if (!outputStream) {
537 outputStream = process.stdout;
538 }
539
540 if (!errorStream) {
541 errorStream = process.stderr;
542 }
543
544 const errorTransform = processorTransformFactory({
545 serviceName: context.serviceName,
546 functionName: context.functionName,
547 errorStream: errorStream
548 });
549
550 var isWin = process.platform === 'win32';
551 if (!isWin) {
552 container.modem.demuxStream(stream, outputStream, errorTransform);
553 }
554
555 await container.start();
556
557 // dockerode bugs on windows. attach could not receive output and error
558 if (isWin) {
559 const logStream = await container.logs({
560 stdout: true,
561 stderr: true,
562 follow: true
563 });
564
565 container.modem.demuxStream(logStream, outputStream, errorTransform);
566 }
567
568 containers.add(container.id);
569
570 writeEventToStreamAndClose(stream, event);
571
572 // exitRs format: {"Error":null,"StatusCode":0}
573 // see https://docs.docker.com/engine/api/v1.37/#operation/ContainerWait
574 const exitRs = await container.wait();
575
576 containers.delete(container.id);
577
578 return exitRs;
579}
580
581function resolveDockerUser(nasConfig) {
582 let { userId, groupId } = definition.getUserIdAndGroupId(nasConfig);
583 if (userId === -1 || userId === undefined) {
584 userId = 10003;
585 }
586 if (groupId === -1 || groupId === undefined) {
587 groupId = 10003;
588 }
589 return `${userId}:${groupId}`;
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 = {} } = {}) => {
719 const options = {
720 Cmd: cmd,
721 Env: dockerOpts.resolveDockerEnv(env),
722 Tty: false,
723 AttachStdin: false,
724 AttachStdout: true,
725 AttachStderr: true,
726 WorkingDir: cwd
727 };
728
729 // docker exec
730 debug('docker exec opts: ' + JSON.stringify(options, null, 4));
731
732 const exec = await container.exec(options);
733
734 const stream = await exec.start({ hijack: true, stdin: false });
735
736 // todo: have to wait, otherwise stdin may not be readable
737 await new Promise(resolve => setTimeout(resolve, 30));
738
739 if (!outputStream) {
740 outputStream = process.stdout;
741 }
742
743 if (!errorStream) {
744 errorStream = process.stderr;
745 }
746
747 if (verbose) {
748 container.modem.demuxStream(stream, outputStream, errorStream);
749 } else {
750 container.modem.demuxStream(stream, devnull(), errorStream);
751 }
752
753 return await waitForExec(exec);
754 }
755 };
756}
757
758async function startInstallationContainer({ runtime, imageName, codeUri, targets, context }) {
759 debug(`runtime: ${runtime}`);
760 debug(`codeUri: ${codeUri}`);
761
762 if (await isDockerToolBoxAndEnsureDockerVersion()) {
763 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.`));
764 }
765
766 if (!imageName) {
767 imageName = await dockerOpts.resolveRuntimeToDockerImage(runtime, true);
768 if (!imageName) {
769 throw new Error(`invalid runtime name ${runtime}`);
770 }
771 }
772
773 const codeMount = await resolveCodeUriToMount(codeUri, false);
774 const installMounts = conventInstallTargetsToMounts(targets);
775 const mounts = [codeMount, ...installMounts];
776
777 await pullImageIfNeed(imageName);
778
779 const envs = addInstallTargetEnv({}, targets);
780 const opts = dockerOpts.generateInstallOpts(imageName, mounts, envs);
781
782 return await startContainer(opts);
783}
784
785function displaySboxTips(runtime) {
786 console.log(yellow(`\nWelcom to fun sbox environment.\n`));
787 console.log(yellow(`You can install system dependencies like this:`));
788 console.log(yellow(`fun-install apt-get install libxss1\n`));
789
790 switch (runtime) {
791 case 'nodejs6':
792 case 'nodejs8':
793 case 'nodejs10':
794 console.log(yellow(`You can install node modules like this:`));
795 console.log(yellow(`fun-install npm install puppeteer\n`));
796 break;
797 case 'python2.7':
798 case 'python3':
799 console.log(yellow(`You can install pip dependencies like this:`));
800 console.log(yellow(`fun-install pip install flask`));
801 break;
802 default:
803 break;
804 }
805 console.log(yellow('type \'fun-install --help\' for more help\n'));
806}
807
808async function startSboxContainer({
809 runtime, imageName,
810 mounts, cmd, envs,
811 isTty, isInteractive
812}) {
813 debug(`runtime: ${runtime}`);
814 debug(`mounts: ${mounts}`);
815 debug(`isTty: ${isTty}`);
816 debug(`isInteractive: ${isInteractive}`);
817
818 if (!imageName) {
819 imageName = await dockerOpts.resolveRuntimeToDockerImage(runtime, true);
820 if (!imageName) {
821 throw new Error(`invalid runtime name ${runtime}`);
822 }
823 }
824
825 debug(`cmd: ${parseArgsStringToArgv(cmd || '')}`);
826
827 const container = await createContainer(dockerOpts.generateSboxOpts({
828 imageName,
829 hostname: `fc-${runtime}`,
830 mounts,
831 envs,
832 cmd: parseArgsStringToArgv(cmd || ''),
833 isTty,
834 isInteractive
835 }));
836
837 containers.add(container.id);
838
839 await container.start();
840
841 const stream = await container.attach({
842 logs: true,
843 stream: true,
844 stdin: isInteractive,
845 stdout: true,
846 stderr: true
847 });
848
849 // show outputs
850 let logStream;
851 if (isTty) {
852 stream.pipe(process.stdout);
853 } else {
854 if (isInteractive || process.platform === 'win32') {
855 // 这种情况很诡异,收不到 stream 的 stdout,使用 log 绕过去。
856 logStream = await container.logs({
857 stdout: true,
858 stderr: true,
859 follow: true
860 });
861 container.modem.demuxStream(logStream, process.stdout, process.stderr);
862 } else {
863 container.modem.demuxStream(stream, process.stdout, process.stderr);
864 }
865
866 }
867
868 if (isInteractive) {
869 displaySboxTips(runtime);
870
871 // Connect stdin
872 process.stdin.pipe(stream);
873
874 let previousKey;
875 const CTRL_P = '\u0010', CTRL_Q = '\u0011';
876
877 process.stdin.on('data', (key) => {
878 // Detects it is detaching a running container
879 const keyStr = key.toString('ascii');
880 if (previousKey === CTRL_P && keyStr === CTRL_Q) {
881 container.stop(() => { });
882 }
883 previousKey = keyStr;
884 });
885
886 }
887
888 let resize;
889
890 const isRaw = process.isRaw;
891 if (isTty) {
892 // fix not exit process in windows
893 goThrough();
894
895 process.stdin.setRawMode(true);
896
897 resize = async () => {
898 const dimensions = {
899 h: process.stdout.rows,
900 w: process.stdout.columns
901 };
902
903 if (dimensions.h !== 0 && dimensions.w !== 0) {
904 await container.resize(dimensions);
905 }
906 };
907
908 await resize();
909 process.stdout.on('resize', resize);
910
911 // 在不加任何 cmd 的情况下 shell prompt 需要输出一些字符才会显示,
912 // 这里输入一个空格+退格,绕过这个怪异的问题。
913 stream.write(' \b');
914 }
915
916 await container.wait();
917
918 // cleanup
919 if (isTty) {
920 process.stdout.removeListener('resize', resize);
921 process.stdin.setRawMode(isRaw);
922 }
923
924 if (isInteractive) {
925 process.stdin.removeAllListeners();
926 process.stdin.unpipe(stream);
927
928 /**
929 * https://stackoverflow.com/questions/31716784/nodejs-process-never-ends-when-piping-the-stdin-to-a-child-process?rq=1
930 * https://github.com/nodejs/node/issues/2276
931 * */
932 process.stdin.destroy();
933 }
934
935 if (logStream) {
936 logStream.removeAllListeners();
937 }
938
939 stream.unpipe(process.stdout);
940
941 // fix not exit process in windows
942 // stream is hackji socks,so need to close
943 stream.destroy();
944
945 containers.delete(container.id);
946
947 if (!isTty) {
948 goThrough();
949 process.stdin.destroy();
950 }
951}
952
953async function zipTo(archive, to) {
954
955 await fs.ensureDir(to);
956
957 await new Promise((resolve, reject) => {
958 archive.pipe(tar.extract(to)).on('error', reject).on('finish', resolve);
959 });
960}
961
962async function copyFromImage(imageName, from, to) {
963 const container = await docker.createContainer({
964 Image: imageName
965 });
966
967 const archive = await container.getArchive({
968 path: from
969 });
970
971 await zipTo(archive, to);
972
973 await container.remove();
974}
975
976function buildImage(dockerBuildDir, dockerfilePath, imageTag) {
977
978 return new Promise((resolve, reject) => {
979 var tarStream = tar.pack(dockerBuildDir);
980
981 docker.buildImage(tarStream, {
982 dockerfile: path.relative(dockerBuildDir, dockerfilePath),
983 t: imageTag
984 }, (error, stream) => {
985 containers.add(stream);
986
987 if (error) { reject(error); }
988 else {
989 stream.on('error', (e) => {
990 containers.delete(stream);
991 reject(e);
992 });
993 stream.on('end', function () {
994 containers.delete(stream);
995 resolve(imageTag);
996 });
997 }
998 followProgress(stream, (err, res) => err ? reject(err) : resolve(res));
999 });
1000 });
1001}
1002async function detectDockerVersion(serverVersion) {
1003 let cur = serverVersion.split('.');
1004 // 1.13.1
1005 if (Number.parseInt(cur[0]) === 1 && Number.parseInt(cur[1]) <= 13) {
1006 throw new Error(red(`\nWe detected that your docker version is ${serverVersion}, for a better experience, please upgrade the docker version.`));
1007 }
1008}
1009
1010module.exports = {
1011 imageExist, generateDockerCmd,
1012 pullImage,
1013 resolveCodeUriToMount, generateFunctionEnvs, run, generateRamdomContainerName,
1014 generateDockerEnvs, pullImageIfNeed,
1015 showDebugIdeTipsForVscode, resolveDockerUser, resolveNasConfigToMounts,
1016 startInstallationContainer, startContainer, isDockerToolBoxAndEnsureDockerVersion,
1017 conventInstallTargetsToMounts, startSboxContainer, buildImage, copyFromImage,
1018 resolveTmpDirToMount, showDebugIdeTipsForPycharm, resolveDebuggerPathToMount,
1019 listContainers, getContainer, createAndRunContainer, execContainer,
1020 renameContainer, detectDockerVersion, resolveNasYmlToMount
1021};
\No newline at end of file