<commit_before>import { cli } from 'cli-ux'
import * as path from 'path'
import RWLockfile from 'rwlockfile'
import _ from 'ts-lodash'

import Config from './config'
import deps from './deps'

const { spawn } = require('cross-spawn')
const debug = require('debug')('cli:updater')

export interface IVersion {
  version: string
  channel: string
  message?: string
}

export interface IManifest {
  version: string
  channel: string
  sha256gz: string
}

async function mtime(f: string) {
  const { mtime } = await deps.file.stat(f)
  return deps.moment(mtime)
}

function timestamp(msg: string): string {
  return `[${deps.moment().format()}] ${msg}`
}

export class Updater {
  config: Config
  lock: RWLockfile
  http: typeof deps.HTTP

  constructor(config: Config) {
    this.config = config
    this.http = deps.HTTP.defaults({ headers: { 'user-agent': config.userAgent } })
  }

  get autoupdatefile(): string {
    return path.join(this.config.cacheDir, 'autoupdate')
  }
  get autoupdatelogfile(): string {
    return path.join(this.config.cacheDir, 'autoupdate.log')
  }
  get versionFile(): string {
    return path.join(this.config.cacheDir, `${this.config.channel}.version`)
  }

  private get clientRoot(): string {
    return path.join(this.config.dataDir, 'client')
  }
  private get clientBin(): string {
    let b = path.join(this.clientRoot, 'bin', this.config.bin)
    return this.config.windows ? `${b}.cmd` : b
  }

  private get binPath(): string {
    return this.config.reexecBin || this.config.bin
  }

  private get s3Host(): string | undefined {
    return this.config.s3 && this.config.s3.host
  }

  s3url(channel: string, p: string): string {
    if (!this.s3Host) throw new Error('S3 host not defined')
    return `https://${this.s3Host}/${this.config.name}/channels/${channel}/${p}`
  }

  async fetchManifest(channel: string): Promise<IManifest> {
    try {
      let { body } = await this.http.get(this.s3url(channel, `${this.config.platform}-${this.config.arch}`))
      return body
    } catch (err) {
      if (err.statusCode === 403) throw new Error(`HTTP 403: Invalid channel ${channel}`)
      throw err
    }
  }

  async fetchVersion(download: boolean): Promise<IVersion> {
    let v: IVersion | undefined
    try {
      if (!download) v = await deps.file.readJSON(this.versionFile)
    } catch (err) {
      if (err.code !== 'ENOENT') throw err
    }
    if (!v) {
      debug('fetching latest %s version', this.config.channel)
      let { body } = await this.http.get(this.s3url(this.config.channel, 'version'))
      v = body
      await this._catch(() => deps.file.outputJSON(this.versionFile, v))
    }
    return v!
  }

  public async warnIfUpdateAvailable() {
    await this._catch(async () => {
      if (!this.s3Host) return
      let v = await this.fetchVersion(false)
      if (deps.util.minorVersionGreater(this.config.version, v.version)) {
        cli.warn(`${this.config.name}: update available from ${this.config.version} to ${v.version}`)
      }
      if (v.message) {
        cli.warn(`${this.config.name}: ${v.message}`)
      }
    })
  }

  public async autoupdate(force: boolean = false) {
    try {
      await this.warnIfUpdateAvailable()
      if (!force && !await this.autoupdateNeeded()) return

      debug('autoupdate running')
      await deps.file.outputFile(this.autoupdatefile, '')

      debug(`spawning autoupdate on ${this.binPath}`)

      let fd = await deps.file.open(this.autoupdatelogfile, 'a')
      deps.file.write(
        fd,
        timestamp(`starting \`${this.binPath} update --autoupdate\` from ${process.argv.slice(1, 3).join(' ')}\n`),
      )

      spawn(this.binPath, ['update', '--autoupdate'], {
        detached: !this.config.windows,
        stdio: ['ignore', fd, fd],
        env: this.autoupdateEnv,
      })
        .on('error', (e: Error) => cli.warn(e, { context: 'autoupdate:' }))
        .unref()
    } catch (e) {
      cli.warn(e, { context: 'autoupdate:' })
    }
  }

