2019-05-11 13:03:38 +03:00
using System ;
2016-05-26 16:06:52 +03:00
using System.Collections.Generic ;
using System.Diagnostics ;
using System.IO ;
using System.Linq ;
2016-11-15 21:04:37 +03:00
using System.Reflection ;
2016-05-26 16:06:52 +03:00
using System.Runtime.InteropServices ;
using System.Text ;
using System.Threading ;
2016-06-06 13:48:53 +03:00
using System.Threading.Tasks ;
2016-05-26 16:06:52 +03:00
using System.Xml ;
2016-11-15 21:04:37 +03:00
using System.Xml.Xsl ;
2018-12-12 17:55:57 +03:00
using Xamarin ;
2017-06-06 23:32:25 +03:00
using Xamarin.Utils ;
2016-05-26 16:06:52 +03:00
namespace xharness
{
2016-11-16 17:23:11 +03:00
public enum AppRunnerTarget
{
None ,
Simulator_iOS ,
Simulator_iOS32 ,
Simulator_iOS64 ,
Simulator_tvOS ,
Simulator_watchOS ,
Device_iOS ,
Device_tvOS ,
Device_watchOS ,
}
2017-01-04 21:45:26 +03:00
public enum Extension
{
WatchKit2 ,
TodayExtension ,
}
2016-05-26 16:06:52 +03:00
public class AppRunner
{
public Harness Harness ;
public string ProjectFile ;
2017-01-04 21:45:26 +03:00
public string AppPath ;
2020-02-11 22:03:16 +03:00
public string Variation ;
2016-05-26 16:06:52 +03:00
2016-06-06 13:48:53 +03:00
public TestExecutingResult Result { get ; private set ; }
2017-02-08 13:44:20 +03:00
public string FailureMessage { get ; private set ; }
2016-05-26 16:06:52 +03:00
string appName ;
string appPath ;
string launchAppPath ;
string bundle_identifier ;
string platform ;
2017-01-04 21:45:26 +03:00
Extension ? extension ;
2016-05-26 16:06:52 +03:00
bool isSimulator ;
string device_name ;
string companion_device_name ;
2017-01-04 21:45:26 +03:00
string configuration ;
public string Configuration {
get { return configuration ? ? Harness . Configuration ; }
set { configuration = value ; }
}
public string DeviceName {
get { return device_name ; }
set { device_name = value ; }
}
public string CompanionDeviceName {
get { return companion_device_name ; }
set { companion_device_name = value ; }
}
public bool isExtension {
get {
return extension . HasValue ;
}
}
2019-05-30 18:40:56 +03:00
public double TimeoutMultiplier { get ; set ; } = 1 ;
2016-06-06 13:48:53 +03:00
// For watch apps we end up with 2 simulators, the watch simulator (the main one), and the iphone simulator (the companion one).
SimDevice [ ] simulators ;
SimDevice simulator { get { return simulators [ 0 ] ; } }
SimDevice companion_simulator { get { return simulators . Length = = 2 ? simulators [ 1 ] : null ; } }
2016-11-16 17:23:11 +03:00
AppRunnerTarget target ;
public AppRunnerTarget Target {
get { return target = = AppRunnerTarget . None ? Harness . Target : target ; }
2016-06-06 13:48:53 +03:00
set { target = value ; }
2016-05-26 16:06:52 +03:00
}
2016-06-06 13:48:53 +03:00
string log_directory ;
public string LogDirectory {
get { return log_directory ? ? Harness . LogDirectory ; }
set { log_directory = value ; }
2016-05-26 16:06:52 +03:00
}
2017-11-28 16:27:31 +03:00
Logs logs ;
public Logs Logs {
get {
return logs ? ? ( logs = new Logs ( LogDirectory ) ) ;
}
}
2016-06-17 18:21:18 +03:00
2017-11-28 16:27:31 +03:00
Log main_log ;
2016-06-17 18:21:18 +03:00
public Log MainLog {
get { return main_log ; }
set { main_log = value ; }
}
2016-05-26 16:06:52 +03:00
2016-06-06 13:48:53 +03:00
public SimDevice [ ] Simulators {
get { return simulators ; }
set { simulators = value ; }
}
2016-05-26 16:06:52 +03:00
2016-06-17 18:21:18 +03:00
public string BundleIdentifier {
get {
return bundle_identifier ;
2016-05-26 16:06:52 +03:00
}
}
2016-06-17 18:21:18 +03:00
string mode ;
2016-11-16 17:23:11 +03:00
async Task < bool > FindSimulatorAsync ( )
2016-05-26 16:06:52 +03:00
{
2016-06-06 13:48:53 +03:00
if ( simulators ! = null )
2016-11-16 17:23:11 +03:00
return true ;
2016-06-06 13:48:53 +03:00
2016-06-16 04:57:47 +03:00
var sims = new Simulators ( ) {
Harness = Harness ,
} ;
2018-10-10 09:05:26 +03:00
await sims . LoadAsync ( Logs . Create ( $"simulator-list-{Harness.Timestamp}.log" , "Simulator list" ) ) ;
2016-11-16 21:06:26 +03:00
simulators = await sims . FindAsync ( Target , main_log ) ;
2016-05-26 16:06:52 +03:00
2016-11-16 17:23:11 +03:00
return simulators ! = null ;
2016-05-26 16:06:52 +03:00
}
void FindDevice ( )
{
if ( device_name ! = null )
return ;
device_name = Environment . GetEnvironmentVariable ( "DEVICE_NAME" ) ;
if ( ! string . IsNullOrEmpty ( device_name ) )
return ;
2016-06-17 18:21:18 +03:00
var devs = new Devices ( ) {
Harness = Harness ,
} ;
2016-06-06 13:48:53 +03:00
Task . Run ( async ( ) = >
{
2016-06-17 18:21:18 +03:00
await devs . LoadAsync ( main_log ) ;
2016-06-06 13:48:53 +03:00
} ) . Wait ( ) ;
2016-05-26 16:06:52 +03:00
2016-06-06 13:48:53 +03:00
string [ ] deviceClasses ;
switch ( mode ) {
case "ios" :
2018-01-30 19:11:04 +03:00
deviceClasses = new string [ ] { "iPhone" , "iPad" , "iPod" } ;
2016-06-06 13:48:53 +03:00
break ;
case "watchos" :
deviceClasses = new string [ ] { "Watch" } ;
break ;
case "tvos" :
deviceClasses = new string [ ] { "AppleTV" } ; // Untested
break ;
default :
throw new Exception ( $"unknown mode: {mode}" ) ;
}
2016-05-26 16:06:52 +03:00
2018-02-12 16:43:16 +03:00
var selected = devs . ConnectedDevices . Where ( ( v ) = > deviceClasses . Contains ( v . DeviceClass ) & & v . IsUsableForDebugging ! = false ) ;
2016-06-06 13:48:53 +03:00
Device selected_data ;
if ( selected . Count ( ) = = 0 ) {
throw new Exception ( $"Could not find any applicable devices with device class(es): {string.Join (" , ", deviceClasses)}" ) ;
} else if ( selected . Count ( ) > 1 ) {
2016-12-22 20:55:25 +03:00
selected_data = selected
. OrderBy ( ( dev ) = >
{
Version v ;
if ( Version . TryParse ( dev . ProductVersion , out v ) )
return v ;
return new Version ( ) ;
} )
. First ( ) ;
main_log . WriteLine ( "Found {0} devices for device class(es) '{1}': '{2}'. Selected: '{3}' (because it has the lowest version)." , selected . Count ( ) , string . Join ( "', '" , deviceClasses ) , string . Join ( "', '" , selected . Select ( ( v ) = > v . Name ) . ToArray ( ) ) , selected_data . Name ) ;
2016-06-06 13:48:53 +03:00
} else {
selected_data = selected . First ( ) ;
}
device_name = selected_data . Name ;
2017-01-04 21:45:26 +03:00
if ( mode = = "watchos" )
companion_device_name = devs . FindCompanionDevice ( main_log , selected_data ) . Name ;
2016-06-06 13:48:53 +03:00
}
2016-06-07 19:49:20 +03:00
bool initialized ;
public void Initialize ( )
2016-05-26 16:06:52 +03:00
{
2016-06-07 19:49:20 +03:00
if ( initialized )
return ;
initialized = true ;
2016-05-26 16:06:52 +03:00
var csproj = new XmlDocument ( ) ;
csproj . LoadWithoutNetworkAccess ( ProjectFile ) ;
appName = csproj . GetAssemblyName ( ) ;
var info_plist_path = csproj . GetInfoPListInclude ( ) ;
var info_plist = new XmlDocument ( ) ;
2017-01-04 21:45:26 +03:00
info_plist . LoadWithoutNetworkAccess ( Path . Combine ( Path . GetDirectoryName ( ProjectFile ) , info_plist_path . Replace ( '\\' , '/' ) ) ) ;
2016-05-26 16:06:52 +03:00
bundle_identifier = info_plist . GetCFBundleIdentifier ( ) ;
2017-01-04 21:45:26 +03:00
var extensionPointIdentifier = info_plist . GetNSExtensionPointIdentifier ( ) ;
if ( ! string . IsNullOrEmpty ( extensionPointIdentifier ) )
extension = extensionPointIdentifier . ParseFromNSExtensionPointIdentifier ( ) ;
2016-06-06 13:48:53 +03:00
switch ( Target ) {
2016-11-16 17:23:11 +03:00
case AppRunnerTarget . Simulator_iOS32 :
2016-05-26 16:06:52 +03:00
mode = "sim32" ;
platform = "iPhoneSimulator" ;
isSimulator = true ;
break ;
2016-11-16 17:23:11 +03:00
case AppRunnerTarget . Simulator_iOS64 :
2016-05-26 16:06:52 +03:00
mode = "sim64" ;
platform = "iPhoneSimulator" ;
isSimulator = true ;
break ;
2016-11-16 17:23:11 +03:00
case AppRunnerTarget . Simulator_iOS :
2016-05-26 16:06:52 +03:00
mode = "classic" ;
platform = "iPhoneSimulator" ;
isSimulator = true ;
break ;
2016-11-16 17:23:11 +03:00
case AppRunnerTarget . Device_iOS :
2016-05-26 16:06:52 +03:00
mode = "ios" ;
platform = "iPhone" ;
isSimulator = false ;
break ;
2016-11-16 17:23:11 +03:00
case AppRunnerTarget . Simulator_tvOS :
2016-05-26 16:06:52 +03:00
mode = "tvos" ;
platform = "iPhoneSimulator" ;
isSimulator = true ;
break ;
2016-11-16 17:23:11 +03:00
case AppRunnerTarget . Device_tvOS :
2016-05-26 16:06:52 +03:00
mode = "tvos" ;
platform = "iPhone" ;
isSimulator = false ;
break ;
2016-11-16 17:23:11 +03:00
case AppRunnerTarget . Simulator_watchOS :
2016-05-26 16:06:52 +03:00
mode = "watchos" ;
platform = "iPhoneSimulator" ;
isSimulator = true ;
break ;
2016-11-16 17:23:11 +03:00
case AppRunnerTarget . Device_watchOS :
2016-05-26 16:06:52 +03:00
mode = "watchos" ;
platform = "iPhone" ;
isSimulator = false ;
break ;
default :
throw new Exception ( string . Format ( "Unknown target: {0}" , Harness . Target ) ) ;
}
2017-01-04 21:45:26 +03:00
appPath = Path . Combine ( Path . GetDirectoryName ( ProjectFile ) , csproj . GetOutputPath ( platform , Configuration ) . Replace ( '\\' , '/' ) , appName + ( isExtension ? ".appex" : ".app" ) ) ;
2016-05-26 16:06:52 +03:00
if ( ! Directory . Exists ( appPath ) )
throw new Exception ( string . Format ( "The app directory {0} does not exist. This is probably a bug in the test harness." , appPath ) ) ;
if ( mode = = "watchos" ) {
launchAppPath = Directory . GetDirectories ( Path . Combine ( appPath , "Watch" ) , "*.app" ) [ 0 ] ;
} else {
launchAppPath = appPath ;
}
}
2019-02-20 08:33:30 +03:00
public async Task < ProcessExecutionResult > InstallAsync ( CancellationToken cancellation_token )
2016-05-26 16:06:52 +03:00
{
Initialize ( ) ;
if ( isSimulator ) {
// We reset the simulator when running, so a separate install step does not make much sense.
throw new Exception ( "Installing to a simulator is not supported." ) ;
}
FindDevice ( ) ;
2019-10-14 17:18:46 +03:00
var args = new List < string > ( ) ;
if ( ! string . IsNullOrEmpty ( Harness . XcodeRoot ) ) {
args . Add ( "--sdkroot" ) ;
args . Add ( Harness . XcodeRoot ) ;
}
2016-05-26 16:06:52 +03:00
for ( int i = - 1 ; i < Harness . Verbosity ; i + + )
2019-10-14 17:18:46 +03:00
args . Add ( "-v" ) ;
2016-05-26 16:06:52 +03:00
2019-10-14 17:18:46 +03:00
args . Add ( "--installdev" ) ;
args . Add ( appPath ) ;
2016-05-26 16:06:52 +03:00
AddDeviceName ( args , companion_device_name ? ? device_name ) ;
2019-10-14 17:18:46 +03:00
if ( mode = = "watchos" ) {
args . Add ( "--device" ) ;
args . Add ( "ios,watchos" ) ;
}
2016-05-26 16:06:52 +03:00
2019-02-20 08:33:30 +03:00
var totalSize = Directory . GetFiles ( appPath , "*" , SearchOption . AllDirectories ) . Select ( ( v ) = > new FileInfo ( v ) . Length ) . Sum ( ) ;
main_log . WriteLine ( $"Installing '{appPath}' to '{companion_device_name ?? device_name}'. Size: {totalSize} bytes = {totalSize / 1024.0 / 1024.0:N2} MB" ) ;
2017-10-12 18:08:54 +03:00
2019-10-14 17:18:46 +03:00
return await ProcessHelper . ExecuteCommandAsync ( Harness . MlaunchPath , args , main_log , TimeSpan . FromHours ( 1 ) , cancellation_token : cancellation_token ) ;
2016-05-26 16:06:52 +03:00
}
2017-01-04 21:45:26 +03:00
public async Task < ProcessExecutionResult > UninstallAsync ( )
2017-01-02 10:58:46 +03:00
{
Initialize ( ) ;
if ( isSimulator )
throw new Exception ( "Uninstalling from a simulator is not supported." ) ;
FindDevice ( ) ;
2019-10-14 17:18:46 +03:00
var args = new List < string > ( ) ;
if ( ! string . IsNullOrEmpty ( Harness . XcodeRoot ) ) {
args . Add ( "--sdkroot" ) ;
args . Add ( Harness . XcodeRoot ) ;
}
2017-01-02 10:58:46 +03:00
for ( int i = - 1 ; i < Harness . Verbosity ; i + + )
2019-10-14 17:18:46 +03:00
args . Add ( "-v" ) ;
2017-01-02 10:58:46 +03:00
2019-10-14 17:18:46 +03:00
args . Add ( "--uninstalldevbundleid" ) ;
args . Add ( bundle_identifier ) ;
2017-01-02 10:58:46 +03:00
AddDeviceName ( args , companion_device_name ? ? device_name ) ;
2019-10-14 17:18:46 +03:00
return await ProcessHelper . ExecuteCommandAsync ( Harness . MlaunchPath , args , main_log , TimeSpan . FromMinutes ( 1 ) ) ;
2017-01-02 10:58:46 +03:00
}
2016-06-17 18:21:18 +03:00
bool ensure_clean_simulator_state = true ;
public bool EnsureCleanSimulatorState {
2016-05-26 16:06:52 +03:00
get {
2016-06-28 20:34:29 +03:00
return ensure_clean_simulator_state & & string . IsNullOrEmpty ( Environment . GetEnvironmentVariable ( "SKIP_SIMULATOR_SETUP" ) ) ;
2016-06-06 13:48:53 +03:00
}
set {
2016-06-17 18:21:18 +03:00
ensure_clean_simulator_state = value ;
2016-06-06 13:48:53 +03:00
}
}
2018-11-16 21:31:40 +03:00
2020-02-04 19:03:38 +03:00
( string resultLine , bool failed , bool crashed ) ParseResult ( string test_log_path , bool timed_out , bool crashed )
2016-11-15 21:04:37 +03:00
{
2020-02-04 19:03:38 +03:00
if ( ! File . Exists ( test_log_path ) )
2019-05-23 11:04:37 +03:00
return ( null , false , true ) ; // if we do not have a log file, the test crashes
2017-06-26 18:34:32 +03:00
// parsing the result is different if we are in jenkins or not.
// When in Jenkins, Touch.Unit produces an xml file instead of a console log (so that we can get better test reporting).
// However, for our own reporting, we still want the console-based log. This log is embedded inside the xml produced
// by Touch.Unit, so we need to extract it and write it to disk. We also need to re-save the xml output, since Touch.Unit
// wraps the NUnit xml output with additional information, which we need to unwrap so that Jenkins understands it.
2019-05-23 11:04:37 +03:00
//
// On the other hand, the nunit and xunit do not have that data and have to be parsed.
2020-01-28 22:44:34 +03:00
//
// This if statement has a small trick, we found out that internet sharing in some of the bots (VSTS) does not work, in
// that case, we cannot do a TCP connection to xharness to get the log, this is a problem since if we did not get the xml
// from the TCP connection, we are going to fail when trying to read it and not parse it. Therefore, we are not only
// going to check if we are in CI, but also if the listener_log is valid.
2020-02-04 19:03:38 +03:00
var path = Path . ChangeExtension ( test_log_path , "xml" ) ;
2020-02-06 05:23:44 +03:00
XmlResultParser . CleanXml ( test_log_path , path ) ;
2019-05-23 11:04:37 +03:00
2020-02-06 05:23:44 +03:00
if ( Harness . InCI & & XmlResultParser . IsValidXml ( path , out var xmlType ) ) {
2020-02-04 19:03:38 +03:00
( string resultLine , bool failed , bool crashed ) parseResult = ( null , false , false ) ;
2016-11-15 21:04:37 +03:00
crashed = false ;
2017-06-26 18:34:32 +03:00
try {
2020-02-06 05:23:44 +03:00
var newFilename = XmlResultParser . GetXmlFilePath ( path , xmlType ) ;
2020-02-11 22:03:16 +03:00
// at this point, we have the test results, but we want to be able to have attachments in vsts, so if the format is
// the right one (NUnitV3) add the nodes. ATM only TouchUnit uses V3.
var testRunName = $"{appName} {Variation}" ;
if ( xmlType = = XmlResultParser . Jargon . NUnitV3 ) {
// add the attachments and write in the new filename
XmlResultParser . UpdateMissingData ( path , newFilename , testRunName , Directory . GetFiles ( Logs . Directory ) ) ;
} else {
// rename the path to the correct value
File . Move ( path , newFilename ) ;
}
2020-02-06 05:23:44 +03:00
path = newFilename ;
// write the human readable results in a tmp file, which we later use to step on the logs
var tmpFile = Path . Combine ( Path . GetTempPath ( ) , Guid . NewGuid ( ) . ToString ( ) ) ;
( parseResult . resultLine , parseResult . failed ) = XmlResultParser . GenerateHumanReadableResults ( path , tmpFile , xmlType ) ;
File . Copy ( tmpFile , test_log_path , true ) ;
File . Delete ( tmpFile ) ;
2020-02-04 19:03:38 +03:00
// we do not longer need the tmp file
2020-02-11 14:42:21 +03:00
Logs . AddFile ( path , Log . XML_LOG ) ;
2019-05-23 11:04:37 +03:00
return parseResult ;
2020-02-06 05:23:44 +03:00
2017-06-26 18:34:32 +03:00
} catch ( Exception e ) {
main_log . WriteLine ( "Could not parse xml result file: {0}" , e ) ;
2020-02-04 19:03:38 +03:00
// print file for better debugging
main_log . WriteLine ( "File data is:" ) ;
main_log . WriteLine ( new string ( '#' , 10 ) ) ;
using ( var stream = new StreamReader ( path ) ) {
string line ;
while ( ( line = stream . ReadLine ( ) ) ! = null ) {
main_log . WriteLine ( line ) ;
}
}
main_log . WriteLine ( new string ( '#' , 10 ) ) ;
main_log . WriteLine ( "End of xml results." ) ;
2017-06-26 18:34:32 +03:00
if ( timed_out ) {
Harness . LogWrench ( $"@MonkeyWrench: AddSummary: <b><i>{mode} timed out</i></b><br/>" ) ;
2019-05-23 11:04:37 +03:00
return parseResult ;
2016-11-15 21:04:37 +03:00
} else {
2017-06-26 18:34:32 +03:00
Harness . LogWrench ( $"@MonkeyWrench: AddSummary: <b><i>{mode} crashed</i></b><br/>" ) ;
main_log . WriteLine ( "Test run crashed" ) ;
crashed = true ;
2019-05-23 11:04:37 +03:00
parseResult . crashed = true ;
return parseResult ;
2016-11-15 21:04:37 +03:00
}
}
2019-05-11 13:03:38 +03:00
2019-05-23 11:04:37 +03:00
} else {
2020-02-04 19:03:38 +03:00
// delete not needed copy
File . Delete ( path ) ;
2019-05-23 11:04:37 +03:00
// not the most efficient way but this just happens when we run
// the tests locally and we usually do not run all tests, we are
// more interested to be efficent on the bots
string resultLine = null ;
2020-02-04 19:03:38 +03:00
using ( var reader = new StreamReader ( test_log_path ) ) {
2019-05-23 11:04:37 +03:00
string line = null ;
bool failed = false ;
2019-05-11 13:03:38 +03:00
while ( ( line = reader . ReadLine ( ) ) ! = null )
{
if ( line . Contains ( "Tests run:" ) ) {
Console . WriteLine ( line ) ;
2019-05-23 11:04:37 +03:00
resultLine = line ;
2019-05-11 13:03:38 +03:00
break ;
} else if ( line . Contains ( "[FAIL]" ) ) {
Console . WriteLine ( line ) ;
failed = true ;
}
2016-11-15 21:04:37 +03:00
}
2019-05-23 11:04:37 +03:00
return ( resultLine , failed , false ) ;
2017-06-26 18:34:32 +03:00
}
2019-05-23 11:04:37 +03:00
}
}
2017-06-26 18:34:32 +03:00
2020-02-04 19:03:38 +03:00
public bool TestsSucceeded ( string test_log_path , bool timed_out , bool crashed )
2019-05-23 11:04:37 +03:00
{
2020-02-04 19:03:38 +03:00
var ( resultLine , failed , crashed_out ) = ParseResult ( test_log_path , timed_out , crashed ) ;
2019-05-23 11:04:37 +03:00
// read the parsed logs in a human readable way
if ( resultLine ! = null ) {
2019-05-24 11:25:00 +03:00
var tests_run = resultLine . Replace ( "Tests run: " , "" ) ;
2017-06-26 18:34:32 +03:00
if ( failed ) {
Harness . LogWrench ( "@MonkeyWrench: AddSummary: <b>{0} failed: {1}</b><br/>" , mode , tests_run ) ;
main_log . WriteLine ( "Test run failed" ) ;
2016-11-15 21:04:37 +03:00
return false ;
} else {
2017-06-26 18:34:32 +03:00
Harness . LogWrench ( "@MonkeyWrench: AddSummary: {0} succeeded: {1}<br/>" , mode , tests_run ) ;
main_log . WriteLine ( "Test run succeeded" ) ;
return true ;
2016-11-15 21:04:37 +03:00
}
2017-06-26 18:34:32 +03:00
} else if ( timed_out ) {
Harness . LogWrench ( "@MonkeyWrench: AddSummary: <b><i>{0} timed out</i></b><br/>" , mode ) ;
return false ;
} else {
Harness . LogWrench ( "@MonkeyWrench: AddSummary: <b><i>{0} crashed</i></b><br/>" , mode ) ;
main_log . WriteLine ( "Test run crashed" ) ;
crashed = true ;
return false ;
2016-11-15 21:04:37 +03:00
}
}
2016-11-04 14:13:20 +03:00
[DllImport ("/usr/lib/libc.dylib")]
extern static IntPtr ttyname ( int filedes ) ;
2016-06-17 18:21:18 +03:00
public async Task < int > RunAsync ( )
2016-05-26 16:06:52 +03:00
{
2016-07-14 22:31:55 +03:00
CrashReportSnapshot crash_reports ;
2017-11-28 16:27:31 +03:00
Log device_system_log = null ;
Log listener_log = null ;
2016-06-17 18:21:18 +03:00
Log run_log = main_log ;
2016-05-26 16:06:52 +03:00
Initialize ( ) ;
2016-10-11 20:30:11 +03:00
if ( ! isSimulator )
FindDevice ( ) ;
2019-10-14 17:18:46 +03:00
crash_reports = new CrashReportSnapshot ( ) {
2016-10-11 20:30:11 +03:00
Device = ! isSimulator ,
DeviceName = device_name ,
Harness = Harness ,
Log = main_log ,
Logs = Logs ,
LogDirectory = LogDirectory ,
} ;
2016-07-14 22:31:55 +03:00
2019-10-14 17:18:46 +03:00
var args = new List < string > ( ) ;
if ( ! string . IsNullOrEmpty ( Harness . XcodeRoot ) ) {
args . Add ( "--sdkroot" ) ;
args . Add ( Harness . XcodeRoot ) ;
}
2016-05-26 16:06:52 +03:00
for ( int i = - 1 ; i < Harness . Verbosity ; i + + )
2019-10-14 17:18:46 +03:00
args . Add ( "-v" ) ;
args . Add ( "-argument=-connection-mode" ) ;
args . Add ( "-argument=none" ) ; // This will prevent the app from trying to connect to any IDEs
args . Add ( "-argument=-app-arg:-autostart" ) ;
args . Add ( "-setenv=NUNIT_AUTOSTART=true" ) ;
args . Add ( "-argument=-app-arg:-autoexit" ) ;
args . Add ( "-setenv=NUNIT_AUTOEXIT=true" ) ;
args . Add ( "-argument=-app-arg:-enablenetwork" ) ;
args . Add ( "-setenv=NUNIT_ENABLE_NETWORK=true" ) ;
2016-11-15 21:04:37 +03:00
// detect if we are using a jenkins bot.
2020-01-24 02:28:12 +03:00
var useXmlOutput = Harness . InCI ;
2017-07-11 09:58:47 +03:00
if ( useXmlOutput ) {
2019-10-14 17:18:46 +03:00
args . Add ( "-setenv=NUNIT_ENABLE_XML_OUTPUT=true" ) ;
args . Add ( "-setenv=NUNIT_ENABLE_XML_MODE=wrapped" ) ;
2020-02-11 22:03:16 +03:00
args . Add ( "-setenv=NUNIT_XML_VERSION=nunitv3" ) ;
2017-07-11 09:58:47 +03:00
}
2016-11-15 21:04:37 +03:00
2019-07-01 20:02:56 +03:00
if ( Harness . InCI ) {
// We use the 'BUILD_REVISION' variable to detect whether we're running CI or not.
2019-10-14 17:18:46 +03:00
args . Add ( $"-setenv=BUILD_REVISION=${Environment.GetEnvironmentVariable (" BUILD_REVISION ")}" ) ;
2019-07-01 20:02:56 +03:00
}
2019-07-12 17:05:57 +03:00
if ( ! Harness . GetIncludeSystemPermissionTests ( TestPlatform . iOS , ! isSimulator ) )
2019-10-14 17:18:46 +03:00
args . Add ( "-setenv=DISABLE_SYSTEM_PERMISSION_TESTS=1" ) ;
2017-08-03 19:58:04 +03:00
2016-05-26 16:06:52 +03:00
if ( isSimulator ) {
2019-10-14 17:18:46 +03:00
args . Add ( "-argument=-app-arg:-hostname:127.0.0.1" ) ;
args . Add ( "-setenv=NUNIT_HOSTNAME=127.0.0.1" ) ;
2016-05-26 16:06:52 +03:00
} else {
var ips = new StringBuilder ( ) ;
var ipAddresses = System . Net . Dns . GetHostEntry ( System . Net . Dns . GetHostName ( ) ) . AddressList ;
for ( int i = 0 ; i < ipAddresses . Length ; i + + ) {
if ( i > 0 )
ips . Append ( ',' ) ;
ips . Append ( ipAddresses [ i ] . ToString ( ) ) ;
}
2019-10-14 17:18:46 +03:00
args . Add ( $"-argument=-app-arg:-hostname:{ips.ToString ()}" ) ;
args . Add ( $"-setenv=NUNIT_HOSTNAME={ips.ToString ()}" ) ;
2016-05-26 16:06:52 +03:00
}
2017-06-26 13:47:37 +03:00
string transport ;
if ( mode = = "watchos" ) {
transport = isSimulator ? "FILE" : "HTTP" ;
} else {
transport = "TCP" ;
}
2019-10-14 17:18:46 +03:00
args . Add ( $"-argument=-app-arg:-transport:{transport}" ) ;
args . Add ( $"-setenv=NUNIT_TRANSPORT={transport}" ) ;
2016-05-26 16:06:52 +03:00
2020-02-11 14:42:21 +03:00
listener_log = Logs . Create ( $"test-{mode}-{Harness.Timestamp}.log" , Log . TEST_LOG , timestamp : ! useXmlOutput ) ;
2017-06-26 13:47:37 +03:00
2016-05-26 16:06:52 +03:00
SimpleListener listener ;
switch ( transport ) {
2017-06-26 13:47:37 +03:00
case "FILE" :
var fn = listener_log . FullPath + ".tmp" ;
listener = new SimpleFileListener ( fn ) ;
2019-10-14 17:18:46 +03:00
args . Add ( $"-setenv=NUNIT_LOG_FILE={fn}" ) ;
2017-06-26 13:47:37 +03:00
break ;
2016-05-26 16:06:52 +03:00
case "HTTP" :
listener = new SimpleHttpListener ( ) ;
break ;
case "TCP" :
listener = new SimpleTcpListener ( ) ;
break ;
default :
throw new NotImplementedException ( ) ;
}
2016-06-18 11:35:57 +03:00
listener . TestLog = listener_log ;
listener . Log = main_log ;
2016-05-26 16:06:52 +03:00
listener . AutoExit = true ;
listener . Address = System . Net . IPAddress . Any ;
2017-06-26 13:47:37 +03:00
listener . XmlOutput = useXmlOutput ;
2016-05-26 16:06:52 +03:00
listener . Initialize ( ) ;
2019-10-14 17:18:46 +03:00
args . Add ( $"-argument=-app-arg:-hostport:{listener.Port}" ) ;
args . Add ( $"-setenv=NUNIT_HOSTPORT={listener.Port}" ) ;
2016-05-26 16:06:52 +03:00
2017-01-04 21:45:26 +03:00
listener . StartAsync ( ) ;
var cancellation_source = new CancellationTokenSource ( ) ;
var timed_out = false ;
2019-02-11 19:36:13 +03:00
listener . ConnectedTask
. TimeoutAfter ( TimeSpan . FromMinutes ( Harness . LaunchTimeout ) )
. ContinueWith ( ( v ) = > {
if ( v . IsFaulted ) {
main_log . WriteLine ( "Test launch failed: {0}" , v . Exception ) ;
} else if ( v . IsCanceled ) {
main_log . WriteLine ( "Test launch was cancelled." ) ;
} else if ( v . Result ) {
main_log . WriteLine ( "Test run started" ) ;
} else {
cancellation_source . Cancel ( ) ;
main_log . WriteLine ( "Test launch timed out after {0} minute(s)." , Harness . LaunchTimeout ) ;
timed_out = true ;
}
} ) . DoNotAwait ( ) ;
2017-01-04 21:45:26 +03:00
2016-08-05 23:28:13 +03:00
foreach ( var kvp in Harness . EnvironmentVariables )
2019-10-14 17:18:46 +03:00
args . Add ( $"-setenv={kvp.Key}={kvp.Value}" ) ;
2016-08-05 23:28:13 +03:00
2016-05-26 16:06:52 +03:00
bool? success = null ;
2016-11-02 17:43:55 +03:00
bool launch_failure = false ;
2016-05-26 16:06:52 +03:00
2017-01-04 21:45:26 +03:00
if ( isExtension ) {
switch ( extension ) {
case Extension . TodayExtension :
2019-10-14 17:18:46 +03:00
args . Add ( isSimulator ? "--launchsimbundleid" : "--launchdevbundleid" ) ;
args . Add ( "todayviewforextensions:" + BundleIdentifier ) ;
args . Add ( "--observe-extension" ) ;
args . Add ( launchAppPath ) ;
2017-01-04 21:45:26 +03:00
break ;
case Extension . WatchKit2 :
default :
throw new NotImplementedException ( ) ;
}
} else {
2019-10-14 17:18:46 +03:00
args . Add ( isSimulator ? "--launchsim" : "--launchdev" ) ;
args . Add ( launchAppPath ) ;
2017-01-04 21:45:26 +03:00
}
2017-10-10 12:02:24 +03:00
if ( ! isSimulator )
2019-10-14 17:18:46 +03:00
args . Add ( "--disable-memory-limits" ) ;
2017-01-04 21:45:26 +03:00
2019-05-30 18:40:56 +03:00
var timeout = TimeSpan . FromMinutes ( Harness . Timeout * TimeoutMultiplier ) ;
2016-05-26 16:06:52 +03:00
if ( isSimulator ) {
2016-11-16 17:23:11 +03:00
if ( ! await FindSimulatorAsync ( ) )
return 1 ;
2016-06-17 18:21:18 +03:00
2016-11-04 14:13:20 +03:00
if ( mode ! = "watchos" ) {
var stderr_tty = Marshal . PtrToStringAuto ( ttyname ( 2 ) ) ;
if ( ! string . IsNullOrEmpty ( stderr_tty ) ) {
2019-10-14 17:18:46 +03:00
args . Add ( $"--stdout={stderr_tty}" ) ;
args . Add ( $"--stderr={stderr_tty}" ) ;
2016-11-04 14:13:20 +03:00
} else {
2017-11-28 16:27:31 +03:00
var stdout_log = Logs . CreateFile ( $"stdout-{Harness.Timestamp}.log" , "Standard output" ) ;
var stderr_log = Logs . CreateFile ( $"stderr-{Harness.Timestamp}.log" , "Standard error" ) ;
2019-10-14 17:18:46 +03:00
args . Add ( $"--stdout={stdout_log}" ) ;
args . Add ( $"--stderr={stderr_log}" ) ;
2016-11-04 14:13:20 +03:00
}
}
2016-06-16 10:07:47 +03:00
var systemLogs = new List < CaptureLog > ( ) ;
foreach ( var sim in simulators ) {
// Upload the system log
2016-06-17 18:21:18 +03:00
main_log . WriteLine ( "System log for the '{1}' simulator is: {0}" , sim . SystemLog , sim . Name ) ;
bool isCompanion = sim ! = simulator ;
2017-11-28 16:27:31 +03:00
var log = new CaptureLog ( Logs , sim . SystemLog , entire_file : Harness . Action ! = HarnessAction . Jenkins )
2016-11-17 10:07:09 +03:00
{
2016-11-07 17:25:53 +03:00
Path = Path . Combine ( LogDirectory , sim . Name + ".log" ) ,
2020-02-11 14:42:21 +03:00
Description = isCompanion ? Log . COMPANION_SYSTEM_LOG : Log . SYSTEM_LOG ,
2016-06-17 18:21:18 +03:00
} ;
log . StartCapture ( ) ;
Logs . Add ( log ) ;
systemLogs . Add ( log ) ;
Harness . LogWrench ( "@MonkeyWrench: AddFile: {0}" , log . Path ) ;
2016-06-16 10:07:47 +03:00
}
2016-06-17 18:21:18 +03:00
main_log . WriteLine ( "*** Executing {0}/{1} in the simulator ***" , appName , mode ) ;
2016-06-16 10:07:47 +03:00
2016-06-17 18:21:18 +03:00
if ( EnsureCleanSimulatorState ) {
foreach ( var sim in simulators )
await sim . PrepareSimulatorAsync ( main_log , bundle_identifier ) ;
}
2016-05-26 16:06:52 +03:00
2019-10-14 17:18:46 +03:00
args . Add ( $"--device=:v2:udid={simulator.UDID}" ) ;
2016-05-26 16:06:52 +03:00
2016-06-29 19:21:03 +03:00
await crash_reports . StartCaptureAsync ( ) ;
2016-05-26 16:06:52 +03:00
2016-06-17 18:21:18 +03:00
main_log . WriteLine ( "Starting test run" ) ;
2016-05-26 16:06:52 +03:00
2019-10-14 17:18:46 +03:00
var result = await ProcessHelper . ExecuteCommandAsync ( Harness . MlaunchPath , args , run_log , timeout , cancellation_token : cancellation_source . Token ) ;
2016-06-17 18:21:18 +03:00
if ( result . TimedOut ) {
2016-05-26 16:06:52 +03:00
timed_out = true ;
2016-06-17 18:21:18 +03:00
success = false ;
2019-05-30 18:40:56 +03:00
main_log . WriteLine ( "Test run timed out after {0} minute(s)." , timeout ) ;
2016-06-17 18:21:18 +03:00
} else if ( result . Succeeded ) {
main_log . WriteLine ( "Test run completed" ) ;
success = true ;
} else {
main_log . WriteLine ( "Test run failed" ) ;
success = false ;
}
2016-05-26 16:06:52 +03:00
2016-06-17 18:21:18 +03:00
if ( ! success . Value ) {
2016-05-26 16:06:52 +03:00
// find pid
var pid = - 1 ;
2016-06-17 18:21:18 +03:00
using ( var reader = run_log . GetReader ( ) ) {
while ( ! reader . EndOfStream ) {
var line = reader . ReadLine ( ) ;
if ( line . StartsWith ( "Application launched. PID = " , StringComparison . Ordinal ) ) {
var pidstr = line . Substring ( "Application launched. PID = " . Length ) ;
if ( ! int . TryParse ( pidstr , out pid ) )
main_log . WriteLine ( "Could not parse pid: {0}" , pidstr ) ;
} else if ( line . Contains ( "Xamarin.Hosting: Launched " ) & & line . Contains ( " with pid " ) ) {
var pidstr = line . Substring ( line . LastIndexOf ( ' ' ) ) ;
if ( ! int . TryParse ( pidstr , out pid ) )
main_log . WriteLine ( "Could not parse pid: {0}" , pidstr ) ;
2016-11-02 17:43:55 +03:00
} else if ( line . Contains ( "error MT1008" ) ) {
launch_failure = true ;
2016-06-17 18:21:18 +03:00
}
2016-05-26 16:06:52 +03:00
}
}
if ( pid > 0 ) {
2016-06-17 18:21:18 +03:00
var launchTimedout = cancellation_source . IsCancellationRequested ;
2016-12-16 14:24:08 +03:00
var timeoutType = launchTimedout ? "Launch" : "Completion" ;
2019-05-30 18:40:56 +03:00
var timeoutValue = launchTimedout ? Harness . LaunchTimeout : timeout . TotalSeconds ;
main_log . WriteLine ( $"{timeoutType} timed out after {timeoutValue} seconds" ) ;
2016-12-16 14:24:08 +03:00
await Process_Extensions . KillTreeAsync ( pid , main_log , true ) ;
2016-05-26 16:06:52 +03:00
} else {
2016-06-17 18:21:18 +03:00
main_log . WriteLine ( "Could not find pid in mtouch output." ) ;
2016-05-26 16:06:52 +03:00
}
}
// cleanup after us
2016-06-17 18:21:18 +03:00
if ( EnsureCleanSimulatorState )
await SimDevice . KillEverythingAsync ( main_log ) ;
2016-06-16 10:07:47 +03:00
foreach ( var log in systemLogs )
log . StopCapture ( ) ;
2016-05-26 16:06:52 +03:00
} else {
2016-10-11 20:30:11 +03:00
main_log . WriteLine ( "*** Executing {0}/{1} on device '{2}' ***" , appName , mode , device_name ) ;
2016-05-26 16:06:52 +03:00
2016-08-05 22:03:56 +03:00
if ( mode = = "watchos" ) {
2019-10-14 17:18:46 +03:00
args . Add ( "--attach-native-debugger" ) ; // this prevents the watch from backgrounding the app.
2017-01-04 21:45:26 +03:00
} else {
2019-10-14 17:18:46 +03:00
args . Add ( "--wait-for-exit" ) ;
2016-08-05 22:03:56 +03:00
}
2016-05-26 16:06:52 +03:00
AddDeviceName ( args ) ;
2017-11-28 16:27:31 +03:00
device_system_log = Logs . Create ( $"device-{device_name}-{Harness.Timestamp}.log" , "Device log" ) ;
2016-05-26 16:06:52 +03:00
var logdev = new DeviceLogCapturer ( ) {
Harness = Harness ,
2016-06-17 18:21:18 +03:00
Log = device_system_log ,
2016-05-26 16:06:52 +03:00
DeviceName = device_name ,
} ;
logdev . StartCapture ( ) ;
2019-11-27 19:44:26 +03:00
try {
await crash_reports . StartCaptureAsync ( ) ;
main_log . WriteLine ( "Starting test run" ) ;
bool waitedForExit = true ;
// We need to check for MT1111 (which means that mlaunch won't wait for the app to exit).
var callbackLog = new CallbackLog ( ( line ) = > {
// MT1111: Application launched successfully, but it's not possible to wait for the app to exit as requested because it's not possible to detect app termination when launching using gdbserver
waitedForExit & = line ? . Contains ( "MT1111: " ) ! = true ;
if ( line ? . Contains ( "error MT1007" ) = = true )
launch_failure = true ;
} ) ;
var runLog = Log . CreateAggregatedLog ( callbackLog , main_log ) ;
var timeoutWatch = Stopwatch . StartNew ( ) ;
var result = await ProcessHelper . ExecuteCommandAsync ( Harness . MlaunchPath , args , runLog , timeout , cancellation_token : cancellation_source . Token ) ;
if ( ! waitedForExit & & ! result . TimedOut ) {
// mlaunch couldn't wait for exit for some reason. Let's assume the app exits when the test listener completes.
main_log . WriteLine ( "Waiting for listener to complete, since mlaunch won't tell." ) ;
if ( ! await listener . CompletionTask . TimeoutAfter ( timeout - timeoutWatch . Elapsed ) ) {
result . TimedOut = true ;
}
2017-11-02 14:46:18 +03:00
}
2019-11-27 19:44:26 +03:00
if ( result . TimedOut ) {
timed_out = true ;
success = false ;
main_log . WriteLine ( "Test run timed out after {0} minute(s)." , timeout . TotalMinutes ) ;
} else if ( result . Succeeded ) {
main_log . WriteLine ( "Test run completed" ) ;
success = true ;
} else {
main_log . WriteLine ( "Test run failed" ) ;
success = false ;
}
} finally {
logdev . StopCapture ( ) ;
device_system_log . Dispose ( ) ;
2016-05-26 16:06:52 +03:00
}
// Upload the system log
2016-06-17 18:21:18 +03:00
if ( File . Exists ( device_system_log . FullPath ) ) {
main_log . WriteLine ( "A capture of the device log is: {0}" , device_system_log . FullPath ) ;
2016-11-17 10:07:09 +03:00
Harness . LogWrench ( "@MonkeyWrench: AddFile: {0}" , device_system_log . FullPath ) ;
2016-05-26 16:06:52 +03:00
}
}
2018-05-15 16:07:45 +03:00
listener . Cancel ( ) ;
2016-05-26 16:06:52 +03:00
listener . Dispose ( ) ;
// check the final status
var crashed = false ;
2016-06-17 18:21:18 +03:00
if ( File . Exists ( listener_log . FullPath ) ) {
Harness . LogWrench ( "@MonkeyWrench: AddFile: {0}" , listener_log . FullPath ) ;
2020-02-04 19:03:38 +03:00
success = TestsSucceeded ( listener_log . FullPath , timed_out , crashed ) ;
2016-05-26 16:06:52 +03:00
} else if ( timed_out ) {
Harness . LogWrench ( "@MonkeyWrench: AddSummary: <b><i>{0} never launched</i></b><br/>" , mode ) ;
2016-06-17 18:21:18 +03:00
main_log . WriteLine ( "Test run never launched" ) ;
2016-08-26 16:12:44 +03:00
success = false ;
2016-11-15 21:04:37 +03:00
} else if ( launch_failure ) {
Harness . LogWrench ( "@MonkeyWrench: AddSummary: <b><i>{0} failed to launch</i></b><br/>" , mode ) ;
main_log . WriteLine ( "Test run failed to launch" ) ;
success = false ;
2016-05-26 16:06:52 +03:00
} else {
Harness . LogWrench ( "@MonkeyWrench: AddSummary: <b><i>{0} crashed at startup (no log)</i></b><br/>" , mode ) ;
2016-06-17 18:21:18 +03:00
main_log . WriteLine ( "Test run crashed before it started (no log file produced)" ) ;
2016-05-26 16:06:52 +03:00
crashed = true ;
2016-08-26 16:12:44 +03:00
success = false ;
2016-05-26 16:06:52 +03:00
}
if ( ! success . HasValue )
success = false ;
2016-06-29 19:21:03 +03:00
await crash_reports . EndCaptureAsync ( TimeSpan . FromSeconds ( success . Value ? 0 : 5 ) ) ;
2016-06-17 18:21:18 +03:00
if ( timed_out ) {
2016-06-06 13:48:53 +03:00
Result = TestExecutingResult . TimedOut ;
} else if ( crashed ) {
Result = TestExecutingResult . Crashed ;
2016-06-17 18:21:18 +03:00
} else if ( success . Value ) {
Result = TestExecutingResult . Succeeded ;
2016-06-06 13:48:53 +03:00
} else {
Result = TestExecutingResult . Failed ;
}
2017-02-08 13:44:20 +03:00
// Check crash reports to see if any of them explains why the test run crashed.
if ( ! success . Value ) {
int pid = 0 ;
string crash_reason = null ;
foreach ( var crash in crash_reports . Logs ) {
try {
if ( pid = = 0 ) {
// Find the pid
using ( var log_reader = main_log . GetReader ( ) ) {
string line ;
while ( ( line = log_reader . ReadLine ( ) ) ! = null ) {
const string str = "was launched with pid '" ;
var idx = line . IndexOf ( str , StringComparison . Ordinal ) ;
if ( idx > 0 ) {
idx + = str . Length ;
var next_idx = line . IndexOf ( '\'' , idx ) ;
if ( next_idx > idx )
int . TryParse ( line . Substring ( idx , next_idx - idx ) , out pid ) ;
}
if ( pid ! = 0 )
break ;
}
}
}
using ( var crash_reader = crash . GetReader ( ) ) {
var text = crash_reader . ReadToEnd ( ) ;
var reader = System . Runtime . Serialization . Json . JsonReaderWriterFactory . CreateJsonReader ( Encoding . UTF8 . GetBytes ( text ) , new XmlDictionaryReaderQuotas ( ) ) ;
var doc = new XmlDocument ( ) ;
doc . Load ( reader ) ;
foreach ( XmlNode node in doc . SelectNodes ( $"/root/processes/item[pid = '" + pid + "']" ) ) {
Console . WriteLine ( node ? . InnerXml ) ;
Console . WriteLine ( node ? . SelectSingleNode ( "reason" ) ? . InnerText ) ;
crash_reason = node ? . SelectSingleNode ( "reason" ) ? . InnerText ;
}
}
if ( crash_reason ! = null )
break ;
} catch ( Exception e ) {
2017-05-31 18:34:26 +03:00
Harness . Log ( 2 , "Failed to process crash report '{1}': {0}" , e . Message , crash . Description ) ;
2017-02-08 13:44:20 +03:00
}
}
if ( ! string . IsNullOrEmpty ( crash_reason ) ) {
if ( crash_reason = = "per-process-limit" ) {
FailureMessage = "Killed due to using too much memory (per-process-limit)." ;
} else {
FailureMessage = $"Killed by the OS ({crash_reason})" ;
}
2018-03-07 16:48:24 +03:00
} else if ( launch_failure ) {
FailureMessage = $"Launch failure" ;
2017-02-08 13:44:20 +03:00
}
}
2016-05-26 16:06:52 +03:00
return success . Value ? 0 : 1 ;
}
2019-10-14 17:18:46 +03:00
public void AddDeviceName ( IList < string > args )
2016-05-26 16:06:52 +03:00
{
AddDeviceName ( args , device_name ) ;
}
2019-10-14 17:18:46 +03:00
public static void AddDeviceName ( IList < string > args , string device_name )
2016-05-26 16:06:52 +03:00
{
if ( ! string . IsNullOrEmpty ( device_name ) ) {
2019-10-14 17:18:46 +03:00
args . Add ( "--devname" ) ;
args . Add ( device_name ) ;
2016-05-26 16:06:52 +03:00
}
}
}
2019-02-20 08:33:30 +03:00
// Monitor the output from 'mlaunch --installdev' and cancel the installation if there's no output for 1 minute.
class AppInstallMonitorLog : Log {
public override string FullPath = > throw new NotImplementedException ( ) ;
Log copy_to ;
CancellationTokenSource cancellation_source ;
public bool CopyingApp ;
public bool CopyingWatchApp ;
public TimeSpan AppCopyDuration ;
public TimeSpan WatchAppCopyDuration ;
public Stopwatch AppCopyStart = new Stopwatch ( ) ;
public Stopwatch WatchAppCopyStart = new Stopwatch ( ) ;
public int AppPercentComplete ;
public int WatchAppPercentComplete ;
public long AppBytes ;
public long WatchAppBytes ;
public long AppTotalBytes ;
public long WatchAppTotalBytes ;
public CancellationToken CancellationToken {
get {
return cancellation_source . Token ;
}
}
public AppInstallMonitorLog ( Log copy_to )
: base ( copy_to . Logs , $"Watch transfer log for {copy_to.Description}" )
{
this . copy_to = copy_to ;
cancellation_source = new CancellationTokenSource ( ) ;
cancellation_source . Token . Register ( ( ) = > {
copy_to . WriteLine ( "App installation cancelled: it timed out after no output for 1 minute." ) ;
} ) ;
}
public override Encoding Encoding = > copy_to . Encoding ;
public override void Flush ( )
{
copy_to . Flush ( ) ;
}
public override StreamReader GetReader ( )
{
return copy_to . GetReader ( ) ;
}
protected override void Dispose ( bool disposing )
{
base . Dispose ( disposing ) ;
copy_to . Dispose ( ) ;
cancellation_source . Dispose ( ) ;
}
void ResetTimer ( )
{
cancellation_source . CancelAfter ( TimeSpan . FromMinutes ( 1 ) ) ;
}
public override void WriteLine ( string value )
{
var v = value . Trim ( ) ;
if ( v . StartsWith ( "Installing application bundle" , StringComparison . Ordinal ) ) {
if ( ! CopyingApp ) {
CopyingApp = true ;
AppCopyStart . Start ( ) ;
} else if ( ! CopyingWatchApp ) {
CopyingApp = false ;
CopyingWatchApp = true ;
AppCopyStart . Stop ( ) ;
WatchAppCopyStart . Start ( ) ;
}
} else if ( v . StartsWith ( "PercentComplete: " , StringComparison . Ordinal ) & & int . TryParse ( v . Substring ( "PercentComplete: " . Length ) . Trim ( ) , out var percent ) ) {
if ( CopyingApp )
AppPercentComplete = percent ;
else if ( CopyingWatchApp )
WatchAppPercentComplete = percent ;
} else if ( v . StartsWith ( "NumBytes: " , StringComparison . Ordinal ) & & int . TryParse ( v . Substring ( "NumBytes: " . Length ) . Trim ( ) , out var num_bytes ) ) {
if ( CopyingApp ) {
AppBytes = num_bytes ;
AppCopyDuration = AppCopyStart . Elapsed ;
} else if ( CopyingWatchApp ) {
WatchAppBytes = num_bytes ;
WatchAppCopyDuration = WatchAppCopyStart . Elapsed ;
}
} else if ( v . StartsWith ( "TotalBytes: " , StringComparison . Ordinal ) & & int . TryParse ( v . Substring ( "TotalBytes: " . Length ) . Trim ( ) , out var total_bytes ) ) {
if ( CopyingApp )
AppTotalBytes = total_bytes ;
else if ( CopyingWatchApp )
WatchAppTotalBytes = total_bytes ;
}
ResetTimer ( ) ;
copy_to . WriteLine ( value ) ;
}
public override void Write ( byte [ ] buffer , int offset , int count )
{
copy_to . Write ( buffer , offset , count ) ;
}
}
2020-02-11 22:03:16 +03:00
}