Sumber yang diterbitkan
Muat turun ZIP sumber
CH-J SM Mobile
Layari direktori dan fail untuk keluaran aplikasi tertentu.
CH-J / SM Mobile
175 fail
sm-mobile-20261016T185235Z-8e93ecc1b59a
Ditambah: · Dibina:
1
import 'dart:async';2
import 'dart:collection';3
import 'dart:convert';4
import 'dart:io';5
import 'dart:math' as math;7
/// Shared models of the network diagnostics: TCP connect ping, ICMP echo8
/// ping and traceroute. Every measurement runs directly from this device.10
/// Which IP versions a measurement uses.11
enum IpMode {12
/// The first address the system resolver returns (its preference order);13
/// the result shows which address and protocol were used.14
auto,15
v4,16
v6,18
/// IPv4 and IPv6 measured independently and at the same time.19
dual,20
}22
enum Family {23
v4,24
v6;26
String get label => this == v4 ? 'IPv4' : 'IPv6';27
InternetAddressType get type =>28
this == v4 ? InternetAddressType.IPv4 : InternetAddressType.IPv6;29
static Family of(InternetAddress a) =>30
a.type == InternetAddressType.IPv6 ? v6 : v4;31
}33
enum MeasureKind { tcp, icmp, trace }35
/// Outcome of one attempt.36
enum Outcome {37
/// TCP handshake completed / ICMP echo reply received.38
ok,40
/// No answer within the timeout.41
timeout,43
/// The target answered with a TCP reset (port closed).44
refused,46
/// ICMP destination unreachable, or the OS reported no route.47
unreachable,49
/// An ICMP time exceeded instead of a reply (TTL too small for ping).50
ttlExceeded,52
/// The packet could not be sent or another local network error.53
error,54
}56
/// One attempt of a ping (TCP or ICMP).57
class Sample {58
final int seq;59
final DateTime time;60
final Family family;61
final String address;62
final int? port;63
final double? rttMs;64
final Outcome outcome;66
/// Extra information: ICMP code, errno, responding router.67
final String? detail;68
const Sample({69
required this.seq,70
required this.time,71
required this.family,72
required this.address,73
required this.outcome,74
this.port,75
this.rttMs,76
this.detail,77
});78
bool get ok => outcome == Outcome.ok && rttMs != null;80
Map<String, dynamic> toJson() => {81
'seq': seq,82
'time': time.toUtc().toIso8601String(),83
'ipVersion': family.label,84
'address': address,85
if (port != null) 'port': port,86
'outcome': outcome.name,87
'rttMs': rttMs,88
if (detail != null) 'detail': detail,89
};90
}92
/// Summary statistics of a series of attempts.93
///94
/// RTT figures use successful attempts only; failures are counted95
/// separately. Definitions:96
/// * standard deviation: population standard deviation of the RTTs;97
/// * jitter: mean absolute difference between consecutive successful RTTs98
/// (as in RFC 3550 §6.4.1, without its smoothing);99
/// * median and P95: nearest-rank percentiles; P95 needs at least 20100
/// successful samples.101
class RttStats {102
final int attempts, successes;103
final double? min, max, mean, median, stddev, jitter, p95;104
final Duration elapsed;106
/// True when median/P95 were computed over only the newest samples.107
final bool windowed;108
const RttStats({109
required this.attempts,110
required this.successes,111
this.min,112
this.max,113
this.mean,114
this.median,115
this.stddev,116
this.jitter,117
this.p95,118
this.elapsed = Duration.zero,119
this.windowed = false,120
});121
int get failures => attempts - successes;122
double get successPercent => attempts == 0 ? 0 : 100 * successes / attempts;123
double get lossPercent => attempts == 0 ? 0 : 100 - successPercent;125
static const empty = RttStats(attempts: 0, successes: 0);127
/// Exact statistics of [rtts] (successful attempts in order) out of128
/// [attempts] attempts.129
factory RttStats.of(130
List<double> rtts, {131
required int attempts,132
Duration elapsed = Duration.zero,133
}) {134
if (rtts.isEmpty) {135
return RttStats(attempts: attempts, successes: 0, elapsed: elapsed);136
}137
final n = rtts.length;138
final mean = rtts.reduce((a, b) => a + b) / n;139
var variance = 0.0;140
for (final r in rtts) {141
variance += (r - mean) * (r - mean);142
}143
var jitter = 0.0;144
for (var i = 1; i < n; i++) {145
jitter += (rtts[i] - rtts[i - 1]).abs();146
}147
final sorted = [...rtts]..sort();148
return RttStats(149
attempts: attempts,150
successes: n,151
min: sorted.first,152
max: sorted.last,153
mean: mean,154
median: percentile(sorted, 50),155
stddev: math.sqrt(variance / n),156
jitter: n < 2 ? null : jitter / (n - 1),157
p95: n < 20 ? null : percentile(sorted, 95),158
elapsed: elapsed,159
);160
}162
Map<String, dynamic> toJson() => {163
'attempts': attempts,164
'successes': successes,165
'failures': failures,166
'successPercent': successPercent,167
'minMs': min,168
'maxMs': max,169
'meanMs': mean,170
'medianMs': median,171
'stddevMs': stddev,172
'jitterMs': jitter,173
'p95Ms': p95,174
'elapsedMs': elapsed.inMilliseconds,175
if (windowed) 'percentilesOverNewest': StatsAccumulator.window,176
};177
}179
/// Nearest-rank percentile of an ascending list.180
double percentile(List<double> sorted, double p) {181
final rank = (p / 100 * sorted.length).ceil().clamp(1, sorted.length);182
return sorted[rank - 1];183
}185
/// Statistics for long or continuous measurements in bounded memory.186
///187
/// Count, minimum, maximum, mean, standard deviation (Welford) and jitter188
/// are exact over all samples. Median and P95 are computed over the newest189
/// [window] successful samples once there are more than that.190
class StatsAccumulator {191
static const window = 10000;192
int attempts = 0, successes = 0;193
double? _min, _max, _last;194
double _mean = 0, _m2 = 0, _jitterSum = 0;195
int _jitterCount = 0;196
final _recent = ListQueue<double>();197
DateTime? start, end;199
void add(Sample s) {200
attempts++;201
start ??= s.time;202
end = s.time;203
if (!s.ok) return;204
final r = s.rttMs!;205
successes++;206
_min = _min == null ? r : math.min(_min!, r);207
_max = _max == null ? r : math.max(_max!, r);208
final delta = r - _mean;209
_mean += delta / successes;210
_m2 += delta * (r - _mean);211
if (_last != null) {212
_jitterSum += (r - _last!).abs();213
_jitterCount++;214
}215
_last = r;216
_recent.addLast(r);217
if (_recent.length > window) _recent.removeFirst();218
}220
RttStats stats([Duration? elapsed]) {221
final duration =222
elapsed ?? (start == null ? Duration.zero : end!.difference(start!));223
if (successes == 0) {224
return RttStats(attempts: attempts, successes: 0, elapsed: duration);225
}226
final sorted = _recent.toList()..sort();227
return RttStats(228
attempts: attempts,229
successes: successes,230
min: _min,231
max: _max,232
mean: _mean,233
median: percentile(sorted, 50),234
stddev: math.sqrt(_m2 / successes),235
jitter: _jitterCount == 0 ? null : _jitterSum / _jitterCount,236
p95: sorted.length < 20 ? null : percentile(sorted, 95),237
elapsed: duration,238
windowed: successes > window,239
);240
}241
}243
/// A bounded list of the newest samples, for the log and the chart.244
class SampleLog {245
final int capacity;246
final _items = ListQueue<Sample>();247
int dropped = 0;248
SampleLog([this.capacity = 5000]);249
void add(Sample s) {250
_items.addLast(s);251
if (_items.length > capacity) {252
_items.removeFirst();253
dropped++;254
}255
}257
List<Sample> get items => List.unmodifiable(_items);258
int get length => _items.length;259
bool get isEmpty => _items.isEmpty;260
}262
/// One measured series: a single IP version of a ping.263
class Series {264
final Family family;265
final InternetAddress address;266
final log = SampleLog();267
final stats = StatsAccumulator();268
Series(this.family, this.address);269
void add(Sample s) {270
log.add(s);271
stats.add(s);272
}273
}275
/// Result of resolving a target for one IP version.276
class FamilyTarget {277
final Family family;278
final List<InternetAddress> addresses;280
/// Why there is no address: 'noAddressFamily' (no A/AAAA record),281
/// 'literalOtherFamily' (an IP literal of the other version), or a282
/// failure code.283
final String? missing;284
const FamilyTarget(this.family, this.addresses, [this.missing]);285
bool get available => addresses.isNotEmpty;286
}288
class Resolution {289
final String host;290
final List<FamilyTarget> families;292
/// For automatic mode: the address the system resolver prefers.293
final InternetAddress? preferred;294
final int dnsMs;295
const Resolution(this.host, this.families, this.dnsMs, {this.preferred});296
FamilyTarget? of(Family f) {297
for (final t in families) {298
if (t.family == f) return t;299
}300
return null;301
}302
}304
typedef Lookup =305
Future<List<InternetAddress>> Function(String, InternetAddressType);307
Future<List<InternetAddress>> systemLookup(308
String host,309
InternetAddressType type,310
) => InternetAddress.lookup(311
host,312
type: type,313
).timeout(const Duration(seconds: 10));315
/// Whether a lookup failure means "no record of this type" rather than a316
/// broken resolver: an empty answer or the resolver's "no address" errors.317
bool _noRecord(Object e) {318
if (e is! SocketException) return false;319
final code = e.osError?.errorCode;320
// EAI_NODATA (Linux -5 / Darwin 7) and EAI_NONAME (Linux -2 / Darwin 8)321
// mean the name exists without records of the type, or does not exist.322
return code == -5 || code == 7 || code == -2 || code == 8;323
}325
/// Resolves [host] for [mode]. A/AAAA are looked up separately so that a326
/// missing record of one type is reported instead of silently using the327
/// other type.328
Future<Resolution> resolveTarget(329
String host,330
IpMode mode, {331
Lookup lookup = systemLookup,332
}) async {333
final watch = Stopwatch()..start();334
final literal = InternetAddress.tryParse(335
host.startsWith('[') && host.endsWith(']')336
? host.substring(1, host.length - 1)337
: host,338
);339
final wanted = switch (mode) {340
IpMode.v4 => [Family.v4],341
IpMode.v6 => [Family.v6],342
IpMode.dual => [Family.v4, Family.v6],343
IpMode.auto => <Family>[],344
};345
if (literal != null) {346
final family = Family.of(literal);347
if (mode == IpMode.auto) {348
return Resolution(349
host,350
[351
FamilyTarget(family, [literal]),352
],353
0,354
preferred: literal,355
);356
}357
return Resolution(host, [358
for (final f in wanted)359
f == family360
? FamilyTarget(f, [literal])361
: FamilyTarget(f, const [], 'literalOtherFamily'),362
], 0);363
}364
if (mode == IpMode.auto) {365
final all = await lookup(host, InternetAddressType.any);366
if (all.isEmpty) throw const SocketException('no address');367
final preferred = all.first;368
final families = <FamilyTarget>[];369
for (final f in Family.values) {370
final list = [371
for (final a in all)372
if (Family.of(a) == f) a,373
];374
if (list.isNotEmpty) families.add(FamilyTarget(f, list));375
}376
return Resolution(377
host,378
families,379
watch.elapsedMilliseconds,380
preferred: preferred,381
);382
}383
final results = await Future.wait([384
for (final f in wanted)385
lookup(host, f.type).then<FamilyTarget>(386
(list) {387
final own = [388
for (final a in list)389
if (Family.of(a) == f) a,390
];391
return own.isEmpty392
? FamilyTarget(f, const [], 'noAddressFamily')393
: FamilyTarget(f, own);394
},395
onError: (Object e) => _noRecord(e)396
? FamilyTarget(f, const [], 'noAddressFamily')397
: FamilyTarget(f, const [], 'dnsFailed'),398
),399
]);400
if (!results.any((r) => r.available)) {401
// Neither version has an address: the name does not resolve at all.402
throw const SocketException('lookup failed');403
}404
return Resolution(host, results, watch.elapsedMilliseconds);405
}407
/// The text of a TCP/ICMP ping report.408
String formatEndpoint(String address, int? port) {409
if (port == null) return address;410
return address.contains(':') ? '[$address]:$port' : '$address:$port';411
}413
/// Report formats for copying and export.414
enum ReportFormat { txt, csv, json }416
String _csv(Object? v) {417
final s = v?.toString() ?? '';418
return s.contains(RegExp(r'[",\n\r]')) ? '"${s.replaceAll('"', '""')}"' : s;419
}421
String _n(double? v) => v == null ? '' : v.toStringAsFixed(3);423
/// A ping report (TCP connect or ICMP echo) for [series].424
String pingReport(425
ReportFormat format, {426
required MeasureKind kind,427
required String host,428
required IpMode mode,429
required Map<String, Object?> settings,430
required List<Series> series,431
}) {432
final method = kind == MeasureKind.tcp ? 'TCP connect' : 'ICMP echo';433
switch (format) {434
case ReportFormat.json:435
return const JsonEncoder.withIndent(' ').convert({436
'tool': 'SM Mobile',437
'method': method,438
'host': host,439
'ipMode': mode.name,440
'settings': settings,441
'series': [442
for (final s in series)443
{444
'ipVersion': s.family.label,445
'address': s.address.address,446
'statistics': s.stats.stats().toJson(),447
if (s.log.dropped > 0) 'samplesOmitted': s.log.dropped,448
'samples': [for (final x in s.log.items) x.toJson()],449
},450
],451
});452
case ReportFormat.csv:453
final b = StringBuffer(454
'ip_version,address,port,seq,time_utc,outcome,rtt_ms,detail\n',455
);456
for (final s in series) {457
for (final x in s.log.items) {458
b.writeln(459
[460
x.family.label,461
x.address,462
x.port ?? '',463
x.seq,464
x.time.toUtc().toIso8601String(),465
x.outcome.name,466
_n(x.rttMs),467
x.detail ?? '',468
].map(_csv).join(','),469
);470
}471
}472
return b.toString();473
case ReportFormat.txt:474
final b = StringBuffer('SM Mobile · $method · $host (${mode.name})\n');475
settings.forEach((k, v) => b.writeln('$k: $v'));476
for (final s in series) {477
final st = s.stats.stats();478
b479
..writeln()480
..writeln('${s.family.label} ${s.address.address}');481
for (final x in s.log.items) {482
b.writeln(483
'#${x.seq} ${x.time.toLocal().toIso8601String()} '484
'${formatEndpoint(x.address, x.port)} ${x.outcome.name}'485
'${x.rttMs == null ? '' : ' ${_n(x.rttMs)} ms'}'486
'${x.detail == null ? '' : ' (${x.detail})'}',487
);488
}489
b.writeln(490
'attempts ${st.attempts}, ok ${st.successes}, failed ${st.failures}'491
' (${st.successPercent.toStringAsFixed(1)} % ok)',492
);493
if (st.successes > 0) {494
b.writeln(495
'rtt min/avg/median/max ${_n(st.min)}/${_n(st.mean)}/'496
'${_n(st.median)}/${_n(st.max)} ms, stddev ${_n(st.stddev)} ms, '497
'jitter ${_n(st.jitter)} ms'498
'${st.p95 == null ? '' : ', p95 ${_n(st.p95)} ms'}',499
);500
}501
}502
return b.toString();503
}504
}SHA-256
507092497445653f54f72fc80d641c8ed3166b9f58ee32441bdbf737213daa70CH-J Proprietary Software License 1.14 · Kod sumber disediakan di bawah CH-J Proprietary Software License 1.14. Ketersediaannya tidak mengubah terma lesen atau memberikan hak tambahan.
SHA-256 arkib
46e427a43e87af091004f36145590bda8fe90086dfb0f631afeb5029fb0b5655