  async update(manifest: IManifest) {
    let base = this.base(manifest)
    const output = path.join(this.clientRoot, manifest.version)
    const tmp = path.join(this.clientRoot, base)
    const lock = new RWLockfile(this.autoupdatefile, { ifLocked: () => cli.action.start('CLI is updating') })

    if (!this.s3Host) throw new Error('S3 host not defined')

    await lock.add('write', { reason: 'update' })

    try {
      let url = `https://${this.s3Host}/${this.config.name}/channels/${manifest.channel}/${base}.tar.gz`
      let { response: stream } = await this.http.stream(url)

      await deps.file.emptyDir(tmp)
      let extraction = this.extract(stream, this.clientRoot, manifest.sha256gz)

      // TODO: use cli.action.type
      if (deps.filesize && (cli.action as any).frames) {
        // if spinner action
        let total = stream.headers['content-length']
        let current = 0
        const updateStatus = _.throttle(
          (newStatus: string) => {
            cli.action.status = newStatus
          },
          500,
          { leading: true, trailing: false },
        )
        stream.on('data', data => {
          current += data.length
          updateStatus(`${deps.filesize(current)}/${deps.filesize(total)}`)
        })
      }

      await extraction
      if (await deps.file.exists(output)) {
        const old = `${output}.old`
        await deps.file.remove(old)
        await deps.file.rename(output, old)
      }
      await deps.file.rename(tmp, output)
      await deps.file.touch(output)

      await this._createBin(manifest)
    } finally {
      await lock.remove('write')
    }
    await this.reexecUpdate()
  }

  public async tidy() {
    try {
      const { moment, file } = deps
      let root = this.clientRoot
      if (!await file.exists(root)) return
      let files = await file.ls(root)
      let promises = files.map(async f => {
        if (['bin', this.config.version].includes(path.basename(f.path))) return
        if (moment(f.stat.mtime).isBefore(moment().subtract(24, 'hours'))) {
          await file.remove(f.path)
        }
      })
      for (let p of promises) await p
    } catch (err) {
      cli.warn(err)
    }
  }

