-
Notifications
You must be signed in to change notification settings - Fork 571
Expand file tree
/
Copy pathFastDeploy.cs
More file actions
1291 lines (1190 loc) · 57.4 KB
/
Copy pathFastDeploy.cs
File metadata and controls
1291 lines (1190 loc) · 57.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.IO;
using System.Linq;
using System.Net;
using System.Buffers;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Android.Build.Tasks;
using Mono.AndroidTools;
using Xamarin.Android.Build.Debugging.Tasks.Properties;
using K4os.Compression.LZ4;
using Microsoft.Build.Framework;
namespace Xamarin.Android.Tasks
{
public class FastDeploy : AsyncTask
{
const string XAToolsTempPath = "/data/local/tmp/.xatools";
const string OverridePath = "files/.__override__";
const string ToolsPath = "files/.__tools__";
const int MAX_COMMAND = 4096;
const int ADB_COMMAND_PADDING = 100;
public override string TaskPrefix => "FD";
public string AdbTarget { get; set; }
public string UploadFlagFile { get; set; }
public bool EmbedAssembliesIntoApk { get; set; }
public bool ReInstall { get; set; } = false;
[Required]
public string PackageName { get; set; }
public string PackageFile { get; set; }
public string PrimaryCpuAbi { get; set; }
public string ToolsAbi { get; set; }
public ITaskItem [] FastDevFiles { get; set; }
public bool PreserveUserData { get; set; } = true;
[Required]
public string FastDevToolPath { get; set; }
public string FastDevTool { get; set; } = "xamarin.sync";
public string FastDevFindTool { get; set; } = "xamarin.find";
public string FastDevStatTool { get; set; } = "xamarin.stat";
public string FastDevCpTool { get; set; } = "xamarin.cp";
[Required]
public string ToolVersion { get; set; }
public bool DiagnosticLogging { get; set; } = false;
public bool UsingAndroidNETSdk { get; set; }
public string UserID { get; set; }
public bool IsTestOnly { get; set; }
[Required]
public string IntermediateOutputPath { get; set; }
public ITaskItem[] EnvironmentFiles { get; set; }
AndroidDevice Device;
DateTime lastUpload = DateTime.MinValue;
internal class PackageInfo {
string internalPath = null;
public string InternalPath {
get { return internalPath; }
set {
internalPath = value?.Trim () ?? null;
}
}
public string ToolVersion { get; set; }
public int? BlockSize { get; set; }
public bool SupportsFastDev { get; set; } = true;
public bool IsSystemApplication { get; set; } = false;
public bool AdbIsRoot { get; set; } = false;
public string UserId { get; set; } = null;
public string PackageName { get; set; } = null;
public bool DiagnosticLogging { get; set; } = false;
public Action<string> LogDebugMessage;
}
private class DiagnosticData {
[JsonPropertyName ("Task")]
public string Task { get; set; } = nameof (FastDeploy);
[JsonPropertyName ("Properties")]
public Dictionary<string, string> Properties { get; set; } = new Dictionary<string, string>() {
{ "target.prop.ro.product.build.version.sdk", "" },
{ "target.prop.ro.product.cpu.abilist", "" },
{ "target.prop.ro.product.manufacturer", "" },
{ "target.prop.ro.product.model", "" },
{ "target.prop.ro.product.cpu.abi", ""},
{ "deploy.error.code", ""},
{ "deploy.tool", "xamarin.sync" },
{ "deploy.result", "Success" },
{ "deploy.supports.fastdev", "True" },
{ "deploy.systemapp", "False" },
{ "deploy.duration.ms", "0" },
{ "pii.deploy.error", "" },
{ "pii.deploy.file", "" },
};
internal void SetProperty (string key, bool? value)
{
Properties[key] = value?.ToString () ?? "False";
}
internal void SetProperty (string key, int? value)
{
Properties[key] = value?.ToString () ?? "-1";
}
internal void SetProperty (string key, long? value)
{
Properties[key] = value?.ToString () ?? "-1";
}
internal void SetProperty (string key, string value)
{
Properties[key] = value ?? "unknown";
}
}
PackageInfo packageInfo;
Stopwatch stopWatch = new Stopwatch ();
Queue<string> diagnosticLogs = new Queue<string> ();
DiagnosticData diagnosticData = new DiagnosticData ();
protected string ToolsFullPath {
get { return packageInfo.IsSystemApplication ? $"{packageInfo.InternalPath}/{ToolsPath}" : ToolsPath; }
}
protected string OverrideFullPath {
get { return packageInfo.IsSystemApplication ? $"{packageInfo.InternalPath}/{OverridePath}" : OverridePath; }
}
void StartTiming ()
{
stopWatch.Restart ();
}
long GetElapsedTimeAndRestart ()
{
stopWatch.Stop ();
long elapsedTime = stopWatch.ElapsedMilliseconds;
stopWatch.Restart ();
return elapsedTime;
}
void DebugHandler (string task, string message)
{
LogDiagnostic ($"DEBUG {task} {message} [{GetElapsedTimeAndRestart ()}ms]");
}
void LogDebugMessageWithTiming (string message)
{
LogDiagnostic ($"{message} [{GetElapsedTimeAndRestart ()}ms]");
}
void LogDiagnostic (string message)
{
if (DiagnosticLogging) {
LogDebugMessage (message);
return;
}
diagnosticLogs.Enqueue (message);
}
void PrintDiagnostics ()
{
while (diagnosticLogs.Count > 0) {
LogMessage (diagnosticLogs.Dequeue ());
}
LogMessage ($"{diagnosticData.Task}");
foreach (var t in diagnosticData.Properties) {
LogMessage ($"\t{t.Key}: {t.Value}");
}
}
void LogDiagnosticDataError (string errorCode, string error, string file = "")
{
diagnosticData.SetProperty ("deploy.result", "Failed");
if (!string.IsNullOrEmpty (file))
diagnosticData.SetProperty ("pii.deploy.file", file);
diagnosticData.SetProperty ("pii.deploy.error", error);
diagnosticData.SetProperty ("deploy.error.code", errorCode);
}
void SaveDiagnosticData (long ms)
{
JsonSerializerOptions options = new JsonSerializerOptions {
WriteIndented = true
};
diagnosticData.SetProperty ("deploy.duration.ms", ms);
string newPath = Path.Combine(IntermediateOutputPath, "diagnostics", "fastdeploy.json");
File.WriteAllText (newPath, JsonSerializer.Serialize (diagnosticData, options));
}
public override bool Execute ()
{
Device = AndroidHelper.ParseTarget (AdbTarget, LogMessage, LogCodedError, logErrors: true, engine4: BuildEngine4);
if (Device == null) {
PrintDiagnostics ();
return false;
}
LogMessage ($"Found device: {Device.ID}");
if (string.IsNullOrEmpty (PrimaryCpuAbi) && !EmbedAssembliesIntoApk) {
PrintDiagnostics ();
LogCodedError ("XA0010", Resources.XA0010_NoAbi, Device.ID);
return false;
}
var lifetime = RegisteredTaskObjectLifetime.AppDomain;
var key = ProjectSpecificTaskObjectKey ($"{Device.ID}_{PackageName}");
if (!File.Exists (UploadFlagFile)) {
packageInfo = new PackageInfo ();
} else {
packageInfo = BuildEngine4.GetRegisteredTaskObjectAssemblyLocal<PackageInfo>(key, lifetime) ?? new PackageInfo ();
}
packageInfo.DiagnosticLogging = DiagnosticLogging;
packageInfo.LogDebugMessage = LogDiagnostic;
AndroidLogger.Debug += DebugHandler;
try {
var flagFilePath = GetFullPath (UploadFlagFile);
lastUpload = File.GetLastWriteTimeUtc (flagFilePath);
LogDiagnostic ($"LastWriteTime of `{flagFilePath}`: {lastUpload}");
StartTiming ();
return base.Execute ();
} finally {
BuildEngine4.RegisterTaskObjectAssemblyLocal (key, packageInfo, lifetime, allowEarlyCollection: false);
stopWatch.Stop ();
AndroidLogger.Debug -= DebugHandler;
}
}
public async override Task RunTaskAsync ()
{
var sw = new Stopwatch ();
sw.Restart ();
try {
await RunInstall ();
} catch {
PrintDiagnostics ();
throw;
} finally {
sw.Stop();
SaveDiagnosticData (sw.ElapsedMilliseconds);
}
}
public async Task RunInstall ()
{
await Device.EnsureProperties (CancellationToken).ConfigureAwait (false);
diagnosticData.SetProperty ("target.prop.ro.product.build.version.sdk", Device.Properties?.BuildVersionSdk);
diagnosticData.SetProperty ("target.prop.ro.product.cpu.abilist", string.Join (";", Device.Properties?.ProductCpuAbiList ?? Array.Empty<string> ()));
diagnosticData.SetProperty ("target.prop.ro.product.cpu.abi", PrimaryCpuAbi);
diagnosticData.SetProperty ("target.prop.ro.product.manufacturer", Device.Properties?.ProductManufacturer);
diagnosticData.SetProperty ("target.prop.ro.product.model", Device.Properties?.ProductModel);
string redirectStdio = Device.Properties.Get ("log.redirect-stdio");
if (redirectStdio != null && string.Equals ("true", redirectStdio.Trim (), StringComparison.OrdinalIgnoreCase)) {
LogDiagnosticDataError ("XA0128", Resources.XA0128_RedirectStdioIsEnabled);
PrintDiagnostics ();
LogCodedError ($"XA0128", Resources.XA0128_RedirectStdioIsEnabled);
return;
}
string runAsDisabled = Device.Properties.Get ("ro.boot.disable_runas");
if (runAsDisabled != null && string.Equals ("true", runAsDisabled.Trim (), StringComparison.OrdinalIgnoreCase)) {
LogDiagnosticDataError ("XA0131", Resources.XA0131_DeveloperModeNotEnabled);
PrintDiagnostics ();
LogCodedError ($"XA0131", Resources.XA0131_DeveloperModeNotEnabled);
return;
}
await CheckAppInstalledAndDebuggable (PackageName);
if (EmbedAssembliesIntoApk) {
// we need to remove the .__override__ directory BEFORE we uninstall the debug apk.
// this is because run-as does NOT work on release apps.
await RemoveOverrideDirectory();
}
if (ReInstall && !string.IsNullOrEmpty (PackageFile)) {
await Device.UninstallPackage (PackageName, PreserveUserData, CancellationToken);
}
if (!string.IsNullOrEmpty (PackageFile) &&
(packageInfo.InternalPath.IndexOf ("unknown", StringComparison.OrdinalIgnoreCase) >= 0 || ReInstall || IsPackageFileOutOfDate ())) {
try {
await InstallPackage (!(packageInfo.InternalPath.IndexOf ("unknown", StringComparison.OrdinalIgnoreCase) >= 0));
} catch (Exception ex) {
LogDiagnosticDataError (GetErrorCode (ex), ex.ToString ());
PrintDiagnostics ();
LogCodedError (GetErrorCode (ex), ex.ToString ());
return;
}
if (!EmbedAssembliesIntoApk && packageInfo.InternalPath.IndexOf ("unknown", StringComparison.OrdinalIgnoreCase) >= 0) {
packageInfo.InternalPath = null;
await CheckAppInstalledAndDebuggable (PackageName);
if (RaiseRunAsError (packageInfo.InternalPath)) {
return;
}
}
}
if (EmbedAssembliesIntoApk)
return;
if (!await InstallFastDevTools (ToolsFullPath)) {
return;
}
if (FastDevFiles?.Any () ?? false) {
await TerminateApp ();
await DeployFastDevFiles (ToolsFullPath, OverrideFullPath);
}
return;
}
bool IsPackageFileOutOfDate ()
{
var packageFile = GetFullPath (PackageFile);
var lastPackage = File.GetLastWriteTimeUtc (packageFile);
LogDiagnostic ($"LastWriteTime of `{packageFile}`: {lastPackage}");
return lastUpload < lastPackage;
}
int CompressLZ4 (ref byte [] data, int len, ref byte [] outBuffer, LZ4Level lZ4Level = LZ4Level.L00_FAST)
{
int compressedLength = LZ4Codec.Encode (data, 0, len, outBuffer, 0, outBuffer.Length, lZ4Level);
if (compressedLength < 0 || compressedLength >= outBuffer.Length) {
if (DiagnosticLogging)
LogDebugMessage ($"Sending Data Uncompressed.");
compressedLength = outBuffer.Length;
data.CopyTo (outBuffer, 0);
}
return compressedLength;
}
async Task CheckAppInstalledAndDebuggable (string packageName)
{
packageInfo.UserId = UserID;
packageInfo.PackageName = packageName;
await EnsureUserIsRunning ();
packageInfo.InternalPath = packageInfo.InternalPath ?? await QueryInternalPathWithRetry ();
if (packageInfo.InternalPath.IndexOf ("Permission denied", StringComparison.OrdinalIgnoreCase) >= 0) {
packageInfo.InternalPath = await Device.RunAs (packageInfo, "readlink", "-f", ".");
}
if (packageInfo.InternalPath.IndexOf ("not an application", StringComparison.OrdinalIgnoreCase) >= 0) {
LogDiagnostic ($"Package {packageInfo.PackageName} is a system application.");
packageInfo.IsSystemApplication = true;
diagnosticData.SetProperty ("deploy.systemapp", value:true);
string whoami = await Device.RunShellCommand ("whoami");
packageInfo.AdbIsRoot = whoami.Trim () == "root";
LogDiagnostic ($"using {(packageInfo.AdbIsRoot ? "root" : $"su {packageInfo.UserId}")} to install fast deployment files. ");
packageInfo.InternalPath = $"/data/user/{(packageInfo.UserId ?? "0")}/{packageInfo.PackageName}";
return;
}
if (packageInfo.InternalPath.IndexOf ("not debuggable", StringComparison.OrdinalIgnoreCase) >= 0) {
// current install is not debuggable, so lets uninstall it
LogDiagnostic ($"Package {packageInfo.PackageName} was not debuggable. Forcing ReInstall");
ReInstall = true;
return;
}
if (packageInfo.InternalPath.IndexOf ("unknown", StringComparison.OrdinalIgnoreCase) >= 0) {
LogDiagnostic ($"Package {packageInfo.PackageName} was not installed.");
return;
}
if (packageInfo.InternalPath.IndexOf ("Permission denied", StringComparison.OrdinalIgnoreCase) >= 0) {
// run-as is probably not supported.
LogDiagnostic ("run-as not supported on this device.");
packageInfo.SupportsFastDev = false;
diagnosticData.SetProperty ("deploy.supports.fastdev", value: false);
}
return;
}
/// <summary>
/// Issues the first <c>run-as <pkg> pwd</c> query, retrying briefly while the
/// per-user data directory is not yet stat-able through <c>run-as</c>.
/// </summary>
/// <remarks>
/// <para>Immediately after <c>pm install</c>, the per-user data directory
/// <c>/data/user/N/<pkg></c> may not yet be stat-able through <c>run-as</c>,
/// even for the primary user (id 0). During that window <c>run-as</c> returns
/// <c>run-as: couldn't stat /data/user/N/<pkg>: No such file or directory</c>,
/// which otherwise raises <c>XA0137</c> and disables Fast Deployment. This races
/// install on the primary user ~daily in CI. Poll for a bounded period to let the
/// directory materialize before giving up. See
/// https://github.com/dotnet/android/issues/7821 and
/// https://github.com/dotnet/android/issues/11808.</para>
/// <para>Retry policy: up to 10 attempts with a 500 ms delay between each, giving
/// a maximum wait of 4.5 seconds before the error is surfaced as <c>XA0137</c>.
/// Only the transient <c>couldn't stat … No such file or directory</c> signature
/// (detected by <see cref="IsTransientRunAsStatRace"/>) triggers a retry; all other
/// <c>run-as</c> failures are surfaced immediately.</para>
/// </remarks>
async Task<string> QueryInternalPathWithRetry ()
{
const int maxAttempts = 10;
var delay = TimeSpan.FromMilliseconds (500);
string result = await Device.RunAs (packageInfo, "pwd");
for (int attempt = 1; attempt < maxAttempts && IsTransientRunAsStatRace (result); attempt++) {
LogDiagnostic ($"run-as could not stat the data directory for {packageInfo.PackageName} yet (attempt {attempt}/{maxAttempts}); retrying in {delay.TotalMilliseconds:0} ms. Output: {result?.Trim ()}");
await Task.Delay (delay, CancellationToken);
result = await Device.RunAs (packageInfo, "pwd");
}
return result;
}
/// <summary>
/// Returns <see langword="true"/> when a <c>run-as</c> result matches the transient
/// install-vs-run-as race signature (<c>couldn't stat … No such file or directory</c>),
/// i.e. the per-user data directory has not yet materialized after <c>pm install</c>.
/// </summary>
internal static bool IsTransientRunAsStatRace (string result)
{
if (string.IsNullOrEmpty (result)) {
return false;
}
return result.IndexOf ("couldn't stat", StringComparison.OrdinalIgnoreCase) >= 0 &&
result.IndexOf ("No such file or directory", StringComparison.OrdinalIgnoreCase) >= 0;
}
/// <summary>
/// Ensures the secondary Android user targeted by this deployment is in the
/// 'running' state before any <c>run-as</c> query is issued against it.
/// </summary>
/// <remarks>
/// <para><c>pm install --user N <apk></c> registers the package but does
/// not materialize the per-user data directory <c>/data/user/N/<pkg></c>;
/// that directory is only created once user <c>N</c> is brought to the running
/// state. Until then, every <c>run-as <pkg> --user N</c> invocation fails
/// with <c>run-as: couldn't stat /data/user/N/<pkg>: No such file or
/// directory</c> and raises <c>XA0137</c>. See
/// https://github.com/dotnet/android/issues/7821.</para>
/// <para>Empirical measurement on an arm64 API-34 emulator (N=50) showed that
/// without this step, 52% of installs fail <c>run-as</c> at <c>t=0</c> and 30%
/// never recover even after 30 seconds of polling — i.e. polling alone is not
/// sufficient. <c>am start-user -w N</c> succeeds in 100/100 attempts within
/// ~134 ms median (max 363 ms), is idempotent, and is a cheap no-op when the
/// user is already running. The primary user (id 0) never requires this step,
/// so skip it to avoid any cost in the common case.</para>
/// </remarks>
async Task EnsureUserIsRunning ()
{
var userId = (UserID ?? string.Empty).Trim ();
if (userId.Length == 0 || (int.TryParse (userId, out var id) && id == 0)) {
return;
}
LogDiagnostic ($"Ensuring Android user {userId} is in the 'running' state before run-as queries.");
string output = await Device.RunShellCommand (CancellationToken, "am", "start-user", "-w", userId);
// `am start-user -w` normally prints `Success: user started`. Surface any
// output (success or failure, e.g. `Error: could not start user`) at the
// diagnostic level so build logs make the cause obvious if the subsequent
// `run-as` query fails. Do not attempt to interpret the output here: the
// existing run-as error path raises XA0137 deterministically on failure,
// and parsing `am`'s output for error markers risks false positives.
LogDiagnostic ($"'am start-user -w {userId}' returned: {(string.IsNullOrWhiteSpace (output) ? "<no output>" : output.Trim ())}");
}
protected async Task RemoveOverrideDirectory () {
// remote //.__override__ directory has files in it.
// We can do that by using out tool stat.
string overrideExists = await Device.RunAs (packageInfo, $"{ToolsFullPath}/{FastDevStatTool}", OverrideFullPath);
if (!(overrideExists.IndexOf ("error:", StringComparison.OrdinalIgnoreCase) >= 0) &&
!(overrideExists.IndexOf ("package not debuggable", StringComparison.OrdinalIgnoreCase) >= 0)) {
await Device.RunAs (packageInfo, "rm", "-Rf", OverrideFullPath);
}
}
protected async Task TerminateApp ()
{
var pid = await Device.GetProcessId (PackageName, CancellationToken);
if (pid == 0) {
LogDebugMessage ($"{PackageName} was not running, skipping kill");
return;
}
LogDebugMessage ($"Terminating {PackageName}...");
await Device.KillProcessAndWaitForExit (PackageName, CancellationToken);
LogDebugMessageWithTiming ($"{PackageName} Terminated.");
}
protected async Task InstallPackage (bool installed = true)
{
LogDebugMessage ($"Installing Package {PackageName}");
try {
await Device.PushAndInstallPackageAsync (new PushAndInstallCommand {
ApkFile = PackageFile,
PackageName = PackageName,
ReInstall = ReInstall,
User = UserID,
TestOnly = IsTestOnly,
}, token: CancellationToken);
LogDebugMessageWithTiming ($"Installed Package {PackageName}.");
} catch (Exception exception) {
var ex = exception;
if (exception is AggregateException aex) {
ex = aex.Flatten ().InnerException;
}
if (!await ShouldThrowIfPackageInstallFailed (ex as PackageAlreadyExistsException)) {
LogDebugMessageWithTiming ($"Installed Package {PackageName}.");
return;
}
throw;
}
return;
}
async Task<bool> ShouldThrowIfPackageInstallFailed (PackageAlreadyExistsException e)
{
if (e == null)
return true;
int s = (e.PackageFile ?? "").LastIndexOf ('/');
string apkBasename = s >= 0 ? e.PackageFile.Substring (s+1) : e.PackageFile;
// If the runtime already exists, ignore the error
// Sometimes android doesn't report it's installed when it is :/
if (apkBasename != Path.GetFileName (PackageFile))
return false;
// Oops; things have gotten wedged (stale/interrupted install?)
// The file we tried to upload already exists on the device!
// Delete and try again.
LogDebugMessage (string.Format ("Package '{0}' already exists. Retrying...", PackageName));
try {
// NOTE We NEED to delete the cache data too other wise the install will fail.
await Device.DeleteFile (e.PackageFile, true, CancellationToken);
} catch {
// Ebil, yes, but...
}
bool preserveData = !(e is RequiresUninstallException);
LogDebugMessage (string.Format ("Forcing complete uninstall of '{0}'... Preserving Data: {1}", PackageName, preserveData));
var uninstallCommand = new PmUninstallCommand() { PackageName = PackageName, User = UserID, PreserveData = preserveData };
await Device.UninstallPackage (uninstallCommand, cancellationToken: CancellationToken);
LogDebugMessage (string.Format ("Installing '{0}'...", PackageName));
await Device.PushAndInstallPackageAsync (new PushAndInstallCommand {
ApkFile = PackageFile,
PackageName = PackageName,
ReInstall = false,
User = UserID
},token: CancellationToken);
return false;
}
protected async Task<bool> InstallFastDevTools (string toolPath)
{
if (string.Compare (packageInfo.ToolVersion ?? string.Empty, ToolVersion, StringComparison.OrdinalIgnoreCase) == 0) {
LogDebugMessage ($"FastDev Tools already installed for the app. {packageInfo.ToolVersion}");
return true;
}
string output = await Device.RunAs (packageInfo, "cat", $"{toolPath}/version");
if (string.Compare (output.Trim (), ToolVersion, StringComparison.OrdinalIgnoreCase) == 0) {
LogDebugMessage ($"FastDev Tools already installed for the app. {output}");
packageInfo.ToolVersion = ToolVersion;
return true;
}
output = await Device.RunAs (packageInfo, "mkdir", "-p", toolPath);
if (output.IndexOf ("run-as:", StringComparison.OrdinalIgnoreCase) >= 0 ||
output.IndexOf ("mkdir:", StringComparison.OrdinalIgnoreCase) >= 0) {
if (!RaiseRunAsError (output)) {
LogDiagnosticDataError ("XA0130", output);
PrintDiagnostics ();
LogCodedError ($"XA0130", Resources.XA0130_FastDevNotSupported);
}
return false;
}
// we have to do this as a normal shell command since running
// mkdir under `run-as` will result in a `permission-denied` error.
output = await Device.RunShellCommand ("mkdir", "-p", XAToolsTempPath);
if (output.IndexOf ("mkdir:", StringComparison.OrdinalIgnoreCase) >= 0) {
if (!RaiseRunAsError (output)) {
LogDiagnosticDataError ("XA0130", output);
PrintDiagnostics ();
LogCodedError ($"XA0130", Resources.XA0130_FastDevNotSupported);
}
return false;
}
string toolAbi = string.IsNullOrEmpty (ToolsAbi) ? PrimaryCpuAbi : ToolsAbi;
var tools = new [] { FastDevFindTool, FastDevTool, FastDevStatTool, FastDevCpTool };
foreach (var tool in tools) {
LogDebugMessage ($"Installing FastDev Tool {toolPath}/{tool} for {toolAbi}");
if (!await PushFileToDevice (Device, PackageName, toolPath, Path.Combine (FastDevToolPath, toolAbi, tool), $"{toolPath}/{tool}", CancellationToken)) {
LogDiagnosticDataError ("XA0126", Resources.XA0126_UnableToCopyFastDevTools);
PrintDiagnostics ();
LogCodedError ($"XA0126", Resources.XA0126_UnableToCopyFastDevTools, toolPath, tool);
return false;
}
}
LogDebugMessage ($"Setting FastDev Tools Permissions");
await Device.RunAs (packageInfo, "chmod", "700", $"{toolPath}/{FastDevTool}", $"{toolPath}/{FastDevFindTool}", $"{toolPath}/{FastDevStatTool}", $"{toolPath}/{FastDevCpTool}");
LogDebugMessage ($"Installing FastDev Tools to {toolPath}/version");
await PushFileTextToDevice (Device, PackageName, ToolVersion, Encoding.ASCII, $"{toolPath}/version", token: CancellationToken);
LogDebugMessage ($"Removing FastDev Tools temp directory.");
await Device.RunShellCommand ("rm", "-Rf", XAToolsTempPath);
packageInfo.ToolVersion = ToolVersion;
return true;
}
async Task<bool> PushFileToDevice (AndroidDevice device, string packageName, string toolPath, string file, string target, CancellationToken token)
{
if (!File.Exists (file)) {
LogDebugMessage ($"File '{file}' does not exists. Skipping.");
return false;
}
using (var fs = File.OpenRead (file)) {
if (!await PushStreamToDevice (device, packageName, toolPath, fs, target, DateTime.UtcNow, token: token)) {
return false;
}
}
return true;
}
async Task<bool> PushFileTextToDevice (AndroidDevice device, string packageName, string fileContents, Encoding encoding, string target, CancellationToken token)
{
using (var ms = new MemoryStream ()) {
using (var sw1 = new StreamWriter (ms, encoding, 1024, leaveOpen: true)) {
sw1.WriteLine (fileContents);
sw1.Flush ();
}
ms.Position = 0;
if (!await PushStreamToDevice (device, packageName, null, ms, target, DateTime.UtcNow, token: token)) {
return false;
}
}
return true;
}
async Task<bool> PushStreamToDeviceWithTool (AndroidDevice device, string packageName, string toolPath, Stream stream, string target, DateTimeOffset modifiedDateTime, CancellationToken token = default (CancellationToken))
{
string targetFile = Path.GetFileName (target);
try {
long wrote = await device.Push (stream, $"{XAToolsTempPath}/{targetFile}", cancellationToken: token);
LogDiagnostic ($"Pushed {wrote} to {XAToolsTempPath}/{targetFile}");
string r = await device.RunAs (packageInfo, $"{toolPath}/{FastDevCpTool}", $"{XAToolsTempPath}/{targetFile}", target, $"{modifiedDateTime.ToUnixTimeMilliseconds ()}");
if (r.IndexOf ("run-as:", StringComparison.OrdinalIgnoreCase) >= 0) {
TryGetRunAsErrorCode (r, out var err);
LogDiagnosticDataError (err.code, r, targetFile);
return false;
}
LogDiagnostic ($"moved {XAToolsTempPath}/{targetFile} to {target}");
LogDebugMessageWithTiming ($"Installed {target}.");
} catch (Exception ex) {
LogDebugMessageWithTiming ($"Failed to push {targetFile} to {target}. {ex}.");
LogDiagnosticDataError(GetErrorCode (ex),ex.ToString (), targetFile);
return false;
}
return true;
}
async Task<bool> PushStreamToDevice (AndroidDevice device, string packageName, string toolPath, Stream stream, string target, DateTimeOffset modifiedDateTime, CancellationToken token = default (CancellationToken))
{
string targetFile = Path.GetFileName (target);
try {
long wrote = await device.Push (stream, $"{XAToolsTempPath}/{targetFile}", cancellationToken: token);
LogDiagnostic ($"Pushed {wrote} to {XAToolsTempPath}/{targetFile}");
string r = await device.RunAs (packageInfo, "cp", $"{XAToolsTempPath}/{targetFile}", target);
if (r.IndexOf ("run-as:", StringComparison.OrdinalIgnoreCase) >= 0) {
TryGetRunAsErrorCode (r, out var err);
LogDiagnosticDataError (err.code, r, targetFile);
return false;
}
LogDiagnostic ($"moved {XAToolsTempPath}/{targetFile} to {target}");
await device.RunAs (packageInfo, "touch", "-t", $"{modifiedDateTime.ToString ("yyyyMMdd.HHmmss")}", target);
LogDebugMessageWithTiming ($"Installed {target}.");
} catch (Exception ex) {
LogDiagnosticDataError (GetErrorCode (ex),ex.ToString ());
LogDebugMessageWithTiming ($"Failed to push {targetFile} to {target}. {ex}.");
return false;
}
return true;
}
string GetTargetPath (ITaskItem file)
{
string targetPath = file.GetMetadata ("TargetPath");
if (string.IsNullOrEmpty (targetPath)) {
// fallback to DestinationSubPath
LogDiagnostic ($"'TargetPath' meta data not found on '{file.ItemSpec}'. Falling back to'DestinationSubPath'");
targetPath = file.GetMetadata ("DestinationSubPath");
}
return targetPath;
}
protected async Task DeployFastDevFiles (string toolPath, string overridePath)
{
// get the optimal blocksize from the device. This will help speed up transfer and disk writes.
LZ4Level lz4level = LZ4Level.L03_HC;
LogDiagnostic ("Calculating subdirectories");
HashSet<string> directories = new HashSet<string> ();
directories.Add (overridePath);
foreach (var file in FastDevFiles) {
string targetPath = GetTargetPath (file);
if (!string.IsNullOrEmpty (targetPath)) {
string dirName = Path.GetDirectoryName (targetPath).Replace ("\\", "/");
if (!string.IsNullOrEmpty (dirName)) {
directories.Add ($"{overridePath}/{dirName}");
LogDiagnostic ($"{targetPath} => {overridePath}/{dirName}");
}
}
}
int length = ADB_COMMAND_PADDING + PackageName.Length;
List<string> args = new List<string>(directories.Count + 2);
args.Add ("mkdir");
args.Add ("-p");
foreach (var dir in directories) {
int newLength = dir.Length + 3;
if ((length + newLength) >= MAX_COMMAND) {
await Device.RunAs (packageInfo, args);
length = ADB_COMMAND_PADDING + PackageName.Length;
args.Clear ();
args.Add ("mkdir");
args.Add ("-p");
}
length += newLength;
args.Add (dir);
}
await Device.RunAs (packageInfo, args);
string filelist = await Device.RunAs (packageInfo, $"{toolPath}/{FastDevFindTool}", DiagnosticLogging ? "-vd" : "-v", overridePath);
LogDiagnostic ($"{FastDevFindTool}: {filelist}");
string [] files = Array.Empty<string> ();
if (!(filelist.IndexOf ("error:", StringComparison.OrdinalIgnoreCase) >= 0)) {
files = filelist.Split (new char [] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
}
Dictionary<string, (long size, DateTimeOffset mtime)> fileData = new Dictionary<string, (long, DateTimeOffset)> ();
foreach (var file in files) { // file size mtime
if (file.IndexOf ("\t") == -1) {
LogDebugMessage ($"{FastDevFindTool}: Ignoring line '{file}'. Line is incorrectly formatted.");
continue;
}
var entires = file.Split (new char [] { '\t' }, StringSplitOptions.RemoveEmptyEntries);
if (entires.Length != 3) {
LogDebugMessage ($"{FastDevFindTool}: Ignoring line {file}. Input does not have 3 items.");
continue;
}
if (long.TryParse (entires [1].Trim (), out long fsize) && long.TryParse (entires [2].Trim (), out long mtime)) {
DateTimeOffset offset;
try {
offset = DateTimeOffset.FromUnixTimeMilliseconds (mtime);
} catch (ArgumentOutOfRangeException) {
offset = DateTimeOffset.MinValue;
}
fileData.Add (entires [0].Replace ("./", "").Trim (), (size: fsize, mtime: offset));
} else {
LogDebugMessage ($"Failed to parse values for line {file}. Ignoring.");
}
}
// remove known directories s they don't get deleted.
fileData.Remove ("links");
foreach (var file in FastDevFiles) {
if (!File.Exists (file.ItemSpec)) {
LogDebugMessage ($"File '{file.ItemSpec}' does not exists. Skipping.");
continue;
}
StartTiming ();
if (Path.GetExtension (file.ItemSpec) == ".so") {
string abi = AndroidRidAbiHelper.GetNativeLibraryAbi (file);
if (abi != PrimaryCpuAbi) {
LogDebugMessageWithTiming ($"NotifySync SkipCopyFile {file.ItemSpec} abi not suitable for this device.");
continue;
}
}
string targetPath = GetTargetPath (file);
if (!string.IsNullOrEmpty (targetPath)) {
targetPath = $"{targetPath}".Replace ("\\", "/");
} else {
targetPath = $"{Path.GetFileName (file.ItemSpec)}";
}
string filename = Path.GetFileName (file.ItemSpec);
var fi = new FileInfo (file.ItemSpec);
bool modified = true;
DateTimeOffset modifiedDateTime = File.GetLastWriteTimeUtc (file.ItemSpec);
DateTimeOffset remoteDateTime = DateTimeOffset.MinValue;
if (fileData.ContainsKey (targetPath)) {
remoteDateTime = fileData [targetPath].mtime;
modified = remoteDateTime.ToUnixTimeMilliseconds () < modifiedDateTime.ToUnixTimeMilliseconds () || fi.Length != fileData [targetPath].size;
}
if (!modified) {
LogDebugMessageWithTiming ($"NotifySync SkipCopyFile {file.ItemSpec}=>{targetPath} file is up to date.");
fileData.Remove (targetPath);
continue;
}
if (!await DeployFileWithFastDevTool (file, toolPath, overridePath, lz4level, modifiedDateTime)) {
diagnosticData.SetProperty ("deploy.result", "Failed");
return;
}
LogDebugMessageWithTiming ($"NotifySync CopyFile {file.ItemSpec}.");
LogDiagnostic ($"Local Modified Time '{modifiedDateTime.ToUnixTimeMilliseconds ()}' is newer than '{remoteDateTime.ToUnixTimeMilliseconds ()}'.");
fileData.Remove (targetPath);
}
if (EnvironmentFiles?.Length > 0) {
string targetPath = $"{PrimaryCpuAbi}/environment";
DateTimeOffset remoteDateTime = DateTimeOffset.MinValue;
if (fileData.ContainsKey (targetPath)) {
remoteDateTime = fileData [targetPath].mtime;
}
await DeployEnvironmentFiles (EnvironmentFiles, toolPath, overridePath, targetPath, remoteDateTime);
fileData.Remove (targetPath);
}
foreach (var file in fileData.Keys) {
// we need to remove unknown files from the .__override__ path
string targetFile = $"{file.Replace ("./", "")}";
LogDebugMessage ($"Remove redundant file {OverrideFullPath}/{targetFile}");
await Device.RunAs (packageInfo, "rm", "-Rf", $"{OverrideFullPath}/{targetFile}");
}
// clean up the temp folder if we are not using the xamarin.sync tool
if (!packageInfo.SupportsFastDev)
await Device.RunShellCommand ("rm", "-Rf", XAToolsTempPath);
return;
}
async Task<bool> DeployFileWithFastDevTool (ITaskItem file, string toolPath, string overridePath, LZ4Level lz4level, DateTimeOffset modifiedDateTime)
{
using (var fs = File.OpenRead (file.ItemSpec)) {
string destination = overridePath;
// This bit handles subdirectories.
int bufferSize = LZ4Codec.MaximumOutputSize (fs.Length > int.MaxValue ? int.MaxValue : (int)fs.Length);
string targetPath = GetTargetPath (file);
if (!string.IsNullOrEmpty (targetPath)) {
destination += $"/{targetPath}".Replace ("\\", "/");
} else {
destination += $"/{Path.GetFileName (file.ItemSpec)}";
}
if (packageInfo.SupportsFastDev) {
byte [] buffer = ArrayPool<byte>.Shared.Rent (bufferSize);
byte [] compressed = ArrayPool<byte>.Shared.Rent (bufferSize);
try {
List<string> args = DeviceExt.BuildArgs (DeviceExt.RunAsCommand, packageInfo);
args.AddRange (new string[] { $"{toolPath}/{FastDevTool}", $"{compressed.Length}", $"{fs.Length}", $"{destination}", $"{modifiedDateTime.ToUnixTimeMilliseconds ()}" });
LogDiagnostic ($"executing: {string.Join (" ", args.ToArray ())}");
var output = await Device.RunShellCommandStream (args.ToArray (), async (s) => {
int read = await fs.ReadAsync (buffer, 0, buffer.Length);
if (read == 0)
return false;
int compressedLength = CompressLZ4 (ref buffer, read, ref compressed, lz4level);
int l = IPAddress.HostToNetworkOrder (compressedLength);
var v = BitConverter.GetBytes (l);
try {
s.Write (v, 0, 4);
s.Write (compressed, 0, compressedLength);
} catch {
return false;
}
return true;
}, CancellationToken);
LogDiagnostic ($"FastDev of {file.ItemSpec} returned: {output}");
if (output.IndexOf ("error:", StringComparison.OrdinalIgnoreCase) >= 0) {
if (output.IndexOf ("from stdin.", StringComparison.OrdinalIgnoreCase) >= 0) {
LogDiagnostic ($"'{FastDevTool}' returned '{output}' when deploying '{destination}'. Falling back to backup deployment.");
diagnosticData.SetProperty ("pii.deploy.error", output);
diagnosticData.SetProperty ("pii.deploy.file", file.ItemSpec);
diagnosticData.SetProperty ("deploy.tool", value:"xamarin.cp");
// Log warning and fallback to adb push style deployment. It will be slower... but it works.
} else {
LogDiagnosticDataError ("XA0127", output, file.ItemSpec);
PrintDiagnostics ();
LogCodedError ($"XA0127", Resources.XA0127_ErrorDeployingFile, destination, FastDevTool, output);
return false;
}
}
if (output.IndexOf ($"wrote [{fs.Length}]", StringComparison.OrdinalIgnoreCase) >= 0) {
return true;
}
// we didn't write the file as we expected so use the backup path.
// this can happen is the devices supports run-as but does not support
// reading data in from stdin. Normally on older devices.
// if we get here, we will just reset the stream and drop through to the
// backup path.
packageInfo.SupportsFastDev = false;
fs.Position = 0;
} catch (Exception ex) {
LogDiagnostic ($"Hit exception. Falling back to slow deployment for {file.ItemSpec}. {ex}");
diagnosticData.SetProperty ("pii.deploy.error", ex.ToString ());
diagnosticData.SetProperty ("deploy.tool", value:"xamarin.cp");
packageInfo.SupportsFastDev = false;
fs.Position = 0;
} finally {
ArrayPool<byte>.Shared.Return (buffer);
ArrayPool<byte>.Shared.Return (compressed);
}
}
if (!packageInfo.SupportsFastDev) {
if (!await PushStreamToDeviceWithTool (Device, PackageName, toolPath, fs, destination, modifiedDateTime, token: CancellationToken)) {
LogDiagnosticDataError ("XA0129", Resources.XA0129_ErrorDeployingFile, destination);
PrintDiagnostics ();
LogCodedError ($"XA0129", Resources.XA0129_ErrorDeployingFile, destination);
return false;
}
}
}
return true;
}
async Task<bool> DeployEnvironmentFiles (ITaskItem[] environments, string toolPath, string overridePath, string targetPath, DateTimeOffset remoteFileModified)
{
int maxKeyLength = 0;
int maxValueLength = 0;
DateTimeOffset newestFileDateTime = DateTimeOffset.MinValue;
var data = new Dictionary<string, string> ();
foreach (ITaskItem env in environments ?? Array.Empty<ITaskItem> ()) {
if (!File.Exists (env.ItemSpec))
continue;
DateTimeOffset modifiedDateTime = File.GetLastWriteTimeUtc (env.ItemSpec);
if (modifiedDateTime > newestFileDateTime)
newestFileDateTime = modifiedDateTime;
foreach (string line in File.ReadLines (env.ItemSpec)) {
if (string.IsNullOrEmpty (line))
continue;
int index = line.IndexOf ('=');
if (index == -1) {
LogDebugMessage ($"Skipping invalid environment line: {line}");
continue;
}
var key = line.Substring (0, index);
var value = line.Substring (index + 1);
maxKeyLength = Math.Max (maxKeyLength, key.Length);
maxValueLength = Math.Max (maxValueLength, value.Length);
data [key] = value;
}
}
// Length+1 so at least one trailing \0 for the longest value
maxKeyLength++;
maxValueLength++;
if (newestFileDateTime <= remoteFileModified) {
LogDebugMessage ($"NotifySync SkipCopyFile @(AndroidEnvironment) files => {targetPath} file is up to date.");
return true;
}
var stream = new MemoryStream (); // dont use Pool as Device.Push dispose's the stream.
var binaryWriter = new BinaryWriter (stream, Encoding.ASCII);
binaryWriter.Write (Encoding.ASCII.GetBytes ("0x" + maxKeyLength.ToString ("X8") + '\0'));
binaryWriter.Write (Encoding.ASCII.GetBytes ("0x" + maxValueLength.ToString ("X8") + '\0'));
foreach (var kvp in data) {
binaryWriter.Write (Encoding.ASCII.GetBytes (kvp.Key.PadRight (maxKeyLength, '\0')));
binaryWriter.Write (Encoding.ASCII.GetBytes (kvp.Value.PadRight (maxValueLength, '\0')));
}
binaryWriter.Flush ();
binaryWriter.BaseStream.Position = 0;
await PushStreamToDeviceWithTool (Device, PackageName, toolPath, binaryWriter.BaseStream, $"{overridePath}/{targetPath}", DateTimeOffset.UtcNow, token: CancellationToken);
LogDebugMessageWithTiming ($"NotifySync CopyFile @(AndroidEnvironment) files.");
LogDiagnostic ($"Local Modified Time '{newestFileDateTime.ToUnixTimeMilliseconds ()}' is newer than '{remoteFileModified.ToUnixTimeMilliseconds ()}'.");
return true;
}
string GetErrorCode (Exception ex)
{
switch (ex) {
case IncompatibleCpuAbiException e:
return "ADB0020";
case RequiresUninstallException e:
return "ADB0030";
case SdkNotSupportedException e:
return "ADB0040";
case PackageAlreadyExistsException e:
return "ADB0050";
case InsufficientSpaceException e:
return "ADB0060";
//NOTE: this one is a base class
case InstallFailedException e:
return "ADB0010";
default:
return GetErrorCode (ex.Message);
}
}
static readonly List<(string error, string code, string message)> runas_codes = new List<(string error, string code, string message)> () {
{ (error: "run-as is disabled", code: "XA0131", message: Resources.XA0131_DeveloperModeNotEnabled ) },
{ (error: "unknown", code: "XA0132", message: Resources.XA0132_PackageNotInstalled ) },
{ (error: "Permission denied", code: "XA0133", message: Resources.XA0133_RunAsPermissionDenied ) },