2019-10-25 19:51:40 -04:00
import * as querystring from "querystring" ;
import * as request from "request" ;
import { app , dialog , ipcMain } from "electron" ;
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" ;
import { parse_version , Version } from "../../shared/version" ;
import Timer = NodeJS . Timer ;
import MessageBoxOptions = Electron . MessageBoxOptions ;
import { Headers } from "tar-stream" ;
import { Arguments , process_args } from "../../shared/process-arguments" ;
import * as electron from "electron" ;
import { PassThrough } from "stream" ;
import * as _main_windows from "../main_window" ;
import ErrnoException = NodeJS . ErrnoException ;
import { EPERM } from "constants" ;
import * as winmgr from "../window" ;
const is_debug = false ;
export function server_url ( ) : string {
const default_path = is_debug ? "http://localhost/home/TeaSpeak/TeaSpeak/Web-Client/client-api/environment/" : "http://clientapi.teaspeak.de/" ;
return process_args . has_value ( . . . Arguments . SERVER_URL ) ? process_args . value ( . . . Arguments . SERVER_URL ) : default_path ;
}
export interface UpdateVersion {
channel : string ;
platform : string ,
arch : string ;
version : Version ;
}
export interface UpdateData {
versions : UpdateVersion [ ] ;
updater_version : UpdateVersion ;
}
let version_cache : UpdateData = undefined ;
export async function load_data ( allow_cached : boolean = true ) : Promise < UpdateData > {
if ( version_cache && allow_cached ) return Promise . resolve ( version_cache ) ;
return new Promise < UpdateData > ( ( resolve , reject ) = > {
const request_url = server_url ( ) + "/api.php?" + querystring . stringify ( {
type : "update-info"
} ) ;
console . log ( "request: %s" , request_url ) ;
request . get ( request_url , {
timeout : 2000
} , ( error , response , body ) = > {
if ( ! response || response . statusCode != 200 ) {
let info ;
try {
info = JSON . parse ( body ) || { msg : error } ;
} catch ( e ) {
info = { msg : "!-- failed to parse json --!" } ;
}
setImmediate ( reject , "Invalid status code (" + ( response || { statusCode : - 1 } ) . statusCode + " | " + ( info || { msg : "undefined" } ) . msg + ")" ) ;
return ;
}
const data = JSON . parse ( body ) ;
if ( ! data ) {
setImmediate ( reject , "Invalid response" ) ;
return ;
}
if ( ! data [ "success" ] ) {
setImmediate ( reject , "Action failed (" + data [ "msg" ] + ")" ) ;
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" ] ) ;
if ( version . channel == 'updater' )
resp . updater_version = version ;
else
resp . versions . push ( version ) ;
}
}
resolve ( resp ) ;
} ) ;
} ) ;
}
export async function newest_version ( current_version : Version , channel? : string ) : Promise < UpdateVersion | undefined > {
if ( ! app . getAppPath ( ) . endsWith ( ".asar" ) ) {
throw "You cant run an update when you're executing the source code!" ;
}
const data = await load_data ( ) ;
let had_data = false ;
for ( const version of data . versions ) {
if ( version . arch == os . arch ( ) && version . platform == os . platform ( ) ) {
if ( ! channel || version . channel == channel ) {
if ( ! current_version || version . version . newer_than ( current_version ) )
return version ;
else
had_data = true ;
}
}
}
if ( ! had_data )
throw "Missing data" ;
return undefined ;
}
2019-10-27 17:39:59 -04:00
/ * *
* @param update_file The input file from where the update will get installed
* @return The target executable file
* /
export async function extract_updater ( update_file : string ) : Promise < string > {
2019-10-25 19:51:40 -04:00
if ( ! fs . existsSync ( update_file ) ) throw "Missing update file!" ;
2019-10-27 17:39:59 -04:00
let update_installer = app . getPath ( 'temp' ) + "/teaclient-update-installer-" + Math . random ( ) . toString ( 36 ) . substring ( 2 , 15 ) + Math . random ( ) . toString ( 36 ) . substring ( 2 , 15 ) ;
if ( os . platform ( ) == "win32" )
update_installer += ".exe" ;
2019-10-25 19:51:40 -04:00
const source = fs . createReadStream ( update_file ) ;
const extract = tar . extract ( ) ;
2019-10-27 17:39:59 -04:00
await new Promise ( ( resolve , reject ) = > {
2019-10-25 19:51:40 -04:00
let updater_found = false ;
source . on ( 'end' , ( ) = > {
if ( ! updater_found ) {
console . error ( "Failed to extract the updater (Updater hasn't been found!)" ) ;
2019-10-27 17:39:59 -04:00
reject ( "Updater hasn't been found in bundle" ) ;
2019-10-25 19:51:40 -04:00
}
2019-10-27 17:39:59 -04:00
2019-10-25 19:51:40 -04:00
resolve ( ) ;
} ) ;
extract . on ( 'entry' , ( header : Headers , stream , callback ) = > {
stream . on ( 'end' , callback ) ;
console . log ( "Got entry " + header . name ) ;
if ( header . name == "./update-installer" || header . name == "./update-installer.exe" ) {
2019-10-27 17:39:59 -04:00
console . log ( "Found updater! (" + header . size + " bytes)" ) ;
console . log ( "Extracting to %s" , update_installer ) ;
const s = fs . createWriteStream ( update_installer ) ;
2019-10-25 19:51:40 -04:00
stream . pipe ( s ) . on ( 'finish' , event = > {
console . log ( "Updater extracted and written!" ) ;
updater_found = true ;
resolve ( ) ;
2019-10-27 17:39:59 -04:00
} ) . on ( 'error' , event = > {
console . error ( "Failed write update file: %o" , event ) ;
reject ( "failed to write file" )
2019-10-25 19:51:40 -04:00
} ) ;
} else {
stream . resume ( ) ; //Drain the stream
}
} ) ;
source . pipe ( extract ) ;
} ) ;
2019-10-27 17:39:59 -04:00
return update_installer ;
2019-10-25 19:51:40 -04:00
}
export async function update_updater ( ) : Promise < void > {
//TODO here
return Promise . resolve ( ) ;
}
function data_directory ( ) : string {
return electron . app . getPath ( 'userData' ) ;
}
function get_update_file ( channel : string , version : Version ) : string {
let _path = fs . realpathSync ( data_directory ( ) ) ;
const name = channel + "_" + version . major + "_" + version . minor + "_" + version . patch + "_" + version . build + ".tar" ;
return path . join ( _path , "app_versions" , name ) ;
}
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)
}
}
export async function download_version ( channel : string , version : Version , status ? : ( state : ProgressState ) = > any ) : Promise < string > {
const target_path = get_update_file ( channel , version ) ;
console . log ( "Downloading version %s to %s" , version . toString ( false ) , target_path ) ;
if ( fs . existsSync ( target_path ) ) {
/* TODO test if this file is valid and can be used */
try {
await fs . remove ( target_path ) ;
} catch ( error ) {
throw "Failed to remove old file: " + error ;
}
}
try {
await fs . mkdirp ( path . dirname ( target_path ) ) ;
} catch ( error ) {
throw "Failed to make target directory: " + path . dirname ( target_path ) ;
}
const url = server_url ( ) + "/api.php?" + querystring . stringify ( {
type : "update-download" ,
platform : os.platform ( ) ,
arch : os.arch ( ) ,
version : version.toString ( ) ,
channel : channel
} ) ;
console . log ( "Downloading update from %s. (%s)" , server_url ( ) , url ) ;
return new Promise < string > ( ( resolve , reject ) = > {
let fired = false ;
let stream = progress ( request . get ( url , {
timeout : 2000
} , ( error , response , body ) = > {
if ( ! response || response . statusCode != 200 ) {
let info ;
try {
info = JSON . parse ( body )
} catch ( e ) {
info = { "msg" : "!-- failed to parse json --!" } ;
}
if ( ! fired && ( fired = true ) )
setImmediate ( reject , "Invalid status code (" + ( response || { statusCode : - 1 } ) . statusCode + "|" + ( info || { "msg" : "undefined" } ) . msg + ")" ) ;
return ;
}
} ) ) . on ( 'progress' , _state = > status ? status ( _state ) : { } ) . on ( 'error' , error = > {
console . warn ( "Encountered error within download pipe. Ignoring error: %o" , error ) ;
} ) . on ( 'end' , function ( ) {
console . log ( "Update downloaded successfully. Waiting for write stream to finish." ) ;
if ( status )
status ( {
percent : 1 ,
speed : 0 ,
size : { total : 0 , transferred : 0 } ,
time : { elapsed : 0 , remaining : 0 }
} )
} ) ;
console . log ( "Decompressing update package while streaming!" ) ;
stream = stream . pipe ( zlib . createGunzip ( ) ) ;
stream . pipe ( fs . createWriteStream ( target_path , {
autoClose : true
} ) ) . on ( 'finish' , ( ) = > {
console . log ( "Write stream has finished. Download successfully." ) ;
if ( ! fired && ( fired = true ) )
setImmediate ( resolve , target_path ) ;
} ) . on ( 'error' , error = > {
console . log ( "Write stream encountered an error while downloading update. Error: %o" , error ) ;
if ( ! fired && ( fired = true ) )
setImmediate ( reject , "failed to write" ) ;
} ) ;
} ) ;
}
if ( typeof ( String . prototype . trim ) === "undefined" )
{
String . prototype . trim = function ( )
{
return String ( this ) . replace ( /^\s+|\s+$/g , '' ) ;
} ;
}
export async function test_file_accessibility ( update_file : string ) : Promise < string [ ] > {
if ( os . platform ( ) === "win32" )
return [ ] ; /* within windows the update installer request admin privileges if required */
const original_fs = require ( 'original-fs' ) ;
if ( ! fs . existsSync ( update_file ) )
throw "Missing update file (" + update_file + ")" ;
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 ) ) ;
} ;
let code = await test_access ( update_file , original_fs . constants . R_OK ) ;
if ( code )
throw "Failed test read for update file. (" + update_file + " results in " + code . code + ")" ;
const fstream = original_fs . createReadStream ( update_file ) ;
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 [ ] ;
}
}
async function build_install_config ( source_root : string , target_root : string ) : Promise < install_config.ConfigFile > {
console . log ( "Building update install config for target directory: %s. Update source: %o" , target_root , source_root ) ;
const result : install_config.ConfigFile = { } as any ;
result . version = 1 ;
result . backup = true ;
{
const data = path . parse ( source_root ) ;
result [ "backup-directory" ] = path . join ( data . dir , data . name + "_backup" ) ;
}
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
}
] ;
const ignore_list = [
"update-installer.exe" , "update-installer"
] ;
const dir_walker = async ( relative_path : string ) = > {
const source_directory = path . join ( source_root , relative_path ) ;
const target_directory = path . join ( target_root , relative_path ) ;
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 ) {
let _exclude = false ;
for ( const exclude of ignore_list ) {
if ( exclude == file ) {
console . debug ( "Ignoring file to update (%s/%s)" , relative_path , file ) ;
_exclude = true ;
break ;
}
}
if ( _exclude )
continue ;
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 ( ) ) {
await dir_walker ( path . join ( relative_path , file ) ) ;
} else {
/* TODO: ensure its a file! */
result . moves . push ( {
"error-id" : "move-file-" + result . moves . length ,
source : source_file ,
target : target_file
} ) ;
}
}
} ;
await dir_walker ( "." ) ;
return result ;
}
export async function execute_update ( update_file : string , restart_callback : ( callback : ( ) = > void ) = > any ) : Promise < void > {
let application_path = app . getAppPath ( ) ;
if ( application_path . endsWith ( ".asar" ) ) {
console . log ( "App path points to ASAR file (Going up to root directory)" ) ;
application_path = await fs . realpath ( path . join ( application_path , ".." , ".." ) ) ;
} else if ( await fs . pathExists ( application_path ) && ( await fs . stat ( application_path ) ) . isFile ( ) )
application_path = path . dirname ( application_path ) ;
console . log ( "Located target app path: %s" , application_path ) ;
console . log ( "Using update file: %s" , update_file ) ;
const temp_directory = path . join ( app . getPath ( "temp" ) , "teaclient_update_" + Math . random ( ) . toString ( 36 ) . substring ( 7 ) ) ;
2019-10-27 17:39:59 -04:00
let updater_executable ;
2019-10-25 19:51:40 -04:00
{
console . log ( "Preparing update source directory at %s" , temp_directory ) ;
try {
await fs . mkdirp ( temp_directory )
} catch ( error ) {
console . error ( "failed to create update source directory: %o" , error ) ;
throw "failed to create update source directory" ;
}
const source = fs . createReadStream ( update_file ) ;
const extract = tar . extract ( ) ;
extract . on ( 'entry' , ( header : Headers , stream : PassThrough , callback ) = > {
const extract = async ( header : Headers , stream : PassThrough ) = > {
const target_file = path . join ( temp_directory , header . name ) ;
console . debug ( "Extracting entry %s of type %s to %s" , header . name , header . type , target_file ) ;
if ( header . type == "directory" ) {
await fs . mkdirp ( target_file ) ;
} else if ( header . type == "file" ) {
2019-10-27 17:39:59 -04:00
const target_finfo = path . parse ( target_file ) ;
2019-10-25 19:51:40 -04:00
{
2019-10-27 17:39:59 -04:00
const directory = target_finfo . dir ;
2019-10-25 19:51:40 -04:00
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 ) ;
}
}
}
const write_stream = ofs . createWriteStream ( target_file ) ;
try {
await new Promise ( ( resolve , reject ) = > {
stream . pipe ( write_stream )
. on ( 'error' , reject )
. on ( 'finish' , resolve ) ;
} ) ;
2019-10-27 17:39:59 -04:00
if ( target_finfo . name === "update-installer" || target_finfo . name === "update-installer.exe" ) {
updater_executable = target_file ;
console . log ( "Found update installer: %s" , target_file ) ;
}
2019-10-25 19:51:40 -04:00
return ; /* success */
} catch ( error ) {
console . error ( "Failed to extract update file %s: %o" , header . name , error ) ;
}
} 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 ( ) ;
} ) ;
} ) ;
source . 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" ;
}
}
2019-10-27 17:39:59 -04:00
if ( typeof ( updater_executable ) !== "string" || ! ( await fs . pathExists ( updater_executable ) ) )
throw "missing update installer executable within update package" ;
2019-10-25 19:51:40 -04:00
/* the "new" environment should now be available at 'temp_directory' */
console . log ( "Update unpacked successfully. Building update extractor file." ) ;
let install_config ;
try {
install_config = await build_install_config ( temp_directory , application_path ) ;
} catch ( error ) {
console . error ( "Failed to build update installer config: %o" , error ) ;
throw "failed to build update installer config" ;
}
const log_file = path . join ( temp_directory , "update-log.txt" ) ;
const config_file = path . join ( temp_directory , "update_install.json" ) ;
console . log ( "Writing config to %s" , config_file ) ;
try {
await fs . writeJSON ( config_file , install_config ) ;
} catch ( error ) {
console . error ( "Failed to write update install config file: %s" , error ) ;
throw "failed to write update install config file" ;
}
if ( os . platform ( ) == "linux" ) {
console . log ( "Executing update install on linux" ) ;
//We have to unpack it later
const rest_callback = ( ) = > {
2019-10-27 17:39:59 -04:00
console . log ( "Executing command %s with args %o" , updater_executable , [ log_file , config_file ] ) ;
2019-10-25 19:51:40 -04:00
try {
2019-10-27 17:39:59 -04:00
let result = child_process . spawnSync ( updater_executable , [ log_file , config_file ] ) ;
2019-10-25 19:51:40 -04: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 ;
if ( errno . errno == EPERM ) {
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 ) ;
} ;
restart_callback ( rest_callback ) ;
} else {
console . log ( "Executing update install on windows" ) ;
//We have to unpack it later
const rest_callback = ( ) = > {
2019-10-27 17:39:59 -04:00
console . log ( "Executing command %s with args %o" , updater_executable , [ log_file , config_file ] ) ;
try {
const pipe = child_process . spawn ( updater_executable , [ log_file , config_file ] , {
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-25 19:51:40 -04:00
} ;
restart_callback ( rest_callback ) ;
}
}
export async function current_version ( ) : Promise < Version > {
if ( process_args . has_value ( Arguments . UPDATER_LOCAL_VERSION ) )
return parse_version ( process_args . value ( Arguments . UPDATER_LOCAL_VERSION ) ) ;
let parent_path = app . getAppPath ( ) ;
if ( parent_path . endsWith ( ".asar" ) ) {
parent_path = path . join ( parent_path , ".." , ".." ) ;
parent_path = fs . realpathSync ( parent_path ) ;
}
try {
const info = await fs . readJson ( path . join ( parent_path , "app_version.json" ) ) ;
let result = parse_version ( info [ "version" ] ) ;
result . timestamp = info [ "timestamp" ] ;
return result ;
} catch ( error ) {
console . log ( "Got no version!" ) ;
return new Version ( 0 , 0 , 0 , 0 , 0 ) ;
}
}
async function minawait < T > ( object : Promise < T > , time : number ) : Promise < T > {
const begin = Date . now ( ) ;
const r = await object ;
const end = Date . now ( ) ;
if ( end - begin < time )
await new Promise ( resolve = > setTimeout ( resolve , time + begin - end ) ) ;
return r ;
}
export let update_restart_pending = false ;
export async function execute_graphical ( channel : string , ask_install : boolean ) : Promise < Boolean > {
const electron = require ( 'electron' ) ;
const ui_debug = process_args . has_flag ( Arguments . UPDATER_UI_DEBUG ) ;
const window = new electron . BrowserWindow ( {
show : false ,
width : ui_debug ? 1200 : 600 ,
height : ui_debug ? 800 : 400 ,
webPreferences : {
devTools : true ,
nodeIntegration : true ,
javascript : true
}
} ) ;
window . loadFile ( path . join ( path . dirname ( module .filename ) , "ui" , "index.html" ) ) ;
if ( ui_debug ) {
window . webContents . openDevTools ( ) ;
}
await new Promise ( resolve = > window . on ( 'ready-to-show' , resolve ) ) ;
window . show ( ) ;
await winmgr . apply_bounds ( 'update-installer' , window ) ;
winmgr . track_bounds ( 'update-installer' , window ) ;
const current_vers = await current_version ( ) ;
console . log ( "Current version: " + current_vers . toString ( true ) ) ;
console . log ( "Showed" ) ;
const set_text = text = > window . webContents . send ( 'status-update-text' , text ) ;
const set_error = text = > window . webContents . send ( 'status-error' , text ) ;
const set_progress = progress = > window . webContents . send ( 'status-update' , progress ) ;
const await_exit = ( ) = > { return new Promise ( resolve = > window . on ( 'closed' , resolve ) ) } ;
const await_version_confirm = version = > {
const id = "version-accept-" + Date . now ( ) ;
window . webContents . send ( 'status-confirm-update' , id , current_vers , version ) ;
return new Promise ( ( resolve , reject ) = > {
window . on ( 'closed' , ( ) = > resolve ( false ) ) ;
ipcMain . once ( id , ( event , result ) = > {
console . log ( "Got response %o" , result ) ;
resolve ( result ) ;
} ) ;
} ) ;
} ;
const await_confirm_execute = ( ) = > {
const id = "status-confirm-execute-" + Date . now ( ) ;
window . webContents . send ( 'status-confirm-execute' , id ) ;
return new Promise ( ( resolve , reject ) = > {
window . on ( 'closed' , ( ) = > resolve ( false ) ) ;
ipcMain . once ( id , ( event , result ) = > {
console . log ( "Got response %o" , result ) ;
resolve ( result ) ;
} ) ;
} ) ;
} ;
set_text ( "Loading data" ) ;
let version : UpdateVersion ;
try {
version = await minawait ( newest_version ( process_args . has_flag ( Arguments . UPDATER_ENFORCE ) ? undefined : current_vers , channel ) , 3000 ) ;
} catch ( error ) {
set_error ( "Failed to get newest information:<br>" + error ) ;
await await_exit ( ) ;
return false ;
}
console . log ( "Got version %o" , version ) ;
if ( ! version ) {
set_error ( "You're already on the newest version!" ) ;
await await_exit ( ) ;
return false ;
}
if ( ask_install ) {
try {
const test = await await_version_confirm ( version . version ) ;
if ( ! test ) {
window . close ( ) ;
return false ;
}
} catch ( error ) {
console . dir ( error ) ;
window . close ( ) ;
return false ;
}
}
set_text ( "Updating to version " + version . version . toString ( ) + "<br>Downloading...." ) ;
let update_path : string ;
try {
update_path = await download_version ( version . channel , version . version , status = > { setImmediate ( set_progress , status . percent ) ; } ) ;
} catch ( error ) {
set_error ( "Failed to download version: <br>" + error ) ;
console . error ( error ) ;
await await_exit ( ) ;
return false ;
}
try {
const inaccessible = await test_file_accessibility ( update_path ) ;
if ( inaccessible . length > 0 ) {
console . log ( "Failed to access the following files:" ) ;
for ( const fail of inaccessible )
console . log ( " - " + fail ) ;
if ( os . platform ( ) == "linux" ) {
set_error ( "Failed to access target files.<br>Please execute this app with administrator (sudo) privileges.<br>Use the following command:<br><p>" +
"sudo " + path . normalize ( app . getAppPath ( ) ) + " --update-execute=\"" + path . normalize ( update_path ) + "\"</p>" ) ;
await await_exit ( ) ;
return false ;
} else if ( os . platform ( ) == "win32" ) {
/* the updater asks for admin rights anyway :/ */
}
}
} catch ( error ) {
set_error ( "Failed to access target files.<br>You may need to execute the TeaClient as Administrator!<br>Error: " + error ) ;
await await_exit ( ) ;
return false ;
}
if ( ! await await_confirm_execute ( ) ) {
window . close ( ) ;
return false ;
}
set_text ( "Extracting update installer...<br>Please wait" ) ;
try {
await extract_updater ( update_path ) ;
} catch ( error ) {
console . error ( "Failed to update the updater! (%o)" , error ) ;
set_error ( "Failed to update the update installer.\nUpdate failed!" ) ;
await await_exit ( ) ;
return false ;
}
set_text ( "Executing update...<br>Please wait" ) ;
try {
await execute_update ( update_path , callback = > {
_main_windows . set_prevent_instant_close ( true ) ;
update_restart_pending = true ;
window . close ( ) ;
callback ( ) ;
} ) ;
} catch ( error ) {
dialog . showErrorBox ( "Update error" , "Failed to execute update!\n" + error ) ;
return false ;
}
return true ;
}
export let update_question_open = false ;
async function check_update ( channel : string ) {
let version : UpdateVersion ;
try {
version = await newest_version ( await current_version ( ) , channel ) ;
} catch ( error ) {
console . warn ( "failed check for newer versions!" ) ;
console . error ( error ) ;
return ;
}
2019-11-24 12:38:50 -05:00
if ( version && ! update_question_open ) {
2019-10-25 19:51:40 -04:00
update_question_open = true ;
dialog . showMessageBox ( {
buttons : [ "update now" , "remind me later" ] ,
title : "TeaClient: Update available" ,
message :
"There is an update available!\n" +
"Should we update now?\n" +
"\n" +
"Current version: " + ( await current_version ( ) ) . toString ( ) + "\n" +
"Target version: " + version . version . toString ( )
} as MessageBoxOptions ) . then ( result = > {
if ( result . response == 0 ) {
execute_graphical ( channel , false ) . then ( ( ) = > {
update_question_open = false ;
} ) ;
} else {
update_question_open = false ;
}
} ) ;
}
}
let update_task : Timer ;
export function start_auto_update_check() {
if ( update_task ) return ;
update_task = setInterval ( check_update , 2 * 60 * 60 * 1000 ) ;
setImmediate ( check_update ) ;
}
export function stop_auto_update_check() {
clearInterval ( update_task ) ;
update_task = undefined ;
}
export async function selected_channel ( ) : Promise < string > {
return process_args . has_value ( Arguments . UPDATER_CHANNEL ) ? process_args . value ( Arguments . UPDATER_CHANNEL ) : "release" ;
}