  private extract(stream: NodeJS.ReadableStream, dir: string, sha: string): Promise<void> {
    const zlib = require('zlib')
    const tar = require('tar-fs')
    const crypto = require('crypto')

    return new Promise((resolve, reject) => {<commit_msg>skip tidying if version is not inside<commit_after>import { cli } from 'cli-ux'
import * as path from 'path'
import RWLockfile from 'rwlockfile'
import _ from 'ts-lodash'

import Config from './config'
import deps from './deps'

const { spawn } = require('cross-spawn')
const debug = require('debug')('cli:updater')

export interface IVersion {
  version: string
  channel: string
  message?: string
}

export interface IManifest {
  version: string
  channel: string
  sha256gz: string
}

async function mtime(f: string) {
  const { mtime } = await deps.file.stat(f)
  return deps.moment(mtime)
}

function timestamp(msg: string): string {
  return `[${deps.moment().format()}] ${msg}`
}

export class Updater {
  config: Config
  lock: RWLockfile
  http: typeof deps.HTTP

  constructor(config: Config) {
    this.config = config
    this.http = deps.HTTP.defaults({ headers: { 'user-agent': config.userAgent } })
  }

  get autoupdatefile(): string {
    return path.join(this.config.cacheDir, 'autoupdate')
  }
  get autoupdatelogfile(): string {
    return path.join(this.config.cacheDir, 'autoupdate.log')
  }
  get versionFile(): string {
    return path.join(this.config.cacheDir, `${this.config.channel}.version`)
  }

  private get clientRoot(): string {
    return path.join(this.config.dataDir, 'client')
  }
  private get clientBin(): string {
    let b = path.join(this.clientRoot, 'bin', this.config.bin)
    return this.config.windows ? `${b}.cmd` : b
  }

  private get binPath(): string {
    return this.config.reexecBin || this.config.bin
  }

  private get s3Host(): string | undefined {
    return this.config.s3 && this.config.s3.host
  }

  s3url(channel: string, p: string): string {
    if (!this.s3Host) throw new Error('S3 host not defined')
    return `https://${this.s3Host}/${this.config.name}/channels/${channel}/${p}`
  }

  async fetchManifest(channel: string): Promise<IManifest> {
    try {
      let { body } = await this.http.get(this.s3url(channel, `${this.config.platform}-${this.config.arch}`))
      return body
    } catch (err) {
      if (err.statusCode === 403) throw new Error(`HTTP 403: Invalid channel ${channel}`)
      throw err
    }
  }

  async fetchVersion(download: boolean): Promise<IVersion> {
    let v: IVersion | undefined
    try {
      if (!download) v = await deps.file.readJSON(this.versionFile)
    } catch (err) {
      if (err.code !== 'ENOENT') throw err
    }
    if (!v) {
      debug('fetching latest %s version', this.config.channel)
      let { body } = await this.http.get(this.s3url(this.config.channel, 'version'))
      v = body
      await this._catch(() => deps.file.outputJSON(this.versionFile, v))
    }
    return v!
  }

  public async warnIfUpdateAvailable() {
    await this._catch(async () => {
      if (!this.s3Host) return
      let v = await this.fetchVersion(false)
      if (deps.util.minorVersionGreater(this.config.version, v.version)) {
        cli.warn(`${this.config.name}: update available from ${this.config.version} to ${v.version}`)
      }
      if (v.message) {
        cli.warn(`${this.config.name}: ${v.message}`)
      }
    })
  }

  public async autoupdate(force: boolean = false) {
    try {
      await this.warnIfUpdateAvailable()
      if (!force && !await this.autoupdateNeeded()) return

      debug('autoupdate running')
      await deps.file.outputFile(this.autoupdatefile, '')

      debug(`spawning autoupdate on ${this.binPath}`)

      let fd = await deps.file.open(this.autoupdatelogfile, 'a')
      deps.file.write(
        fd,
        timestamp(`starting \`${this.binPath} update --autoupdate\` from ${process.argv.slice(1, 3).join(' ')}\n`),
      )

      spawn(this.binPath, ['update', '--autoupdate'], {
        detached: !this.config.windows,
        stdio: ['ignore', fd, fd],
        env: this.autoupdateEnv,
      })
        .on('error', (e: Error) => cli.warn(e, { context: 'autoupdate:' }))
        .unref()
    } catch (e) {
      cli.warn(e, { context: 'autoupdate:' })
    }
  }

  async update(manifest: IManifest) {
    let base = this.base(manifest)
    const output = path.join(this.clientRoot, manifest.version)
    const tmp = path.join(this.clientRoot, base)
    const lock = new RWLockfile(this.autoupdatefile, { ifLocked: () => cli.action.start('CLI is updating') })

    if (!this.s3Host) throw new Error('S3 host not defined')

    await lock.add('write', { reason: 'update' })

    try {
      let url = `https://${this.s3Host}/${this.config.name}/channels/${manifest.channel}/${base}.tar.gz`
      let { response: stream } = await this.http.stream(url)

      await deps.file.emptyDir(tmp)
      let extraction = this.extract(stream, this.clientRoot, manifest.sha256gz)

      // TODO: use cli.action.type
      if (deps.filesize && (cli.action as any).frames) {
        // if spinner action
        let total = stream.headers['content-length']
        let current = 0
        const updateStatus = _.throttle(
          (newStatus: string) => {
            cli.action.status = newStatus
          },
          500,
          { leading: true, trailing: false },
        )
        stream.on('data', data => {
          current += data.length
          updateStatus(`${deps.filesize(current)}/${deps.filesize(total)}`)
        })
      }

      await extraction
      if (await deps.file.exists(output)) {
        const old = `${output}.old`
        await deps.file.remove(old)
        await deps.file.rename(output, old)
      }
      await deps.file.rename(tmp, output)
      await deps.file.touch(output)

      await this._createBin(manifest)
    } finally {
      await lock.remove('write')
    }
    await this.reexecUpdate()
  }

  public async tidy() {
    try {
      if (!this.config.reexecBin) return
      if (!this.config.reexecBin.includes(this.config.version)) return
      const { moment, file } = deps
      let root = this.clientRoot
      if (!await file.exists(root)) return
      let files = await file.ls(root)
      let promises = files.map(async f => {
        if (['bin', this.config.version].includes(path.basename(f.path))) return
        if (moment(f.stat.mtime).isBefore(moment().subtract(24, 'hours'))) {
          await file.remove(f.path)
        }
      })
      for (let p of promises) await p
    } catch (err) {
      cli.warn(err)
    }
  }

  private extract(stream: NodeJS.ReadableStream, dir: string, sha: string): Promise<void> {
    const zlib = require('zlib')
    const tar = require('tar-fs')
    const crypto = require('crypto')

    return new Promise((resolve, reject) => {
{'fraction_non_alphanumeric': 0.1499000153822489, 'fraction_numerical': 0.005076142131979695, 'mean_word_length': 2.7755516840882692, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 25, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6293224', 'n_tokens_mistral': 4264, 'n_tokens_neox': 4223, 'n_words': 964}