summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/remote-debugging/version-checker.js
blob: 247f44e2604206728c22399dca4749c88c5ef733 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

const Services = require("Services");
const { AppConstants } = require("resource://gre/modules/AppConstants.jsm");

const MS_PER_DAY = 1000 * 60 * 60 * 24;

const COMPATIBILITY_STATUS = {
  COMPATIBLE: "compatible",
  TOO_OLD: "too-old",
  TOO_OLD_FENNEC: "too-old-fennec",
  TOO_RECENT: "too-recent",
};
exports.COMPATIBILITY_STATUS = COMPATIBILITY_STATUS;

function getDateFromBuildID(buildID) {
  // Build IDs are a timestamp in the yyyyMMddHHmmss format.
  // Extract the year, month and day information.
  const fields = buildID.match(/(\d{4})(\d{2})(\d{2})/);
  // Date expects 0 - 11 for months
  const month = Number.parseInt(fields[2], 10) - 1;
  return new Date(fields[1], month, fields[3]);
}

function getMajorVersion(platformVersion) {
  // Retrieve the major platform version, i.e. if we are on Firefox 64.0a1, it will be 64.
  return Number.parseInt(platformVersion.match(/\d+/)[0], 10);
}

/**
 * Compute the minimum and maximum supported version for remote debugging for the provided
 * version of Firefox. Backward compatibility policy for devtools supports at most 2
 * versions older than the current version.
 *
 * @param {String} localVersion
 *        The version of the local Firefox instance, eg "67.0"
 * @return {Object}
 *         - minVersion {String} the minimum supported version, eg "65.0a1"
 *         - maxVersion {String} the first unsupported version, eg "68.0a1"
 */
function computeMinMaxVersion(localVersion) {
  // Retrieve the major platform version, i.e. if we are on Firefox 64.0a1, it will be 64.
  const localMajorVersion = getMajorVersion(localVersion);

  return {
    // Define the minimum officially supported version of Firefox when connecting to a
    // remote runtime. (Use ".0a1" to support the very first nightly version)
    // This matches the release channel's version when we are on nightly,
    // or 2 versions before when we are on other channels.
    minVersion: localMajorVersion - 2 + ".0a1",
    // The maximum version is the first excluded from the support range. That's why we
    // increase the current version by 1 and use ".0a1" to point to the first Nightly.
    // We do not support forward compatibility at all.
    maxVersion: localMajorVersion + 1 + ".0a1",
  };
}

/**
 * Tells if the remote device is using a supported version of Firefox.
 *
 * @param {DevToolsClient} devToolsClient
 *        DevToolsClient instance connected to the target remote Firefox.
 * @return Object with the following attributes:
 *   * String status, one of COMPATIBILITY_STATUS
 *            COMPATIBLE if the runtime is compatible,
 *            TOO_RECENT if the runtime uses a too recent version,
 *            TOO_OLD if the runtime uses a too old version.
 *   * String minVersion
 *            The minimum supported version.
 *   * String runtimeVersion
 *            The remote runtime version.
 *   * String localID
 *            Build ID of local runtime. A date with like this: YYYYMMDD.
 *   * String deviceID
 *            Build ID of remote runtime. A date with like this: YYYYMMDD.
 */
async function checkVersionCompatibility(devToolsClient) {
  const localDescription = {
    appbuildid: Services.appinfo.appBuildID,
    platformversion: AppConstants.MOZ_APP_VERSION,
  };

  try {
    const deviceFront = await devToolsClient.mainRoot.getFront("device");
    const description = await deviceFront.getDescription();
    return _compareVersionCompatibility(localDescription, description);
  } catch (e) {
    // If we failed to retrieve the device description, assume we are trying to connect to
    // a really old version of Firefox.
    const localVersion = localDescription.platformversion;
    const { minVersion } = computeMinMaxVersion(localVersion);
    return {
      minVersion,
      runtimeVersion: "<55",
      status: COMPATIBILITY_STATUS.TOO_OLD,
    };
  }
}
exports.checkVersionCompatibility = checkVersionCompatibility;

function _compareVersionCompatibility(localDescription, deviceDescription) {
  const runtimeID = deviceDescription.appbuildid.substr(0, 8);
  const localID = localDescription.appbuildid.substr(0, 8);

  const runtimeDate = getDateFromBuildID(runtimeID);
  const localDate = getDateFromBuildID(localID);

  const runtimeVersion = deviceDescription.platformversion;
  const localVersion = localDescription.platformversion;

  const { minVersion, maxVersion } = computeMinMaxVersion(localVersion);
  const isTooOld = Services.vc.compare(runtimeVersion, minVersion) < 0;
  const isTooRecent = Services.vc.compare(runtimeVersion, maxVersion) >= 0;

  const runtimeMajorVersion = getMajorVersion(runtimeVersion);
  const localMajorVersion = getMajorVersion(localVersion);
  const isSameMajorVersion = runtimeMajorVersion === localMajorVersion;

  let status;
  if (isTooOld) {
    if (runtimeMajorVersion === 68 && deviceDescription.os === "Android") {
      status = COMPATIBILITY_STATUS.TOO_OLD_FENNEC;
    } else {
      status = COMPATIBILITY_STATUS.TOO_OLD;
    }
  } else if (isTooRecent) {
    status = COMPATIBILITY_STATUS.TOO_RECENT;
  } else if (isSameMajorVersion && runtimeDate - localDate > 7 * MS_PER_DAY) {
    // If both local and remote runtimes have the same major version, compare build dates.
    // This check is useful for Gecko developers as we might introduce breaking changes
    // within a Nightly cycle.
    // Still allow devices to be newer by up to a week. This accommodates those with local
    // device builds, since their devices will almost always be newer than the client.
    status = COMPATIBILITY_STATUS.TOO_RECENT;
  } else {
    status = COMPATIBILITY_STATUS.COMPATIBLE;
  }

  return {
    localID,
    localVersion,
    minVersion,
    runtimeID,
    runtimeVersion,
    status,
  };
}
// Exported for tests.
exports._compareVersionCompatibility = _compareVersionCompatibility;