CH-J SM Mobilepengurusan pelayan melalui SSH
Menu
Sumber yang diterbitkan

CH-J SM Mobile

Layari direktori dan fail untuk keluaran aplikasi tertentu.

Muat turun ZIP sumber
CH-J / SM Mobile 175 fail
sm-mobile-20261016T185235Z-8e93ecc1b59a Ditambah: · Dibina:
measure.dart Ditambah: Tarikh tidak direkodkan 14.4 KB · 504 baris
Kod Muat turun fail
1import 'dart:async';
2import 'dart:collection';
3import 'dart:convert';
4import 'dart:io';
5import 'dart:math' as math;
7/// Shared models of the network diagnostics: TCP connect ping, ICMP echo
8/// ping and traceroute. Every measurement runs directly from this device.
10/// Which IP versions a measurement uses.
11enum 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,
22enum 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;
33enum MeasureKind { tcp, icmp, trace }
35/// Outcome of one attempt.
36enum 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,
56/// One attempt of a ping (TCP or ICMP).
57class 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 };
92/// Summary statistics of a series of attempts.
93///
94/// RTT figures use successful attempts only; failures are counted
95/// separately. Definitions:
96/// * standard deviation: population standard deviation of the RTTs;
97/// * jitter: mean absolute difference between consecutive successful RTTs
98/// (as in RFC 3550 §6.4.1, without its smoothing);
99/// * median and P95: nearest-rank percentiles; P95 needs at least 20
100/// successful samples.
101class 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 of
128 /// [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 };
179/// Nearest-rank percentile of an ascending list.
180double percentile(List<double> sorted, double p) {
181 final rank = (p / 100 * sorted.length).ceil().clamp(1, sorted.length);
182 return sorted[rank - 1];
185/// Statistics for long or continuous measurements in bounded memory.
186///
187/// Count, minimum, maximum, mean, standard deviation (Welford) and jitter
188/// are exact over all samples. Median and P95 are computed over the newest
189/// [window] successful samples once there are more than that.
190class 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 }
243/// A bounded list of the newest samples, for the log and the chart.
244class 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;
262/// One measured series: a single IP version of a ping.
263class 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 }
275/// Result of resolving a target for one IP version.
276class 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 a
282 /// failure code.
283 final String? missing;
284 const FamilyTarget(this.family, this.addresses, [this.missing]);
285 bool get available => addresses.isNotEmpty;
288class 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 }
304typedef Lookup =
305 Future<List<InternetAddress>> Function(String, InternetAddressType);
307Future<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 a
316/// broken resolver: an empty answer or the resolver's "no address" errors.
317bool _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;
325/// Resolves [host] for [mode]. A/AAAA are looked up separately so that a
326/// missing record of one type is reported instead of silently using the
327/// other type.
328Future<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 == family
360 ? 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.isEmpty
392 ? 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);
407/// The text of a TCP/ICMP ping report.
408String formatEndpoint(String address, int? port) {
409 if (port == null) return address;
410 return address.contains(':') ? '[$address]:$port' : '$address:$port';
413/// Report formats for copying and export.
414enum ReportFormat { txt, csv, json }
416String _csv(Object? v) {
417 final s = v?.toString() ?? '';
418 return s.contains(RegExp(r'[",\n\r]')) ? '"${s.replaceAll('"', '""')}"' : s;
421String _n(double? v) => v == null ? '' : v.toStringAsFixed(3);
423/// A ping report (TCP connect or ICMP echo) for [series].
424String 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 b
479 ..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 }
SHA-256507092497445653f54f72fc80d641c8ed3166b9f58ee32441bdbf737213daa70

CH-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 arkib46e427a43e87af091004f36145590bda8fe90086dfb0f631afeb5029fb0b5655
Fail