NsisCompatUpdater.ts 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. import { dirname } from 'path';
  2. import { resolve as urlResolve } from 'url';
  3. import { lstat, readFile, createReadStream, createWriteStream, Stats, ReadStream } from 'fs';
  4. import { IncomingMessage } from 'http';
  5. import { createHash } from 'crypto';
  6. import { spawn } from 'child_process';
  7. import * as semver from 'semver';
  8. import * as tmp from 'tmp';
  9. const debug = require('debug/src/browser')('nsis-compat-updater');
  10. const got = require('got');
  11. const progressStream = require('progress-stream');
  12. import { Event } from './Event';
  13. interface IInstaller {
  14. arch: string;
  15. path: string;
  16. hash: string;
  17. created: number;
  18. }
  19. interface IUpdater {
  20. arch: string;
  21. fromVersion: string;
  22. path: string;
  23. hash: string;
  24. created: number;
  25. }
  26. interface IVersion {
  27. version: string;
  28. changelog: string;
  29. source: string;
  30. installers: IInstaller[];
  31. updaters: IUpdater[];
  32. }
  33. interface IVersionInfo {
  34. latest: string;
  35. versions: IVersion[];
  36. }
  37. interface IStreamProgress {
  38. percentage: number;
  39. transferred: number;
  40. length: number;
  41. remaining: number;
  42. eta: number;
  43. runtime: number;
  44. delta: number;
  45. speed: number;
  46. }
  47. export class NsisCompatUpdater {
  48. public onDownloadProgress: Event<IStreamProgress> = new Event('downloadProgress');
  49. protected versionInfo: IVersionInfo;
  50. constructor(protected seed: string, protected currentVersion: string, protected currentArch: 'x86' | 'x64') {
  51. }
  52. public async checkForUpdates(): Promise<IVersion> {
  53. debug('in checkForUpdates');
  54. const versionInfo = await this.getVersionInfo();
  55. if(!semver.gt(versionInfo.latest, this.currentVersion)) {
  56. return null;
  57. }
  58. return await this.getVersion(versionInfo.latest);
  59. }
  60. public async downloadUpdate(version: string): Promise<string> {
  61. debug('in downloadUpdate', 'version', version);
  62. const { installers, updaters } = await this.getVersion(version);
  63. const { url, hash } = (() => {
  64. const updater = updaters.filter(updater => updater.fromVersion == this.currentVersion && updater.arch == this.currentArch)[0];
  65. if(updater) {
  66. return {
  67. url: `${ urlResolve(dirname(this.seed), updater.path) }`,
  68. hash: updater.hash,
  69. };
  70. }
  71. const installer = installers.filter(installer => installer.arch == this.currentArch)[0];
  72. if(installer) {
  73. return {
  74. url: `${ urlResolve(dirname(this.seed), installer.path) }`,
  75. hash: installer.hash,
  76. };
  77. }
  78. throw new Error('ERROR_UPDATER_NOT_FOUND');
  79. })();
  80. const path = await this.tmpUpdateFile();
  81. debug('in downloadUpdate', 'url', url);
  82. await this.download(url, path);
  83. debug('in downloadUpdate', 'path', path);
  84. if(!await this.checkFileHash('sha256', path, hash)) {
  85. throw new Error('ERROR_HASH_MISMATCH');
  86. }
  87. return path;
  88. }
  89. public install(path: string, slient: boolean = false) {
  90. debug('in install', 'path', path);
  91. debug(`in install`, 'slient', slient);
  92. const args = [];
  93. const options = {
  94. detached: true,
  95. stdio: 'ignore',
  96. };
  97. if(slient) {
  98. args.push('/S');
  99. }
  100. try {
  101. spawn(path, args, options)
  102. .unref();
  103. }
  104. catch(err) {
  105. if(err.code == 'UNKNOWN') {
  106. /*
  107. // TODO: Elevate and run again.
  108. spawn(elevate, [ path, ...args ], options)
  109. .unref();
  110. */
  111. }
  112. else {
  113. throw err;
  114. }
  115. }
  116. }
  117. public installWhenQuit(path: string) {
  118. console.info('installWhenQuit');
  119. if((<any>process.versions).nw) {
  120. throw new Error('ERROR_UNKNOWN');
  121. }
  122. else if((<any>process.versions).electron) {
  123. return require('electron').app.on('quit', () => this.install(path, true));
  124. }
  125. else {
  126. throw new Error('ERROR_UNKNOWN');
  127. }
  128. }
  129. public quitAndInstall(path: string) {
  130. console.info('quitAndInstall');
  131. if((<any>process.versions).nw) {
  132. this.install(path, false);
  133. nw.App.quit();
  134. }
  135. else if((<any>process.versions).electron) {
  136. return require('electron').app.quit();
  137. }
  138. else {
  139. throw new Error('ERROR_UNKNOWN');
  140. }
  141. }
  142. protected async getVersion(version: string): Promise<IVersion> {
  143. const versionInfo = await this.getVersionInfo();
  144. const item = versionInfo.versions.filter(item => item.version == version)[0];
  145. if(!item) {
  146. throw new Error('ERROR_VERSION_NOT_FOUND');
  147. }
  148. return item;
  149. }
  150. protected tmpUpdateFile(): Promise<string> {
  151. return new Promise((resolve, reject) => {
  152. tmp.file(<any>{
  153. postfix: '.exe',
  154. discardDescriptor: true,
  155. }, (err, path, fd, cleanup) => err ? reject(err) : resolve(path));
  156. });
  157. }
  158. protected checkFileHash(type: string, path: string, expected: string): Promise<boolean> {
  159. return new Promise((resolve, reject) => {
  160. const hasher = createHash(type);
  161. hasher.on('error', reject);
  162. hasher.on('readable', () => {
  163. const data = hasher.read();
  164. if(data) {
  165. resolve((<any>data).toString('hex') == expected);
  166. }
  167. });
  168. createReadStream(path).pipe(hasher);
  169. });
  170. }
  171. protected async getVersionInfo(): Promise<IVersionInfo> {
  172. if(!this.versionInfo) {
  173. const versionInfo = await got(this.seed, {
  174. timeout: 5000,
  175. })
  176. .then((res: any) => JSON.parse(res.body));
  177. debug('in getVersionInfo', 'versionInfo', versionInfo);
  178. this.versionInfo = versionInfo;
  179. }
  180. return this.versionInfo;
  181. }
  182. protected async download(url: string, path: string, onProgress?: (state: IStreamProgress) => void) {
  183. const stream = got.stream(url);
  184. const size = await new Promise((resolve, reject) => {
  185. stream.on('error', reject);
  186. stream.on('response', resolve);
  187. })
  188. .then((res: IncomingMessage) => res.headers['content-type']);
  189. const progress = progressStream({
  190. length: size,
  191. time: 1000,
  192. });
  193. progress.on('progress', this.onDownloadProgress.trigger);
  194. await new Promise((resolve, reject) => {
  195. stream.pipe(progress)
  196. .pipe(createWriteStream(path))
  197. .on('finish', resolve);
  198. });
  199. }
  200. }