::nsmcp::AccessDiagnosticTool
create ... \
[ -adminExample (default "") ] \
[ -adminSummary (default "") ] \
[ -authorizer:required authorizer:required ] \
[ -defaultMode (default "scan") ] \
[ -description:required description:required ] \
[ -inputSchema (default "
{
"type": "object",
"properties": {
"mode": {
"type": "string",
"enum": [
"scan",
"page"
],
"description": "Bot routing: aggregateTraffic true adds address-free source concentration and User-Agent rankings; path selects an exact queryless path. scan aggregates all pages in a background job, polled with log_results. page returns one bounded byte page. Slow requests remain paged."
},
"authenticationState": {
"type": "string",
"enum": [
"anonymous",
"authenticated",
"unknown"
],
"description": "Filter by logged authentication state; no identity is disclosed."
},
"aggregateTraffic": {
"type": "boolean",
"default": false,
"description": "Scan mode only: address-free source concentration and User-Agent rankings over all selected records, independent of evidence limits. No peer-disclosure permission required."
},
"path": {
"type": "string",
"description": "Exact queryless path, e.g. /bugtracker/openacs/"
},
"scanStrategy": {
"type": "string",
"enum": [
"window",
"full"
],
"default": "window",
"description": "Background scans: window seeks by completion timestamps with a 300s margin and explicit ordering assumptions; full examines the entire current file."
},
"lastMinutes": {
"type": "integer",
"minimum": 1,
"maximum": 1440,
"default": 60
},
"from": {
"type": "number"
},
"to": {
"type": "number"
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"default": 20
},
"pool": {
"type": "string",
"default": "bots"
},
"module": {
"type": "string",
"default": "nslog"
},
"category": {
"type": "string",
"enum": [
"matching-rule-in-target",
"matching-rule-other-pool-candidate",
"bot-like-unmatched-heuristic",
"no-target-rule-match",
"unknown",
"native-target-in-target",
"native-target-other-pool-candidate",
"bot-like-routed-elsewhere-heuristic",
"native-other-pool"
],
"description": "Bot routing only: retain evidence for this exact category. categoryTotals still covers all matching records; selectedMatches and filteredOutRecords distinguish selection from omitted evidence."
},
"includePeerAddresses": {
"type": "boolean",
"default": false
},
"beforeOffset": {
"type": "integer",
"minimum": 0
},
"sourceBytes": {
"type": "integer",
"minimum": 0
},
"sourceIdentity": {
"type": "string"
},
"mappingIdentity": {
"type": "string"
}
},
"additionalProperties": false
}
") ] \
[ -maxAggregateSources:integer (default "100000") ] \
[ -maxAggregateUserAgents:integer (default "4096") ] \
[ -maxReadBytes:integer (default "4194304") ] \
[ -name:required name:required ] \
[ -operation (default "bot") ] \
[ -outputSchema (default "") ] \
[ -peerRedactor:required peerRedactor:required ] \
[ -permission (default "diagnostics.logs.read") ] \
[ -redactor:required redactor:required ] \
[ -routing:required routing:required ] \
[ -seekMinimumBytes:integer (default "8388608") ] \
[ -service (default "") ]
Defined in /usr/local/ns/tcl/nsmcp/traffic-tools.tclClass Relations
- class: ::nx::Class
![[i]](/resources/acs-subsite/ZoomIn16.gif)
- superclass: ::nsmcp::Tool
![[i]](/resources/acs-subsite/ZoomIn16.gif)
::nx::Class create ::nsmcp::AccessDiagnosticTool \
-superclass ::nsmcp::Tool
Methods (to be applied on instances)
classify (scripted, public)
<instance of nsmcp::AccessDiagnosticTool
> classify record maps \
target
- Parameters:
- record (required)
- maps (required)
- target (required)
- Testcases:
-
No testcase defined.
set matches {}; set unknown 0; set index 0
foreach map $maps {
set id $index; incr index
if {[llength $map] ni {4 5}} {incr unknown; continue}
lassign $map method base glob inherit condition
if {$method ne [dict get $record method]} continue
if {$base ne "/" || $glob ne "*" || $inherit ne "inherit" || [llength $map]!=5} {incr unknown; continue}
if {[llength $condition]!=2 || ![string equal -nocase [lindex $condition 0] user-agent]} {incr unknown; continue}
if {![dict exists $record userAgent]} {incr unknown; continue}
if {[string match [lindex $condition 1] [dict get $record userAgent]]} {lappend matches $id}
}
if {[dict get $record pool] eq "unknown"} {
set category unknown
} elseif {[llength $matches]} {
set category [expr {[dict get $record pool] eq $target ? "matching-rule-in-target" : "matching-rule-other-pool-candidate"}]
} elseif {$unknown || ![dict exists $record userAgent]} {
set category unknown
} elseif {[regexp -nocase {bot|crawl|spider|baidu|knowledge} [dict get $record userAgent]] && [dict get $record pool] ne $target} {
set category bot-like-unmatched-heuristic
} else {set category no-target-rule-match}
return [dict create category $category matchingRuleIndexes $matches unknownRules $unknown] <instance of nsmcp::AccessDiagnosticTool
> contextComplete record \
snapshot
- Parameters:
- record (required)
- snapshot (required)
- Testcases:
-
No testcase defined.
if {[dict size [dict get $snapshot availability]]>0} {return false}
dict for {pool maps} [dict get $snapshot pools] {
foreach map $maps {
if {[llength $map] ni {4 5}} {return false}
if {[lindex $map 0] ne [dict get $record method]} {continue}
if {[llength $map]==4} {continue}
set condition [lindex $map 4]
if {[llength $condition]%2} {return false}
foreach {field pattern} $condition {
switch -nocase -- $field {
user-agent {
if {![dict exists $record userAgent] || [dict get $record userAgent] in {{} - {[REDACTED]}}} {return false}
}
x-ns-ip {
if {![ns_ip valid [dict get $record peerAddress]]} {return false}
}
default {return false}
}
}
}
}
return trueexecute (scripted, public)
<instance of nsmcp::AccessDiagnosticTool
> execute context \
arguments
- Parameters:
- context (required)
- arguments (required)
- Testcases:
-
No testcase defined.
set options [:options $arguments]
if {${:operation} eq "bot" && [dict get $options mode] eq "scan"} {
${:authorizer} require $context diagnostics.runtime.read
if {[dict get $options includePeerAddresses]} {${:authorizer} require $context diagnostics.logs.peers.read}
if {${:service} eq ""} {error "Routing background service is not configured"}
dict set options mode page
dict unset options lastMinutes
return [${:service} start $context routingSummary [dict create tool [self] options $options]]
}
if {[dict get $options aggregateTraffic]} {error "aggregateTraffic requires bot routing mode scan"}
return [:page $context $arguments] <instance of nsmcp::AccessDiagnosticTool
> nativeClassify server \
record snapshot target
- Parameters:
- server (required)
- record (required)
- snapshot (required)
- target (required)
- Testcases:
-
No testcase defined.
set result [dict create lookupMode native-context expectedPool unknown contextComplete [:contextComplete $record $snapshot] category unknown matchingRuleIndexes {} unknownRules 0 lookupError {}]
if {![dict get $result contextComplete]} {return $result}
if {[catch {${:routing} mapped $server $record} expected]} {
dict set result lookupError {Native route lookup failed for this record.}
return $result
}
dict set result expectedPool $expected
set actual [dict get $record pool]
if {$actual eq "unknown"} {return $result}
if {$expected eq $target} {
dict set result category [expr {$actual eq $target ? "native-target-in-target" : "native-target-other-pool-candidate"}]
} elseif {[dict exists $record userAgent] &&
[regexp -nocase {bot|crawl|spider|baidu|knowledge} [dict get $record userAgent]] && $actual ne $target} {
dict set result category bot-like-routed-elsewhere-heuristic
} else {
dict set result category native-other-pool
}
return $resultpage (scripted, public)
<instance of nsmcp::AccessDiagnosticTool
> page context arguments \
[ floor ] [ aggregationVar ]
- Parameters:
- context (required)
- arguments (required)
- floor (optional, defaults to
"0")
- aggregationVar (optional)
- Testcases:
-
No testcase defined.
if {$aggregationVar ne ""} {upvar 1 $aggregationVar traffic}
set options [:options $arguments]
set server [$context server]
set peers [dict get $options includePeerAddresses]
if {$peers} {${:authorizer} require $context diagnostics.logs.peers.read}
set redactor [expr {$peers ? ${:peerRedactor} : ${:redactor}}]
set result [list server string $server from number [dict get $options from] to number [dict get $options to]]
foreach key {path authenticationState} {if {[dict exists $options $key]} {lappend result $key string [dict get $options $key]}}
set snapshot {}; set maps {}; set mappingIdentity {}
set lookupMode fallback-rule-membership
set routeCache {}
if {${:operation} eq "bot"} {
${:authorizer} require $context diagnostics.runtime.read
set snapshot [${:routing} snapshot $server]
set target [dict get $options pool]
if {![dict exists $snapshot pools $target]} {
return [list {} object [concat $result [list available boolean false reason string {Effective target-pool mappings unavailable.}]]]
}
set maps [dict get $snapshot pools $target]
if {[${:routing} contextSupported $server]} {set lookupMode native-context}
set category [dict get $options category]
if {$category ne "" && $category ne "unknown"} {
set nativeCategory [expr {$category in {
native-target-in-target native-target-other-pool-candidate
bot-like-routed-elsewhere-heuristic native-other-pool
}}]
if {$nativeCategory != ($lookupMode eq "native-context")} {
return -code error -errorcode {NSMCP RPC -32602} "Category '$category' is not produced by lookupMode '$lookupMode'; select a category for the current mode."
}
}
set mappingIdentity [ns_crypto::md string -digest sha256 -- [list [dict get $snapshot pools] $lookupMode]]
if {[dict exists $options mappingIdentity] && [dict get $options mappingIdentity] ne $mappingIdentity} {error "Effective mappings changed; start a new analysis"}
lappend result pool string $target lookupMode string $lookupMode mappingIdentity string $mappingIdentity {*}[${:routing} encodeSnapshot $snapshot]
}
if {${:operation} eq "slow"} {
set thresholds {}
foreach key {slowlogconntime slowlogfiltertime slowlogqueuetime} {
set value [ns_config ns/server/$server/module/[dict get $options module] $key {}]
lappend thresholds $key string $value
}
lappend result configuredThresholds object $thresholds
}
set path [:source $server $options]
if {$path eq "" || ![file isfile $path] || ![file readable $path]} {
return [list {} object [concat $result [list available boolean false reason string {Configured log is missing, disabled or unreadable.}]]]
}
set path [file normalize $path]
file stat $path before
set channel [open $path rb]
try {
set prefix [read $channel [expr {min(256,$before(size))}]]
set identity [ns_crypto::md string -digest sha256 -- [list $path $before(dev) $before(ino) $prefix]]
set sourceBytes $before(size); set end $sourceBytes
if {[dict exists $options beforeOffset]} {
if {$identity ne [dict get $options sourceIdentity] || $sourceBytes<[dict get $options sourceBytes]} {error "Log rotated or truncated; start a new query"}
set sourceBytes [dict get $options sourceBytes]
set end [dict get $options beforeOffset]
}
set start [expr {max($floor,$end-${:maxReadBytes})}]
seek $channel $start
set bytes [read $channel [expr {$end-$start}]]
set discarded 0
if {$start>0} {
seek $channel [expr {$start-1}]
if {[read $channel 1] ne "\n"} {
set newline [string first \n $bytes]
if {$newline>=0} {set discarded [expr {$newline+1}]} else {set discarded [string length $bytes]}
}
}
file stat $path after
if {[list $before(dev) $before(ino)] ne [list $after(dev) $after(ino)] || $after(size)<$sourceBytes} {error "Log changed during read; start a new query"}
} finally {close $channel}
set position [expr {$start+$discarded}]
set cursor $position
if {$cursor==$end && $start>0} {set cursor $start}
set bytes [string range $bytes $discarded end]
set pending [expr {$bytes ne "" && [string index $bytes end] ne "\n"}]
set records {}; set groups {}; set totals {}; set matches 0; set unrecognized 0; set oversized 0; set missingUA 0; set missingPool 0
set selectedMatches 0
foreach line [lrange [split $bytes \n] 0 end-1] {
set offset $position
incr position [expr {[string length $line]+1}]
if {[string length $line]>65536} {incr oversized; continue}
set record [::nsmcp::accessRecordParser parse [encoding convertfrom utf-8 [string trimright $line \r]]]
if {$record eq ""} {incr unrecognized; continue}
set at [dict get $record startTime]
if {$at<[dict get $options from] || $at>=[dict get $options to]} continue
if {[dict exists $options path] && [dict get $record path] ne [dict get $options path]} continue
if {[dict exists $options authenticationState] && [dict get $record authenticationState] ne [dict get $options authenticationState]} continue
incr matches
dict set record offset $offset; dict set record endOffset $position
if {![dict exists $record userAgent]} {incr missingUA}
if {[dict get $record pool] eq "unknown"} {incr missingPool}
if {${:operation} eq "bot"} {
if {$lookupMode eq "native-context"} {
set routeKey [list [dict get $record method] [dict get $record path] [dict get $record peerAddress] [dict get $record pool] [dict exists $record userAgent] [expr {[dict exists $record userAgent] ? [dict get $record userAgent] : ""}]]
if {[dict exists $routeCache $routeKey]} {
set classification [dict get $routeCache $routeKey]
} else {
set classification [:nativeClassify $server $record $snapshot [dict get $options pool]]
if {[dict size $routeCache]<4096} {dict set routeCache $routeKey $classification}
}
} else {
set classification [:classify $record $maps [dict get $options pool]]
dict set classification lookupMode fallback-rule-membership
dict set classification expectedPool unknown
dict set classification contextComplete false
dict set classification lookupError {}
}
set category [dict get $classification category]
dict incr totals $category
if {[dict get $options category] ne "" && [dict get $options category] ne $category} {continue}
incr selectedMatches
if {$aggregationVar ne ""} {:accumulateTraffic traffic $record}
set record [$redactor record $record]
if {[dict exists $record userAgent]} {dict set record userAgent [$redactor message [dict get $record userAgent]]}
set record [dict merge $record $classification]
set key [list [dict get $record category] [dict get $record method] [dict get $record path] [dict get $record pool] [dict get $record status]]
lappend key [expr {[dict exists $record userAgent] ? [dict get $record userAgent] : "unknown"}]
lappend key [dict get $record expectedPool] [dict get $record contextComplete] [dict get $record lookupError]
if {$peers} {lappend key [dict get $record peerAddress]}
if {![dict exists $groups $key]} {
if {[dict size $groups]>=[dict get $options limit]} {
set priority [:evidencePriority $category]
set victim {}
set worst $priority
dict for {existingKey existingGroup} $groups {
set rank [:evidencePriority [dict get $existingGroup category]]
if {$rank>$worst} {
set worst $rank
set victim $existingKey
}
}
if {$victim eq ""} {continue}
dict unset groups $victim
}
set group [dict create method [dict get $record method] path [$redactor queryValues [dict get $record path]] pool [dict get $record pool] status [dict get $record status] userAgent [lindex $key 5] category [dict get $record category] matchingRuleIndexes [dict get $record matchingRuleIndexes] unknownRules [dict get $record unknownRules] sampleRequestIdentifier [dict get $record requestIdentifier] firstAt $at lastAt $at count 0 runTotal 0 runMax 0 timingCount 0]
foreach field {lookupMode expectedPool contextComplete lookupError} {
dict set group $field [dict get $record $field]
}
if {$peers} {dict set group peerAddress [dict get $record peerAddress]}
dict set groups $key $group
}
set group [dict get $groups $key]; dict incr group count
dict set group firstAt [expr {min([dict get $group firstAt],$at)}]
dict set group lastAt [expr {max([dict get $group lastAt],$at)}]
if {[dict exists $record runTime]} {dict incr group timingCount; dict set group runMax [expr {max([dict get $group runMax],[dict get $record runTime])}]; dict set group runTotal [expr {[dict get $group runTotal]+[dict get $record runTime]}]}
dict set groups $key $group
} else {
set record [$redactor record $record]
if {[dict exists $record userAgent]} {dict set record userAgent [$redactor message [dict get $record userAgent]]}
lappend records $record
set records [lrange $records end-[expr {[dict get $options limit]-1}] end]
}
}
if {${:operation} eq "bot"} {
set latest [${:routing} snapshot $server]
set latestIdentity [ns_crypto::md string -digest sha256 -- [list [dict get $latest pools] $lookupMode]]
if {$latestIdentity ne $mappingIdentity} {error "Effective mappings changed during analysis; start a new query"}
}
set entries {}; set i 0
if {${:operation} eq "bot"} {
set ordered {}
dict for {key group} $groups {
lappend ordered [list [:evidencePriority [dict get $group category]] $group]
}
set records {}
foreach pair [lsort -integer -index 0 $ordered] {lappend records [lindex $pair 1]}
set records [lreverse $records]
} else {
set selectedMatches $matches
}
set returnedRecords {}; set resultBytes 0
foreach record [lreverse $records] {
set fields [:recordFields $record]
set bytes [string length [ns_json value -- [list {} object $fields]]]
if {$resultBytes+$bytes>65536} {
continue
}
incr resultBytes $bytes
lappend returnedRecords $record
lappend entries $i object $fields
incr i
}
set records $returnedRecords
set counts {}; dict for {key value} $totals {lappend counts $key number $value}
set omittedCounts {}
set returnedMatches [llength $records]
if {${:operation} eq "bot"} {
set remaining {}
dict for {category value} $totals {
if {[dict get $options category] eq "" || [dict get $options category] eq $category} {
dict set remaining $category $value
}
}
set returnedMatches 0
foreach record $records {
set count [dict get $record count]
incr returnedMatches $count
dict incr remaining [dict get $record category] [expr {-$count}]
}
dict for {category value} $remaining {lappend omittedCounts $category number $value}
}
set next {}
if {$cursor>$floor} {
foreach key {from to limit pool module includePeerAddresses mode} {
lappend next $key [expr {$key eq "includePeerAddresses" ? "boolean" : ($key in {from to limit} ? "number" : "string")}] [dict get $options $key]
}
if {${:operation} eq "bot" && [dict get $options category] ne ""} {
lappend next category string [dict get $options category]
}
foreach key {path authenticationState} {if {[dict exists $options $key]} {lappend next $key string [dict get $options $key]}}
lappend next beforeOffset number $cursor sourceBytes number $sourceBytes sourceIdentity string $identity
if {$mappingIdentity ne ""} {lappend next mappingIdentity string $mappingIdentity}
}
set evidencePolicy {Newest matching slow-request records within evidence limits.}
if {${:operation} eq "bot"} {
set evidencePolicy {Routing candidates first, then unknowns, then routine groups; first encounter within priority. Category filtering selects evidence only; categoryTotals still includes all observed records.}
}
lappend result available boolean true sourcePath string $path sourceIdentity string $identity matches number $matches categoryTotals object $counts records array $entries selectedCategory string [dict get $options category] selectedMatches number $selectedMatches filteredOutRecords number [expr {$matches-$selectedMatches}] omittedRecords number [expr {$selectedMatches-$returnedMatches}] omittedCategoryTotals object $omittedCounts evidencePolicy string $evidencePolicy more boolean [expr {$cursor>$floor}] nextArguments object $next coverage object [list sourceBytes number $sourceBytes examinedFromOffset number [expr {$start+$discarded}] examinedToOffset number $end unreadEarlierBytes number $cursor pendingFinalRecord boolean $pending unrecognizedRecords number $unrecognized oversizedRecords number $oversized missingUserAgent number $missingUA missingPool number $missingPool rotationsExamined boolean false windowComplete boolean false limitations string {Page-local counts; sum disjoint pages. Current file only. Full-window coverage is not certified. Mappings reflect query time, not historical configuration. lookupMode identifies native-context or fallback-rule-membership. Native lookup uses current URL/path precedence and logged User-Agent and valid peer IP, without disclosing peers by default. Unlogged context predicates remain unknown; completeness checks conservatively inspect all same-method rules. Older versions retain root wildcard User-Agent membership analysis, not precedence replay. Bot-like unmatched uses a separate case-insensitive heuristic, not proof of a bot. Slow-log timings are access-log timings, not SQL profiles.}]
return [list {} object $result]scanStep (scripted, public)
<instance of nsmcp::AccessDiagnosticTool
> scanStep context state
- Parameters:
- context (required)
- state (required)
- Testcases:
-
No testcase defined.
set options [dict get $state options]
if {![dict exists $state bounds]} {
if {![dict exists $options scanStrategy]} {dict set options scanStrategy window}
set bounds [:scanBounds $context $options]
dict set state bounds $bounds
if {[dict get $bounds strategy] eq "completion-window"} {
dict set options beforeOffset [dict get $bounds to]
dict set options sourceBytes [dict get $bounds sourceBytes]
dict set options sourceIdentity [dict get $bounds identity]
}
}
set bounds [dict get $state bounds]
set aggregateTraffic [expr {[dict exists $options aggregateTraffic] && [dict get $options aggregateTraffic]}]
set traffic {}
if {$aggregateTraffic} {
if {[dict exists $state traffic]} {set traffic [dict get $state traffic]} else {
set traffic {sources {} agents {} authenticationStates {} requestCount 0 status503Count 0 successfulCount 0 successfulTimingCount 0 successfulRunTotal 0.0 unknownPeerCount 0 untrackedSourceRecords 0 unknownUserAgentCount 0 untrackedUserAgentRecords 0}
dict set traffic salt [ns_crypto::randombytes -encoding hex 32]
}
}
set page [:page $context [:argumentTriples $options] [dict get $bounds from] [expr {$aggregateTraffic ? "traffic" : ""}]]
if {$aggregateTraffic} {dict set state traffic $traffic}
set value [ns_json parse [ns_json value -- $page]]
if {![dict get $value available]} {return [list $page {} {}]}
if {![dict exists $state fields]} {
dict set state fields [lindex $page 2]
dict set state groups {}
dict set state totals {}
dict set state matches 0
dict set state selected 0
dict set state pages 0
dict set state coverage [dict get $value coverage]
} else {
set initial [ns_json parse [ns_json value -- [list {} object [dict get $state fields]]]]
foreach key {sourceIdentity mappingIdentity from to lookupMode selectedCategory} {
if {[dict get $initial $key] ne [dict get $value $key]} {error "Routing scan identity changed; start a new query"}
}
set coverage [dict get $state coverage]
set nextCoverage [dict get $value coverage]
foreach key {unrecognizedRecords oversizedRecords missingUserAgent missingPool} {
dict incr coverage $key [dict get $nextCoverage $key]
}
dict set coverage pendingFinalRecord [expr {[dict get $coverage pendingFinalRecord] || [dict get $nextCoverage pendingFinalRecord]}]
dict set coverage examinedFromOffset [dict get $nextCoverage examinedFromOffset]
dict set coverage unreadEarlierBytes [dict get $nextCoverage unreadEarlierBytes]
dict set state coverage $coverage
}
dict incr state pages
dict incr state matches [dict get $value matches]
dict incr state selected [dict get $value selectedMatches]
set totals [dict get $state totals]
dict for {category count} [dict get $value categoryTotals] {dict incr totals $category $count}
dict set state totals $totals
set groups [dict get $state groups]
foreach record [dict get $value records] {
set key [:aggregateKey $record]
if {[dict exists $groups $key]} {
set group [dict get $groups $key]
foreach field {count timingCount} {dict incr group $field [dict get $record $field]}
dict set group runTotal [expr {[dict get $group runTotal]+[dict get $record runTotal]}]
dict set group runMax [expr {max([dict get $group runMax],[dict get $record runMax])}]
dict set group firstAt [expr {min([dict get $group firstAt],[dict get $record firstAt])}]
dict set group lastAt [expr {max([dict get $group lastAt],[dict get $record lastAt])}]
dict set groups $key $group
} else {
if {[dict size $groups]>=[dict get $options limit]} {
set worst [:evidencePriority [dict get $record category]]
set victim {}
dict for {otherKey otherRecord} $groups {
set rank [:evidencePriority [dict get $otherRecord category]]
if {$rank>$worst} {set worst $rank; set victim $otherKey}
}
if {$victim eq ""} {continue}
dict unset groups $victim
}
dict set groups $key $record
}
}
set ordered {}
dict for {key group} $groups {lappend ordered [list [:evidencePriority [dict get $group category]] $key $group]}
set groups {}; set entries {}; set index 0; set bytes 0; set returned 0
set omitted {}
dict for {category count} $totals {
if {[dict get $value selectedCategory] eq "" || [dict get $value selectedCategory] eq $category} {dict set omitted $category $count}
}
foreach pair [lsort -integer -index 0 $ordered] {
lassign $pair priority key record
set fields [:recordFields $record]
set size [string bytelength [ns_json value -- [list {} object $fields]]]
if {$bytes+$size>65536} {continue}
incr bytes $size
dict set groups $key $record
lappend entries $index object $fields
incr index
incr returned [dict get $record count]
dict incr omitted [dict get $record category] [expr {-[dict get $record count]}]
}
dict set state groups $groups
set counts {}; dict for {category count} $totals {lappend counts $category number $count}
set omittedCounts {}; dict for {category count} $omitted {lappend omittedCounts $category number $count}
set coverage [dict get $state coverage]
set coverageFields {}
foreach {key type original} [ns_json triples getvalue -output triples -path coverage -- $page] {
lappend coverageFields $key $type [dict get $coverage $key]
}
lappend coverageFields scanStrategy string [dict get $bounds strategy] seekReason string [dict get $bounds reason] seekMarginSeconds number 300 seekProbeBytes number [dict get $bounds probeBytes] skippedEarlierBytes number [dict get $bounds from] skippedLaterBytes number [expr {[dict get $bounds sourceBytes]-[dict get $bounds to]}] completionOrderAssumed boolean [expr {[dict get $bounds strategy] eq "completion-window"}]
lappend coverageFields pages number [dict get $state pages] aggregationScope string all-examined-pages sourceFullyExamined boolean [expr {[dict get $coverage unreadEarlierBytes]==0 && [dict get $coverage examinedToOffset]==[dict get $coverage sourceBytes]}]
set fields {}
foreach {key type original} [dict get $state fields] {
switch -- $key {
matches {set original [dict get $state matches]}
selectedMatches {set original [dict get $state selected]}
filteredOutRecords {set original [expr {[dict get $state matches]-[dict get $state selected]}]}
categoryTotals {set original $counts}
omittedRecords {set original [expr {[dict get $state selected]-$returned}]}
omittedCategoryTotals {set original $omittedCounts}
records {set original $entries}
more {set original false}
nextArguments {set original {}}
coverage {set original $coverageFields}
}
lappend fields $key $type $original
}
if {$aggregateTraffic} {
lappend fields trafficAggregation object [:trafficTriples $traffic [expr {[dict get $options to]-[dict get $options from]}] [expr {![dict get $value more]}]]
}
lappend fields scanComplete boolean [expr {![dict get $value more]}]
set continuation {}
if {[dict get $value more]} {
set nextOptions [dict get $value nextArguments]
dict set nextOptions aggregateTraffic $aggregateTraffic
dict set state options $nextOptions
set continuation $state
}
set progress [list examinedBytes number [expr {[dict get $bounds to]-[dict get $coverage unreadEarlierBytes]}] sourceBytes number [expr {[dict get $bounds to]-[dict get $bounds from]}]]
return [list [list {} object $fields] $continuation $progress]validate (scripted, public)
<instance of nsmcp::AccessDiagnosticTool
> validate arguments
- Parameters:
- arguments (required)
- Testcases:
-
No testcase defined.
if {[catch {:options $arguments} message]} {return -code error -errorcode {NSMCP RPC -32602} $message}