1 | import _ from 'lodash';
|
2 | import path from 'path';
|
3 | import { mkdirp, fs, system } from 'appium-support';
|
4 | import axios from 'axios';
|
5 | import { exec } from 'teen_process';
|
6 | import { rootDir } from './utils';
|
7 | import logger from './logger';
|
8 | import semver from 'semver';
|
9 | import {
|
10 | StoreDeprecatedDefaultCapabilityAction, DEFAULT_CAPS_ARG,
|
11 | } from './argsparse-actions';
|
12 |
|
13 |
|
14 | const npmPackage = require(path.resolve(rootDir, 'package.json'));
|
15 | const APPIUM_VER = npmPackage.version;
|
16 | const MIN_NODE_VERSION = npmPackage.engines.node;
|
17 |
|
18 | const GIT_META_ROOT = '.git';
|
19 | const GIT_BINARY = `git${system.isWindows() ? '.exe' : ''}`;
|
20 | const GITHUB_API = 'https://api.github.com/repos/appium/appium';
|
21 |
|
22 | const BUILD_INFO = {
|
23 | version: APPIUM_VER,
|
24 | };
|
25 |
|
26 | function getNodeVersion () {
|
27 | return semver.coerce(process.version);
|
28 | }
|
29 |
|
30 | function isSubClass (candidateClass, superClass) {
|
31 | return _.isFunction(superClass) && _.isFunction(candidateClass)
|
32 | && (candidateClass.prototype instanceof superClass || candidateClass === superClass);
|
33 | }
|
34 |
|
35 | async function updateBuildInfo (useGithubApiFallback = false) {
|
36 | const sha = await getGitRev(useGithubApiFallback);
|
37 | if (!sha) {
|
38 | return;
|
39 | }
|
40 | BUILD_INFO['git-sha'] = sha;
|
41 | const built = await getGitTimestamp(sha, useGithubApiFallback);
|
42 | if (!_.isEmpty(built)) {
|
43 | BUILD_INFO.built = built;
|
44 | }
|
45 | }
|
46 |
|
47 | async function getGitRev (useGithubApiFallback = false) {
|
48 | if (await fs.exists(path.resolve(rootDir, GIT_META_ROOT))) {
|
49 | try {
|
50 | const {stdout} = await exec(GIT_BINARY, ['rev-parse', 'HEAD'], {
|
51 | cwd: rootDir
|
52 | });
|
53 | return stdout.trim();
|
54 | } catch (ign) {}
|
55 | }
|
56 |
|
57 | if (!useGithubApiFallback) {
|
58 | return null;
|
59 | }
|
60 |
|
61 | try {
|
62 | const resBodyObj = (await axios.get(`${GITHUB_API}/tags`, {
|
63 | headers: {
|
64 | 'User-Agent': `Appium ${APPIUM_VER}`
|
65 | }
|
66 | })).data;
|
67 | if (_.isArray(resBodyObj)) {
|
68 | for (const {name, commit} of resBodyObj) {
|
69 | if (name === `v${APPIUM_VER}` && commit && commit.sha) {
|
70 | return commit.sha;
|
71 | }
|
72 | }
|
73 | }
|
74 | } catch (ign) {}
|
75 | return null;
|
76 | }
|
77 |
|
78 | async function getGitTimestamp (commitSha, useGithubApiFallback = false) {
|
79 | if (await fs.exists(path.resolve(rootDir, GIT_META_ROOT))) {
|
80 | try {
|
81 | const {stdout} = await exec(GIT_BINARY, ['show', '-s', '--format=%ci', commitSha], {
|
82 | cwd: rootDir
|
83 | });
|
84 | return stdout.trim();
|
85 | } catch (ign) {}
|
86 | }
|
87 |
|
88 | if (!useGithubApiFallback) {
|
89 | return null;
|
90 | }
|
91 |
|
92 | try {
|
93 | const resBodyObj = (await axios.get(`${GITHUB_API}/commits/${commitSha}`, {
|
94 | headers: {
|
95 | 'User-Agent': `Appium ${APPIUM_VER}`
|
96 | }
|
97 | })).data;
|
98 | if (resBodyObj && resBodyObj.commit) {
|
99 | if (resBodyObj.commit.committer && resBodyObj.commit.committer.date) {
|
100 | return resBodyObj.commit.committer.date;
|
101 | }
|
102 | if (resBodyObj.commit.author && resBodyObj.commit.author.date) {
|
103 | return resBodyObj.commit.author.date;
|
104 | }
|
105 | }
|
106 | } catch (ign) {}
|
107 | return null;
|
108 | }
|
109 |
|
110 |
|
111 |
|
112 |
|
113 |
|
114 |
|
115 |
|
116 | function getBuildInfo () {
|
117 | return BUILD_INFO;
|
118 | }
|
119 |
|
120 | function checkNodeOk () {
|
121 | const version = getNodeVersion();
|
122 | if (!semver.satisfies(version, MIN_NODE_VERSION)) {
|
123 | logger.errorAndThrow(`Node version must be ${MIN_NODE_VERSION}. Currently ${version.version}`);
|
124 | }
|
125 | }
|
126 |
|
127 | function warnNodeDeprecations () {
|
128 | |
129 |
|
130 |
|
131 |
|
132 |
|
133 |
|
134 |
|
135 |
|
136 |
|
137 |
|
138 |
|
139 | }
|
140 |
|
141 | async function showConfig () {
|
142 | await updateBuildInfo(true);
|
143 | console.log(JSON.stringify(getBuildInfo()));
|
144 | }
|
145 |
|
146 | function getNonDefaultArgs (parser, args) {
|
147 | return parser.rawArgs.reduce((acc, [, {dest, default: defaultValue}]) => {
|
148 | if (args[dest] && args[dest] !== defaultValue) {
|
149 | acc[dest] = args[dest];
|
150 | }
|
151 | return acc;
|
152 | }, {});
|
153 | }
|
154 |
|
155 | function getDeprecatedArgs (parser, args) {
|
156 |
|
157 |
|
158 | return parser.rawArgs.reduce((acc, [[name], {dest, default: defaultValue, action}]) => {
|
159 | if (!args[dest] || args[dest] === defaultValue) {
|
160 | return acc;
|
161 | }
|
162 |
|
163 | if (action?.deprecated_for) {
|
164 | acc[name] = action.deprecated_for;
|
165 | } else if (isSubClass(action, StoreDeprecatedDefaultCapabilityAction)) {
|
166 | acc[name] = DEFAULT_CAPS_ARG;
|
167 | }
|
168 | return acc;
|
169 | }, {});
|
170 | }
|
171 |
|
172 | function checkValidPort (port, portName) {
|
173 | if (port > 0 && port < 65536) return true;
|
174 | logger.error(`Port '${portName}' must be greater than 0 and less than 65536. Currently ${port}`);
|
175 | return false;
|
176 | }
|
177 |
|
178 | function validateServerArgs (parser, args) {
|
179 |
|
180 | let exclusives = [
|
181 | ['noReset', 'fullReset'],
|
182 | ['ipa', 'safari'],
|
183 | ['app', 'safari'],
|
184 | ['forceIphone', 'forceIpad'],
|
185 | ['deviceName', 'defaultDevice']
|
186 | ];
|
187 |
|
188 | for (let exSet of exclusives) {
|
189 | let numFoundInArgs = 0;
|
190 | for (let opt of exSet) {
|
191 | if (_.has(args, opt) && args[opt]) {
|
192 | numFoundInArgs++;
|
193 | }
|
194 | }
|
195 | if (numFoundInArgs > 1) {
|
196 | throw new Error(`You can't pass in more than one argument from the ` +
|
197 | `set ${JSON.stringify(exSet)}, since they are ` +
|
198 | `mutually exclusive`);
|
199 | }
|
200 | }
|
201 |
|
202 | const validations = {
|
203 | port: checkValidPort,
|
204 | callbackPort: checkValidPort,
|
205 | bootstrapPort: checkValidPort,
|
206 | chromedriverPort: checkValidPort,
|
207 | robotPort: checkValidPort,
|
208 | backendRetries: (r) => r >= 0,
|
209 | };
|
210 |
|
211 | const nonDefaultArgs = getNonDefaultArgs(parser, args);
|
212 |
|
213 | for (let [arg, validator] of _.toPairs(validations)) {
|
214 | if (_.has(nonDefaultArgs, arg)) {
|
215 | if (!validator(args[arg], arg)) {
|
216 | throw new Error(`Invalid argument for param ${arg}: ${args[arg]}`);
|
217 | }
|
218 | }
|
219 | }
|
220 | }
|
221 |
|
222 | async function validateTmpDir (tmpDir) {
|
223 | try {
|
224 | await mkdirp(tmpDir);
|
225 | } catch (e) {
|
226 | throw new Error(`We could not ensure that the temp dir you specified ` +
|
227 | `(${tmpDir}) exists. Please make sure it's writeable.`);
|
228 | }
|
229 | }
|
230 |
|
231 | export {
|
232 | getBuildInfo, validateServerArgs, checkNodeOk, showConfig,
|
233 | warnNodeDeprecations, validateTmpDir, getNonDefaultArgs, getDeprecatedArgs,
|
234 | getGitRev, checkValidPort, APPIUM_VER, updateBuildInfo,
|
235 | };
|