2019-10-26 01:51:40 +02:00
import * as querystring from "querystring" ;
import * as request from "request" ;
2020-12-02 18:08:49 +01:00
import { app , dialog } from "electron" ;
2019-10-26 01:51:40 +02:00
import * as fs from "fs-extra" ;
import * as ofs from "original-fs" ;
import * as os from "os" ;
import * as tar from "tar-stream" ;
import * as path from "path" ;
import * as zlib from "zlib" ;
import * as child_process from "child_process" ;
import * as progress from "request-progress" ;
import * as util from "util" ;
2020-12-02 18:08:49 +01:00
import { parseVersion , Version } from "../../shared/version" ;
2019-10-26 01:51:40 +02:00
import MessageBoxOptions = Electron . MessageBoxOptions ;
import { Headers } from "tar-stream" ;
2020-10-01 10:56:22 +02:00
import { Arguments , processArguments } from "../../shared/process-arguments" ;
2019-10-26 01:51:40 +02:00
import * as electron from "electron" ;
import { PassThrough } from "stream" ;
import ErrnoException = NodeJS . ErrnoException ;
2020-12-02 18:08:49 +01:00
import { default as validateUpdateConfig } from "./UpdateConfigFile.validator" ;
import { default as validateAppInfo } from "./AppInfoFile.validator" ;
import UpdateConfigFile from "./UpdateConfigFile" ;
import AppInfoFile from "./AppInfoFile" ;
export type UpdateStatsCallback = ( message : string , progress : number ) = > void ;
export type UpdateLogCallback = ( type : "error" | "info" , message : string ) = > void ;
export function updateServerUrl ( ) : string {
/* FIXME! */
return "https://clientapi.teaspeak.de/" ;
return processArguments . has_value ( . . . Arguments . SERVER_URL ) ? processArguments . value ( . . . Arguments . SERVER_URL ) : "https://clientapi.teaspeak.de/" ;
2019-10-26 01:51:40 +02:00
}
export interface UpdateVersion {
channel : string ;
platform : string ,
arch : string ;
version : Version ;
}
export interface UpdateData {
versions : UpdateVersion [ ] ;
updater_version : UpdateVersion ;
}
2020-12-02 18:08:49 +01:00
let remoteVersionCacheTimestamp : number ;
let remoteVersionCache : Promise < UpdateData > ;
export async function fetchRemoteUpdateData ( ) : Promise < UpdateData > {
if ( remoteVersionCache && remoteVersionCacheTimestamp > Date . now ( ) - 60 * 60 * 1000 ) {
return remoteVersionCache ;
}
2019-10-26 01:51:40 +02:00
2020-12-02 18:08:49 +01:00
/* TODO: Validate remote response schema */
remoteVersionCacheTimestamp = Date . now ( ) ;
return ( remoteVersionCache = new Promise < UpdateData > ( ( resolve , reject ) = > {
const request_url = updateServerUrl ( ) + "/api.php?" + querystring . stringify ( {
2019-10-26 01:51:40 +02:00
type : "update-info"
} ) ;
console . log ( "request: %s" , request_url ) ;
request . get ( request_url , {
timeout : 2000
} , ( error , response , body ) = > {
2020-12-02 18:08:49 +01:00
if ( response . statusCode !== 200 ) {
setImmediate ( reject , "Invalid status code (" + response . statusCode + ( response . statusMessage ? "/" + response . statusMessage : "" ) + ")" ) ;
return ;
}
if ( ! response ) {
setImmediate ( reject , "Missing response object" ) ;
2019-10-26 01:51:40 +02:00
return ;
}
2020-12-02 18:08:49 +01:00
let data : any ;
try {
data = JSON . parse ( body ) ;
} catch ( _error ) {
setImmediate ( reject , "Failed to parse response" ) ;
2019-10-26 01:51:40 +02:00
return ;
}
2020-12-02 18:08:49 +01:00
2019-10-26 01:51:40 +02:00
if ( ! data [ "success" ] ) {
2020-12-02 18:08:49 +01:00
setImmediate ( reject , "Action failed (" + ( data [ "msg" ] || "unknown error" ) + ")" ) ;
2019-10-26 01:51:40 +02:00
return ;
}
let resp : UpdateData = { } as any ;
resp . versions = [ ] ;
for ( const channel of Object . keys ( data ) ) {
if ( channel == "success" ) continue ;
for ( const entry of data [ channel ] ) {
let version : UpdateVersion = { } as any ;
version . channel = channel ;
version . arch = entry [ "arch" ] ;
version . platform = entry [ "platform" ] ;
version . version = new Version ( entry [ "version" ] [ "major" ] , entry [ "version" ] [ "minor" ] , entry [ "version" ] [ "patch" ] , entry [ "version" ] [ "build" ] , entry [ "version" ] [ "timestamp" ] ) ;
2020-12-02 18:08:49 +01:00
if ( version . channel == 'updater' ) {
2019-10-26 01:51:40 +02:00
resp . updater_version = version ;
2020-12-02 18:08:49 +01:00
} else {
2019-10-26 01:51:40 +02:00
resp . versions . push ( version ) ;
2020-12-02 18:08:49 +01:00
}
2019-10-26 01:51:40 +02:00
}
}
2020-12-02 18:08:49 +01:00
setImmediate ( resolve , resp ) ;
2019-10-26 01:51:40 +02:00
} ) ;
2020-12-02 18:08:49 +01:00
} ) ) . catch ( error = > {
/* Don't cache errors */
remoteVersionCache = undefined ;
remoteVersionCacheTimestamp = undefined ;
return Promise . reject ( error ) ;
2019-10-26 01:51:40 +02:00
} ) ;
}
2020-12-02 18:08:49 +01:00
export async function availableRemoteChannels ( ) : Promise < string [ ] > {
const versions = ( await fetchRemoteUpdateData ( ) ) . versions . map ( e = > e . channel ) ;
2019-10-26 01:51:40 +02:00
2020-12-02 18:08:49 +01:00
return [ . . . new Set ( versions ) ] ;
2019-10-26 01:51:40 +02:00
}
2020-12-02 18:08:49 +01:00
export async function newestRemoteClientVersion ( channel : string ) : Promise < UpdateVersion | undefined > {
const data = await fetchRemoteUpdateData ( ) ;
2019-10-26 01:51:40 +02:00
2020-12-02 18:08:49 +01:00
let currentVersion : UpdateVersion ;
for ( const version of data . versions ) {
if ( version . arch == os . arch ( ) && version . platform == os . platform ( ) ) {
if ( version . channel == channel ) {
if ( ! currentVersion || version . version . newerThan ( currentVersion . version ) ) {
currentVersion = version ;
}
2019-10-26 01:51:40 +02:00
}
2020-12-02 18:08:49 +01:00
}
}
2019-10-26 01:51:40 +02:00
2020-12-02 18:08:49 +01:00
return currentVersion ;
2019-10-26 01:51:40 +02:00
}
2020-12-02 18:08:49 +01:00
function getAppDataDirectory ( ) : string {
2019-10-26 01:51:40 +02:00
return electron . app . getPath ( 'userData' ) ;
}
2020-12-02 18:08:49 +01:00
function generateUpdateFilePath ( channel : string , version : Version ) : string {
let directory = fs . realpathSync ( getAppDataDirectory ( ) ) ;
2019-10-26 01:51:40 +02:00
const name = channel + "_" + version . major + "_" + version . minor + "_" + version . patch + "_" + version . build + ".tar" ;
2020-12-02 18:08:49 +01:00
return path . join ( directory , "app_versions" , name ) ;
2019-10-26 01:51:40 +02:00
}
export interface ProgressState {
percent : number , // Overall percent (between 0 to 1)
speed : number , // The download speed in bytes/sec
size : {
total : number , // The total payload size in bytes
transferred : number // The transferred payload size in bytes
} ,
time : {
elapsed : number , // The total elapsed seconds since the start (3 decimals)
remaining : number // The remaining seconds to finish (3 decimals)
}
}
2020-12-02 18:08:49 +01:00
export async function downloadClientVersion ( channel : string , version : Version , status : ( state : ProgressState ) = > any , callbackLog : UpdateLogCallback ) : Promise < string > {
const targetFilePath = generateUpdateFilePath ( channel , version ) ;
if ( fs . existsSync ( targetFilePath ) ) {
callbackLog ( "info" , "Removing old update file located at " + targetFilePath ) ;
2019-10-26 01:51:40 +02:00
/* TODO test if this file is valid and can be used */
try {
2020-12-02 18:08:49 +01:00
await fs . remove ( targetFilePath ) ;
2019-10-26 01:51:40 +02:00
} catch ( error ) {
throw "Failed to remove old file: " + error ;
}
}
try {
2020-12-02 18:08:49 +01:00
await fs . mkdirp ( path . dirname ( targetFilePath ) ) ;
2019-10-26 01:51:40 +02:00
} catch ( error ) {
2020-12-02 18:08:49 +01:00
throw "Failed to make target directory: " + path . dirname ( targetFilePath ) ;
2019-10-26 01:51:40 +02:00
}
2020-12-02 18:08:49 +01:00
const requestUrl = updateServerUrl ( ) + "/api.php?" + querystring . stringify ( {
2019-10-26 01:51:40 +02:00
type : "update-download" ,
platform : os.platform ( ) ,
arch : os.arch ( ) ,
version : version.toString ( ) ,
channel : channel
} ) ;
2020-12-02 18:08:49 +01:00
callbackLog ( "info" , "Downloading version " + version . toString ( false ) + " to " + targetFilePath + " from " + updateServerUrl ( ) ) ;
console . log ( "Downloading update from %s. (%s)" , updateServerUrl ( ) , requestUrl ) ;
2019-10-26 01:51:40 +02:00
return new Promise < string > ( ( resolve , reject ) = > {
let fired = false ;
2020-12-02 18:08:49 +01:00
const fireFailed = ( reason : string ) = > {
if ( fired ) { return ; }
fired = true ;
setImmediate ( reject , reason ) ;
} ;
let stream = progress ( request . get ( requestUrl , {
timeout : 10_000
} , ( error , response , _body ) = > {
if ( ! response ) {
fireFailed ( "Missing response object" ) ;
return ;
}
if ( response . statusCode != 200 ) {
fireFailed ( "Invalid HTTP response code: " + response . statusCode + ( response . statusMessage ? "/" + response . statusMessage : "" ) ) ;
2019-10-26 01:51:40 +02:00
return ;
}
2020-12-02 18:08:49 +01:00
} ) ) . on ( 'progress' , status ) . on ( 'error' , error = > {
2019-10-26 01:51:40 +02:00
console . warn ( "Encountered error within download pipe. Ignoring error: %o" , error ) ;
} ) . on ( 'end' , function ( ) {
2020-12-02 18:08:49 +01:00
callbackLog ( "info" , "Update downloaded." ) ;
2019-10-26 01:51:40 +02:00
console . log ( "Update downloaded successfully. Waiting for write stream to finish." ) ;
2020-12-02 18:08:49 +01:00
if ( status ) {
2019-10-26 01:51:40 +02:00
status ( {
percent : 1 ,
speed : 0 ,
size : { total : 0 , transferred : 0 } ,
time : { elapsed : 0 , remaining : 0 }
2020-12-02 18:08:49 +01:00
} ) ;
}
2019-10-26 01:51:40 +02:00
} ) ;
console . log ( "Decompressing update package while streaming!" ) ;
stream = stream . pipe ( zlib . createGunzip ( ) ) ;
2020-12-02 18:08:49 +01:00
stream . pipe ( fs . createWriteStream ( targetFilePath , {
2019-10-26 01:51:40 +02:00
autoClose : true
} ) ) . on ( 'finish' , ( ) = > {
console . log ( "Write stream has finished. Download successfully." ) ;
2020-12-02 18:08:49 +01:00
if ( ! fired && ( fired = true ) ) {
setImmediate ( resolve , targetFilePath ) ;
}
2019-10-26 01:51:40 +02:00
} ) . on ( 'error' , error = > {
console . log ( "Write stream encountered an error while downloading update. Error: %o" , error ) ;
2020-12-02 18:08:49 +01:00
fireFailed ( "disk write error" ) ;
2019-10-26 01:51:40 +02:00
} ) ;
} ) ;
}
if ( typeof ( String . prototype . trim ) === "undefined" )
{
String . prototype . trim = function ( )
{
return String ( this ) . replace ( /^\s+|\s+$/g , '' ) ;
} ;
}
2020-12-02 18:08:49 +01:00
export async function ensureTargetFilesAreWriteable ( updateFile : string ) : Promise < string [ ] > {
2019-10-26 01:51:40 +02:00
const original_fs = require ( 'original-fs' ) ;
2020-12-02 18:08:49 +01:00
if ( ! fs . existsSync ( updateFile ) ) {
throw "Missing update file (" + updateFile + ")" ;
}
2019-10-26 01:51:40 +02:00
let parent_path = app . getAppPath ( ) ;
if ( parent_path . endsWith ( ".asar" ) ) {
parent_path = path . join ( parent_path , ".." , ".." ) ;
parent_path = fs . realpathSync ( parent_path ) ;
}
const test_access = async ( file : string , mode : number ) = > {
return await new Promise < NodeJS.ErrnoException > ( resolve = > original_fs . access ( file , mode , resolve ) ) ;
} ;
2020-12-02 18:08:49 +01:00
let code = await test_access ( updateFile , original_fs . constants . R_OK ) ;
2019-10-26 01:51:40 +02:00
if ( code )
2020-12-02 18:08:49 +01:00
throw "Failed test read for update file. (" + updateFile + " results in " + code . code + ")" ;
2019-10-26 01:51:40 +02:00
2020-12-02 18:08:49 +01:00
const fstream = original_fs . createReadStream ( updateFile ) ;
2019-10-26 01:51:40 +02:00
const tar_stream = tar . extract ( ) ;
const errors : string [ ] = [ ] ;
const tester = async ( header : Headers ) = > {
const entry_path = path . normalize ( path . join ( parent_path , header . name ) ) ;
if ( header . type == "file" ) {
if ( original_fs . existsSync ( entry_path ) ) {
code = await test_access ( entry_path , original_fs . constants . W_OK ) ;
if ( code )
errors . push ( "Failed to acquire write permissions for file " + entry_path + " (Code " + code . code + ")" ) ;
} else {
let directory = path . dirname ( entry_path ) ;
while ( directory . length != 0 && ! original_fs . existsSync ( directory ) )
directory = path . normalize ( path . join ( directory , ".." ) ) ;
code = await test_access ( directory , original_fs . constants . W_OK ) ;
if ( code ) errors . push ( "Failed to acquire write permissions for directory " + entry_path + " (Code " + code . code + ". Target directory " + directory + ")" ) ;
}
} else if ( header . type == "directory" ) {
let directory = path . dirname ( entry_path ) ;
while ( directory . length != 0 && ! original_fs . existsSync ( directory ) )
directory = path . normalize ( path . join ( directory , ".." ) ) ;
code = await test_access ( directory , original_fs . constants . W_OK ) ;
if ( code ) errors . push ( "Failed to acquire write permissions for directory " + entry_path + " (Code " + code . code + ". Target directory " + directory + ")" ) ;
}
} ;
tar_stream . on ( 'entry' , ( header : Headers , stream , next ) = > {
tester ( header ) . catch ( error = > {
console . log ( "Emit out of tar_stream.on('entry' ...)" ) ;
tar_stream . emit ( 'error' , error ) ;
} ) . then ( ( ) = > {
stream . on ( 'end' , next ) ;
stream . resume ( ) ;
} ) ;
} ) ;
fstream . pipe ( tar_stream ) ;
try {
await new Promise ( ( resolve , reject ) = > {
tar_stream . on ( 'finish' , resolve ) ;
tar_stream . on ( 'error' , error = > { reject ( error ) ; } ) ;
} ) ;
} catch ( error ) {
throw "Failed to list files within tar: " + error ;
}
return errors ;
}
namespace install_config {
export interface LockFile {
filename : string ;
timeout : number ;
"error-id" : string ;
}
export interface MoveFile {
source : string ;
target : string ;
"error-id" : string ;
}
export interface ConfigFile {
version : number ;
backup : boolean ;
"backup-directory" : string ;
"callback_file" : string ;
"callback_argument_fail" : string ;
"callback_argument_success" : string ;
moves : MoveFile [ ] ;
locks : LockFile [ ] ;
}
}
2020-12-02 18:08:49 +01:00
async function createUpdateInstallConfig ( sourceRoot : string , targetRoot : string ) : Promise < install_config.ConfigFile > {
console . log ( "Building update install config for target directory: %s. Update source: %o" , targetRoot , sourceRoot ) ;
2019-10-26 01:51:40 +02:00
const result : install_config.ConfigFile = { } as any ;
result . version = 1 ;
result . backup = true ;
{
2020-12-02 18:08:49 +01:00
const data = path . parse ( sourceRoot ) ;
2019-10-26 01:51:40 +02:00
result [ "backup-directory" ] = path . join ( data . dir , data . name + "_backup" ) ;
}
2020-12-02 18:52:59 +01:00
result [ "permission-test-directory" ] = targetRoot ;
2019-10-26 01:51:40 +02:00
result . callback_file = app . getPath ( "exe" ) ;
result . callback_argument_fail = "--no-single-instance --update-failed-new=" ;
result . callback_argument_success = "--no-single-instance --update-succeed-new=" ;
result . moves = [ ] ;
result . locks = [
{
"error-id" : "main-exe-lock" ,
filename : app.getPath ( "exe" ) ,
timeout : 5000
}
] ;
2020-12-02 18:08:49 +01:00
const ignoreFileList = [
2019-10-26 01:51:40 +02:00
"update-installer.exe" , "update-installer"
] ;
2020-12-02 18:08:49 +01:00
const dirWalker = async ( relative_path : string ) = > {
const source_directory = path . join ( sourceRoot , relative_path ) ;
const target_directory = path . join ( targetRoot , relative_path ) ;
2019-10-26 01:51:40 +02:00
let files : string [ ] ;
try {
files = await util . promisify ( ofs . readdir ) ( source_directory ) ;
} catch ( error ) {
console . warn ( "Failed to iterate over source directory \"%s\": %o" , source_directory , error ) ;
return ;
}
for ( const file of files ) {
2020-12-02 18:08:49 +01:00
let shouldBeExcluded = false ;
for ( const ignoredFile of ignoreFileList ) {
if ( ignoredFile == file ) {
2019-10-26 01:51:40 +02:00
console . debug ( "Ignoring file to update (%s/%s)" , relative_path , file ) ;
2020-12-02 18:08:49 +01:00
shouldBeExcluded = true ;
2019-10-26 01:51:40 +02:00
break ;
}
}
2020-12-02 18:08:49 +01:00
if ( shouldBeExcluded ) {
2019-10-26 01:51:40 +02:00
continue ;
2020-12-02 18:08:49 +01:00
}
2019-10-26 01:51:40 +02:00
const source_file = path . join ( source_directory , file ) ;
const target_file = path . join ( target_directory , file ) ;
//TODO check if file content has changed else ignore?
const info = await util . promisify ( ofs . stat ) ( source_file ) ;
if ( info . isDirectory ( ) ) {
2020-12-02 18:08:49 +01:00
await dirWalker ( path . join ( relative_path , file ) ) ;
2019-10-26 01:51:40 +02:00
} else {
/* TODO: ensure its a file! */
result . moves . push ( {
"error-id" : "move-file-" + result . moves . length ,
source : source_file ,
target : target_file
} ) ;
}
}
} ;
2020-12-02 18:08:49 +01:00
await dirWalker ( "." ) ;
2019-10-26 01:51:40 +02:00
return result ;
}
2020-12-02 18:08:49 +01:00
export async function extractUpdateFile ( updateFile : string , callbackLog : UpdateLogCallback ) : Promise < { updateSourceDirectory : string , updateInstallerExecutable : string } > {
const temporaryDirectory = path . join ( app . getPath ( "temp" ) , "teaclient_update_" + Math . random ( ) . toString ( 36 ) . substring ( 7 ) ) ;
2019-10-26 01:51:40 +02:00
2020-12-02 18:08:49 +01:00
try {
await fs . mkdirp ( temporaryDirectory )
} catch ( error ) {
console . error ( "failed to create update source directory (%s): %o" , temporaryDirectory , error ) ;
throw "failed to create update source directory" ;
}
2019-10-26 01:51:40 +02:00
2020-12-02 18:08:49 +01:00
callbackLog ( "info" , "Extracting update to " + temporaryDirectory ) ;
console . log ( "Extracting update file %s to %s" , updateFile , temporaryDirectory ) ;
2019-10-26 01:51:40 +02:00
2020-12-02 18:08:49 +01:00
let updateInstallerPath = undefined ;
2019-10-26 01:51:40 +02:00
2020-12-02 18:08:49 +01:00
const updateFileStream = fs . createReadStream ( updateFile ) ;
const extract = tar . extract ( ) ;
extract . on ( 'entry' , ( header : Headers , stream : PassThrough , callback ) = > {
const extract = async ( header : Headers , stream : PassThrough ) = > {
const targetFile = path . join ( temporaryDirectory , header . name ) ;
console . debug ( "Extracting entry %s of type %s to %s" , header . name , header . type , targetFile ) ;
if ( header . type == "directory" ) {
await fs . mkdirp ( targetFile ) ;
} else if ( header . type == "file" ) {
const targetPath = path . parse ( targetFile ) ;
{
const directory = targetPath . dir ;
console . debug ( "Testing for directory: %s" , directory ) ;
if ( ! ( await util . promisify ( ofs . exists ) ( directory ) ) || ! ( await util . promisify ( ofs . stat ) ( directory ) ) . isDirectory ( ) ) {
console . log ( "Creating directory %s" , directory ) ;
try {
await fs . mkdirp ( directory ) ;
} catch ( error ) {
console . warn ( "failed to create directory for file %s" , header . type ) ;
2019-10-27 22:39:59 +01:00
}
2020-12-02 18:08:49 +01:00
}
}
2019-10-27 22:39:59 +01:00
2020-12-02 18:08:49 +01:00
const write_stream = ofs . createWriteStream ( targetFile ) ;
try {
await new Promise ( ( resolve , reject ) = > {
stream . pipe ( write_stream )
. on ( 'error' , reject )
. on ( 'finish' , resolve ) ;
} ) ;
if ( targetPath . name === "update-installer" || targetPath . name === "update-installer.exe" ) {
updateInstallerPath = targetFile ;
callbackLog ( "info" , "Found update installer at " + targetFile ) ;
2019-10-26 01:51:40 +02:00
}
2020-12-02 18:08:49 +01:00
return ; /* success */
} catch ( error ) {
console . error ( "Failed to extract update file %s: %o" , header . name , error ) ;
2019-10-26 01:51:40 +02:00
}
2020-12-02 18:08:49 +01:00
} else {
console . debug ( "Skipping this unknown file type" ) ;
}
stream . resume ( ) ; /* drain the stream */
} ;
extract ( header , stream ) . catch ( error = > {
console . log ( "Ignoring file %s due to an error: %o" , header . name , error ) ;
} ) . then ( ( ) = > {
callback ( ) ;
2019-10-26 01:51:40 +02:00
} ) ;
2020-12-02 18:08:49 +01:00
} ) ;
2019-10-26 01:51:40 +02:00
2020-12-02 18:08:49 +01:00
updateFileStream . pipe ( extract ) ;
try {
await new Promise ( ( resolve , reject ) = > {
extract . on ( 'finish' , resolve ) ;
extract . on ( 'error' , reject ) ;
} ) ;
} catch ( error ) {
console . error ( "Failed to unpack update: %o" , error ) ;
throw "update unpacking failed" ;
}
if ( typeof updateInstallerPath !== "string" || ! ( await fs . pathExists ( updateInstallerPath ) ) ) {
throw "missing update installer executable within update package" ;
}
callbackLog ( "info" , "Update successfully extracted" ) ;
return { updateSourceDirectory : temporaryDirectory , updateInstallerExecutable : updateInstallerPath }
}
let cachedAppInfo : AppInfoFile ;
async function initializeAppInfo() {
let directory = app . getAppPath ( ) ;
if ( ! directory . endsWith ( ".asar" ) ) {
/* we're in a development version */
cachedAppInfo = {
version : 2 ,
clientVersion : {
major : 0 ,
minor : 0 ,
patch : 0 ,
buildIndex : 0 ,
timestamp : Date.now ( )
} ,
uiPackChannel : "release" ,
clientChannel : "release"
} ;
return ;
}
cachedAppInfo = validateAppInfo ( await fs . readJson ( path . join ( directory , ".." , ".." , "app-info.json" ) ) ) ;
if ( cachedAppInfo . version !== 2 ) {
cachedAppInfo = undefined ;
throw "invalid app info version" ;
}
}
export function clientAppInfo ( ) : AppInfoFile {
if ( typeof cachedAppInfo !== "object" ) {
throw "app info not initialized" ;
}
return cachedAppInfo ;
}
export async function currentClientVersion ( ) : Promise < Version > {
if ( processArguments . has_value ( Arguments . UPDATER_LOCAL_VERSION ) ) {
return parseVersion ( processArguments . value ( Arguments . UPDATER_LOCAL_VERSION ) ) ;
}
const info = clientAppInfo ( ) ;
return new Version ( info . clientVersion . major , info . clientVersion . minor , info . clientVersion . patch , info . clientVersion . buildIndex , info . clientVersion . timestamp ) ;
}
let cachedUpdateConfig : UpdateConfigFile ;
function updateConfigFile ( ) : string {
return path . join ( electron . app . getPath ( 'userData' ) , "update-settings.json" ) ;
}
export async function initializeAppUpdater() {
try {
await initializeAppInfo ( ) ;
} catch ( error ) {
console . error ( "Failed to parse app info: %o" , error ) ;
throw "Failed to parse app info file" ;
}
const config = updateConfigFile ( ) ;
if ( await fs . pathExists ( config ) ) {
2019-10-26 01:51:40 +02:00
try {
2020-12-02 18:08:49 +01:00
cachedUpdateConfig = validateUpdateConfig ( await fs . readJson ( config ) ) ;
if ( cachedUpdateConfig . version !== 1 ) {
cachedUpdateConfig = undefined ;
throw "invalid update config version" ;
}
} catch ( error ) {
console . warn ( "Failed to parse update config file: %o. Invalidating it." , error ) ;
try {
await fs . rename ( config , config + "." + Date . now ( ) ) ;
} catch ( _ ) { }
}
}
if ( ! cachedUpdateConfig ) {
cachedUpdateConfig = {
version : 1 ,
selectedChannel : "release"
}
}
}
export function updateConfig() {
if ( typeof cachedUpdateConfig === "string" ) {
throw "app updater hasn't been initialized yet" ;
}
return cachedUpdateConfig ;
}
export function saveUpdateConfig() {
const file = updateConfigFile ( ) ;
fs . writeJson ( file , cachedUpdateConfig ) . catch ( error = > {
console . error ( "Failed to save update config: %o" , error ) ;
} ) ;
}
/* Attention: The current channel might not be the channel the client has initially been loaded with! */
export function clientUpdateChannel ( ) : string {
return updateConfig ( ) . selectedChannel ;
}
export function setClientUpdateChannel ( channel : string ) {
if ( updateConfig ( ) . selectedChannel == channel ) {
return ;
}
updateConfig ( ) . selectedChannel = channel ;
saveUpdateConfig ( ) ;
}
export async function availableClientUpdate ( ) : Promise < UpdateVersion | undefined > {
const version = await newestRemoteClientVersion ( clientAppInfo ( ) . clientChannel ) ;
if ( ! version ) { return undefined ; }
const localVersion = await currentClientVersion ( ) ;
return ! localVersion . isDevelopmentVersion ( ) && version . version . newerThan ( localVersion ) ? version : undefined ;
}
/ * *
* @returns The callback to execute the update
* /
export async function prepareUpdateExecute ( targetVersion : UpdateVersion , callbackStats : UpdateStatsCallback , callbackLog : UpdateLogCallback ) : Promise < { callbackExecute : ( ) = > void , callbackAbort : ( ) = > void } > {
let targetApplicationPath = app . getAppPath ( ) ;
if ( targetApplicationPath . endsWith ( ".asar" ) ) {
console . log ( "App path points to ASAR file (Going up to root directory)" ) ;
targetApplicationPath = await fs . realpath ( path . join ( targetApplicationPath , ".." , ".." ) ) ;
} else {
throw "the source can't be updated" ;
}
callbackStats ( "Downloading update" , 0 ) ;
const updateFilePath = await downloadClientVersion ( targetVersion . channel , targetVersion . version , status = > {
callbackStats ( "Downloading update" , status . percent ) ;
} , callbackLog ) ;
/* TODO: Remove this step and let the actual updater so this. If this fails we'll already receiving appropiate error messages. */
if ( os . platform ( ) !== "win32" ) {
callbackLog ( "info" , "Checking file permissions" ) ;
callbackStats ( "Checking file permissions" , . 25 ) ;
/* We must be on a unix based system */
try {
const inaccessiblePaths = await ensureTargetFilesAreWriteable ( updateFilePath ) ;
if ( inaccessiblePaths . length > 0 ) {
console . log ( "Failed to access the following files:" ) ;
for ( const fail of inaccessiblePaths ) {
console . log ( " - " + fail ) ;
}
const executeCommand = "sudo " + path . normalize ( app . getAppPath ( ) ) + " --update-execute" ;
throw "Failed to access target files.\nPlease execute this app with administrator (sudo) privileges.\nUse the following command:\n" + executeCommand ;
}
2019-10-26 01:51:40 +02:00
} catch ( error ) {
2020-12-02 18:08:49 +01:00
console . warn ( "Failed to validate target file accessibility: %o" , error ) ;
2019-10-26 01:51:40 +02:00
}
2020-12-02 18:08:49 +01:00
} else {
/* the windows update already requests admin privileges */
2019-10-26 01:51:40 +02:00
}
2019-10-27 22:39:59 +01:00
2020-12-02 18:08:49 +01:00
callbackStats ( "Extracting update" , . 5 ) ;
const { updateSourceDirectory , updateInstallerExecutable } = await extractUpdateFile ( updateFilePath , callbackLog ) ;
2019-10-27 22:39:59 +01:00
2020-12-02 18:08:49 +01:00
callbackStats ( "Generating install config" , . 5 ) ;
2019-10-26 01:51:40 +02:00
2020-12-02 18:08:49 +01:00
callbackLog ( "info" , "Generating install config" ) ;
let installConfig ;
2019-10-26 01:51:40 +02:00
try {
2020-12-02 18:08:49 +01:00
installConfig = await createUpdateInstallConfig ( updateSourceDirectory , targetApplicationPath ) ;
2019-10-26 01:51:40 +02:00
} catch ( error ) {
console . error ( "Failed to build update installer config: %o" , error ) ;
throw "failed to build update installer config" ;
}
2020-12-02 18:08:49 +01:00
const installLogFile = path . join ( updateSourceDirectory , "update-log.txt" ) ;
const installConfigFile = path . join ( updateSourceDirectory , "update_install.json" ) ;
console . log ( "Writing config to %s" , installConfigFile ) ;
2019-10-26 01:51:40 +02:00
try {
2020-12-02 18:08:49 +01:00
await fs . writeJSON ( installConfigFile , installConfig ) ;
2019-10-26 01:51:40 +02:00
} catch ( error ) {
console . error ( "Failed to write update install config file: %s" , error ) ;
throw "failed to write update install config file" ;
}
2020-12-02 18:08:49 +01:00
callbackLog ( "info" , "Generating config generated at " + installConfigFile ) ;
let executeCallback : ( ) = > void ;
2019-10-26 01:51:40 +02:00
if ( os . platform ( ) == "linux" ) {
console . log ( "Executing update install on linux" ) ;
//We have to unpack it later
2020-12-02 18:08:49 +01:00
executeCallback = ( ) = > {
console . log ( "Executing command %s with args %o" , updateInstallerExecutable , [ installLogFile , installConfigFile ] ) ;
2019-10-26 01:51:40 +02:00
try {
2020-12-02 18:08:49 +01:00
let result = child_process . spawnSync ( updateInstallerExecutable , [ installLogFile , installConfigFile ] ) ;
2019-10-26 01:51:40 +02:00
if ( result . status != 0 ) {
console . error ( "Failed to execute update installer! Return code: %d" , result . status ) ;
dialog . showMessageBox ( {
buttons : [ "update now" , "remind me later" ] ,
title : "Update available" ,
message :
"Failed to execute update installer\n" +
"Installer exited with code " + result . status
} as MessageBoxOptions ) ;
}
} catch ( error ) {
console . error ( "Failed to execute update installer (%o)" , error ) ;
if ( "errno" in error ) {
const errno = error as ErrnoException ;
2020-07-28 20:01:25 +02:00
if ( errno . errno == os . constants . errno . EPERM ) {
2019-10-26 01:51:40 +02:00
dialog . showMessageBox ( {
buttons : [ "quit" ] ,
title : "Update execute failed" ,
message : "Failed to execute update installer. (No permissions)\nPlease execute the client with admin privileges!"
} as MessageBoxOptions ) ;
return ;
}
dialog . showMessageBox ( {
buttons : [ "quit" ] ,
title : "Update execute failed" ,
message : "Failed to execute update installer.\nError: " + errno . message
} as MessageBoxOptions ) ;
return ;
}
dialog . showMessageBox ( {
buttons : [ "quit" ] ,
title : "Update execute failed" ,
message : "Failed to execute update installer.\nLookup console for more detail"
} as MessageBoxOptions ) ;
return ;
}
if ( electron . app . hasSingleInstanceLock ( ) )
electron . app . releaseSingleInstanceLock ( ) ;
const ids = child_process . execSync ( "pgrep TeaClient" ) . toString ( ) . split ( os . EOL ) . map ( e = > e . trim ( ) ) . reverse ( ) . join ( " " ) ;
console . log ( "Executing %s" , "kill -9 " + ids ) ;
child_process . execSync ( "kill -9 " + ids ) ;
} ;
} else {
console . log ( "Executing update install on windows" ) ;
2020-12-02 18:08:49 +01:00
executeCallback = ( ) = > {
console . log ( "Executing command %s with args %o" , updateInstallerExecutable , [ installLogFile , installConfigFile ] ) ;
2019-10-27 22:39:59 +01:00
try {
2020-12-02 18:08:49 +01:00
const pipe = child_process . spawn ( updateInstallerExecutable , [ installLogFile , installConfigFile ] , {
2019-10-27 22:39:59 +01:00
detached : true ,
shell : true ,
cwd : path.dirname ( app . getAppPath ( ) ) ,
stdio : "ignore"
} ) ;
pipe . unref ( ) ;
app . quit ( ) ;
} catch ( error ) {
console . dir ( error ) ;
electron . dialog . showErrorBox ( "Failed to finalize update" , "Failed to finalize update.\nInvoking the update-installer.exe failed.\nLookup the console for more details." ) ;
}
2019-10-26 01:51:40 +02:00
} ;
}
2020-12-02 18:08:49 +01:00
callbackStats ( "Update successfully prepared" , 1 ) ;
callbackLog ( "info" , "Update successfully prepared" ) ;
2019-10-26 01:51:40 +02:00
2020-12-02 18:08:49 +01:00
return {
callbackExecute : executeCallback ,
callbackAbort : ( ) = > {
/* TODO: Cleanup */
2019-10-26 01:51:40 +02:00
}
}
}