#150238 - 2005-10-19 10:16 PM
Script sanity checker
|
Glenn Barnas
KiX Supporter
   
Registered: 2003-01-28
Posts: 4402
Loc: New Jersey
|
Hi, everyone
I've been working on a large application project, and using Kix to prototype the code / logic. I've created a UDF that scans the KiX code and locates things like variable names defined as both Global and Local, undeclared variable names, and variable names declared but never referenced. It also does some basic tests of matched quotes and parens (but not across multiple lines).
I've circulated it offline for a bit, and am still in early coding stages, so will post it here instead of the UDF section for now. I'd like to get some feedback, suggestions, feature requests, etc, before I do a final revision and post it. This release is working fairly well, and has proven useful in validating my declarations and other basic syntax.
Just pass the UDF the name of a kix source file, or a string containing the script source (and set Mode to 1)
Glenn
NOTE: After editing this to post the updated version, the code became truncated. It's available for download on my web site. It will be embedded in a small demo script.
Code:
;; ;;====================================================================== ;; ;;FUNCTION Sanity() ;; ;;ACTION Performs a "sanity check" on KiX scripts ;; ;;AUTHOR Glenn Barnas ;; ;;VERSION 0.95 - PreRelease ;; Works with files or tlobs ;; ;;SYNTAX Sanity( Script [, Mode] ) ;; ;;PARAMETERS Script - REQUIRED, script to review * ;; If a string, the name of the file to open ;; If an array, the file data ;; ;; Mode - OPTIONAL, Defines data format, Mode 0 is default ;; MODE=0 - Script is the name of the file to open ;; MODE=1 - Script is a string containing the file data ;; ;;REMARKS Developed as part of KGen, the Kix Script Generator, this ;; UDF performs basic sanity checks and reports the results. ;; * Generate list of Declared VarNames - Global, Main, & per UDF ;; * Identify VarNames declared by both DIM and GLOBAL ;; * Identify VarNames that were referenced but not declared ;; * Identify VarNamed Declared but not referenced ;; * Identify suspected use of Vars or Macros in strings ;; * Identify suspected mismatched single & double quotes ;; * Identify suspected mismatched parens ;; ;; Quote and Paren matching DOES NOT cross line boundaries! ;; This is intentional at this time, since KiX does not employ ;; a line-continuation char, no assumption of when a closing char ;; is found will be made. ;; ;; The goal is not to have "Zero Warnings", but to be aware ;; that the warnings should be reviewed and understood. ;; YOU are the ultimate authority of what you've coded! ;; ;;RETURNS Array of warning messages in CSV format ;; FuncName, Line, VarName, Warning_ID, Extra_Info, Message ;; Warning IDs: ;; 1 - Recursive Function Definition ;; 2 - Variable declared as local & global (Extra=Global Def Line) ;; 3 - Undeclared variable ;; 4 - Variable inside string ;; 5 - Unterminated single quote ;; 6 - Unterminated double quote ;; 7 - Mismatched parenthesis ;; 8 - Variable declared but not referenced ;; ;; ;;DEPENDENCIES none ;; ;;TESTED WITH W2K, WXP, W2K3 ;; ;;EXAMPLES Sanity('MyScript_or_UDF', 0) ; Function Sanity($_File, OPTIONAL $_Mode)
Dim $_, $_Line, $_Ctr, $_SQF, $_DQF, $_VF, $_FF, $_PF, $_DT, $_Fn, $_Vt Dim $_C, $_P, $_E, $_G, $_L, $_ECt, $_SectList, $_KeyList, $_Var, $_Vars
; ========================== Dim $_MaxLines $_MaxLines = 10000 ; set maximum input file buffer size, in lines Dim $_FData[$_MaxLines] ; ==========================
Dim $_Warning[1000] ; Warning array
$_Ctr = 0 ; Line Counter - 0-based, post-incremented $_SQF = 0 ; Single Quote Flag: 0=inactive; 1=active; -1=active,embedded in other string $_DQF = 0 ; Double Quote Flag: 0=inactive; 1=active; -1=active,embedded in other string $_VF = 0 ; Variable Flag: true while assembling a var name $_FF = 0 ; Function Flag: true while a UDF is active $_PF = 0 ; Paren Flag: count of parens; "(" increments, ")" decrements $_ECt = 0 ; count of errors/warnings $_Fn = 'Main' ; name of active function $_DT = 0 ; Declaration type
; $_Vt defines characters not allowed in a variable name (denoting the end of a var name!) $_Vt = "-+*/\,.?&=@@()[]<>"+Chr(9)+Chr(32)+Chr(34)+Chr(36)+Chr(39)
; Variable Info: ; $_ Temp var, holds anything, lifespan is no more than 4 lines! ; $_G, $_L Hold results of varname queries for Global or Local vars. ; $_C Current character when parsing line. ; $_P, $_E Current and ending char position when parsing line. ; $_Var Name of active variable . ; $_Vars List of variables found. ; $_Line Currently active source line - may be trimmed or otherwise modified. ; $_FData[] Array of original source lines, 10,000 line max as currently defined ; This array will exactly represent the original file data, except that ; Multi-line declarations will be combined onto a single line, and ; leading / trailing spaces are trimmed. ; $_SectList List of sections of the INI file that are being enumerated. ; $_KeyList List of keys in one section of the INI file that are being enumerated.
; Start with a fresh index file Del '.\VarInfo.ini'
;============================================================================================= ; Open & load the source file into an array ;============================================================================================= If $_Mode = 0 If Open(5, $_File, 2) = 0 While Not @ERROR And $_Ctr < $_MaxLines $_FData[$_Ctr] = Trim(ReadLine(5)) ; read the line, trimming spaces $_Ctr = $_Ctr + 1 Loop ReDim Preserve $_FData[$_Ctr - 1] $_ = Close(5) EndIf Else $_FData = Split($_FILE, @CRLF) EndIf
;============================================================================================= ; PASS 1 ; Parse the file data, looking for all function and variable declaration statements. ; Variable declaration statements can span multiple lines (which are consolidated in ; the array for further processing). This pass only prepares the raw data for further ; processing. Note that when a reference is made to a consolidated line, it is made ; to the first line of reference (Declaration Line), and not the actual line in the ; source file. ;=============================================================================================
$_Ctr = 0 ; init array/line pointer
While $_Ctr <= UBound($_FData) $_Line = $_FData[$_Ctr] $_Ctr = $_Ctr + 1 ; data is 0 indexed, references are 1 indexed
$_DT = 0 ; Start with _DT undefined $_E = 0
If $_Line <> '' ; skip blank lines
; Check for "dim $" and "global $" inside other code, and convert line to place the declaration ; on the left edge of the line so the processing that follows will find it properly. $_L = InStr($_Line, ' Dim ' + Chr(36)) $_G = InStr($_Line, ' Global ' + Chr(36)) If $_L > 1 Or $_G > 1 ; only 1 will be found $_SQF = 0 $_DQF = 0 ; read from beginning of line to declaration, setting quote flags and checking for comments ; Ignore the declaration if it exists within quotes, or in a comment For $_P = 1 to $_L + $_G ; combine $_C = SubStr($_Line, $_P, 1) Select ; set the quote flags: 0=not active, 1=active alone, -1=active inside other quote Case $_C = Chr(34) And Not $_DQF ; opening double quote If $_SQF $_DQF = -1 Else $_DQF = 1 EndIF Case $_C = Chr(34) And $_DQF ; closing double quote $_DQF = 0 If $_SQF = -1 $_SQF = 0 EndIf
Case $_C = Chr(39) And Not $_SQF ; opening single quote If $_DQF <> 0 $_SQF = -1 Else $_SQF = 1 EndIF Case $_C = Chr(39) And $_SQF ; closing single quote $_SQF = 0 If $_DQF = -1 $_DQF = 0 EndIf
Case $_C = ';' And $_SQF = 0 And $_DQF = 0 ; comment - ignore rest of line $_E = 1 EndSelect Next
; If one quote (any type) or a comment tag exist before the declaration, it isn't valid ; process the line only if none of the flags are active If $_DQF = 0 And $_SQF = 0 And $_E = 0 ; Declaration is valid $_ = Split(SubStr($_Line, $_L + $_G + 1), ' ') ; break into words, first is "Global" $_Line = $_[0] + ' ' For $_P = 1 to UBound($_) If Left($_[$_P], 1) = Chr(36) ; If first char is $, add to line $_Line = $_Line + $_[$_P] EndIF Next EndIF ; active flag
EndIf ; Dim or Global in code
; Line is ready for evaluation - find function, dim, and global declarations
Select
; =============================================================================== ; Check for function Start Case InStr($_Line, 'Function ') = 1 $_Fn = Trim(Split(SubStr($_Line, 10), '(')[0]) ; warn if defining a function without terminating the prior definition If $_FF $_ECt = $_ECt + 1 $_Warning[$_Ect] = $_Fn + ',' + $_Ctr + ',' + $_Var + ',' + 1 + ',' + '' + ',' + 'Recursive function definition, Prior function definition may not be properly terminated.', '', $_Fn, $_Ctr EndIF
; define the reference to the function, and turn on the active Function Flag $_FF = 1 $_ = WriteProfileString('.\VarInfo.ini', $_Fn, 'DefinedOnLine', $_Ctr)
; include the Fn name as a declared var $_ = WriteProfileString('.\VarInfo.ini', $_Fn, cHR(36) + $_Fn, '1,' + CStr($_Ctr))
; mark any passed arguments as defined variables - find text between parens & remove spaces $_Vars = Join(Split(Split(Split($_Line, Chr(40))[1], Chr(41))[0], ' '), '') If $_Vars <> '' For Each $_Var in Split($_Vars, ',') If Left($_Var, 9) = 'optional' + Chr(36) ; check for "Optional$VarName" & trim it $_Var = SubStr($_Var, 9) EndIf If Trim($_Var) <> 'Optional' ; exclude "optional" tags $_ = WriteProfileString('.\VarInfo.ini', $_Fn, Trim($_Var), '1,' + CStr($_Ctr)) EndIf Next EndIF ; Function ?
; Check for Function End Case InStr($_Line, 'EndFunction') = 1 $_FF = 0 $_Fn = 'Main' ; EndFunction ?
; =============================================================================== ; Check for Variable Declaration statements - can be one of: ; ; Declare Varname, varname... (type 1/2) ; ; Declare Varname, varname... , (type 3/4) ; more_var_names..., last_var_name ; ; Declare (type 5/6) ; Varname, varname... , ; more_var_names..., last_var_name ; ; "Declare" can be "Global" or "Dim"
Case InStr($_Line, 'Global') = 1 ; found a GLOBAL declaration $_Line = Trim(Split($_Line, ';')[0]) ; remove comment portion Select Case $_Line = 'Global' ; type 5 $_DT = 5 $_Line = $_Line + ' ' Case Right($_Line, 1) = ',' ; type 3 $_DT = 3 Case 1 ; type 1 $_DT = 1 EndSelect
Case InStr($_Line, 'Dim') = 1 $_Line = Trim(Split($_Line, ';')[0]) ; remove comment portion Select Case $_Line = 'Dim' ; type 6 $_DT = 6 $_Line = $_Line + ' ' Case Right($_Line, 1) = ',' ; type 4 $_DT = 4 Case 1 ; type 2 $_DT = 2 EndSelect
EndSelect
; If $_DT > 2, source lines are split. ; collect & combine the following lines until none end in ',' before continuing ; The data in the array source is combined - this is the If $_DT > 2 $_ = $_Ctr - 1 ; remember where the declaration started Do $_Line = $_Line + $_FData[$_Ctr] $_FData[$_Ctr] = '' ; clear the extended source line $_Ctr = $_Ctr + 1 Until Right($_Line, 1) <> ',' $_FData[$_] = $_Line ; write the combined source line EndIf
; Lines are combined, process either basic DIM or GLOBAL statements If $_DT = 2 Or $_DT = 4 Or $_DT = 6 ; DIM ; $_Line = Split($_Line, ';')[0] ; remove comment portion $_E = Len($_Line) For $_P = 5 to $_E $_C = SubStr($_Line, $_P, 1)
Select Case $_C = Chr(36) And $_VF = 0 ; found "$" $_Var = $_C If $_P = $_E ; trap for "Dim $" by itself $_VF = 0 $_ = WriteProfileString('.\VarInfo.ini', $_Fn, Trim($_Var), '1,' + CStr($_Ctr)) Else $_VF = 1 EndIf
Case $_VF = 1 And (InStr($_Vt, $_C) Or $_P = $_E) ; terminating char $_VF = 0 If $_P = $_E And Not InStr($_Vt, $_C) ; EOL - add last char if needed $_Var = $_Var + $_C EndIf $_ = WriteProfileString('.\VarInfo.ini', $_Fn, Trim($_Var), '1,' + CStr($_Ctr))
If $_C = '[' $_VF = -1 EndIf ; handle arrays
Case $_VF = -1 ; skip chars betweet [ and ] If $_C = ']' $_VF = 0 EndIf
Case $_VF = 1 And Not InStr($_Vt, $_C) ; build var name $_Var = $_Var + $_C EndSelect Next EndIf
If $_DT = 1 Or $_DT = 3 Or $_DT = 5 ; GLOBAL ; $_Line = Split($_Line, ';')[0] ; remove comment portion $_E = Len($_Line) For $_P = 8 to $_E $_C = SubStr($_Line, $_P, 1)
Select Case $_C = Chr(36) And $_VF = 0 ; found "$" $_Var = $_C If $_P = $_E ; trap for "Global $" by itself (rare) $_VF = 0 $_ = WriteProfileString('.\VarInfo.ini', 'Global', Trim($_Var), '1,' + CStr($_Ctr)) Else $_VF = 1 EndIf
Case $_VF = 1 And (InStr($_Vt, $_C) Or $_P = $_E) ; terminating char $_VF = 0 If $_P = $_E And Not InStr($_Vt, $_C) ; EOL - add last char if needed $_Var = $_Var + $_C EndIf $_ = WriteProfileString('.\VarInfo.ini', 'Global', Trim($_Var), '1,' + CStr($_Ctr)) If $_C = '[' ; handle arrays $_VF = -1 EndIf
Case $_VF = -1 ; skip chars betweet [ and ] If $_C = ']' $_VF = 0 EndIF
Case $_VF = 1 And Not InStr($_Vt, $_C) ; build var name $_Var = $_Var + $_C EndSelect Next EndIf
EndIf ; $_Line <> ''
Loop
;============================================================================================= ; PASS 2 ; Enumerate the INI file and check for vars that have been declared both as local and global ;=============================================================================================
$_SectList = ReadProfileString('.\VarInfo.ini', '', '') ; enum sections $_SectList = Split(Left($_SectList,len($_SectList)-1), Chr(10)) For Each $_Fn in $_SectList If $_Fn <> 'Global' ; ignore the global sect $_KeyList = ReadProfileString('.\VarInfo.ini', $_Fn, '') $_KeyList = Split(Left($_KeyList ,len($_KeyList)-1), Chr(10)) ; enum keys in section For Each $_Var in $_KeyList $_ = ReadProfileString('.\VarInfo.ini', 'Global', $_Var) ; is it also a Global? If $_ <> '' $_P = Split(ReadProfileString('.\VarInfo.ini', $_Fn, $_Var), ',')[1] $_ = Split(ReadProfileString('.\VarInfo.ini', 'Global', $_Var), ',')[1] $_ECt = $_ECt + 1 $_Warning[$_Ect] = $_Fn + ',' + $_P + ',' + $_Var + ',' + 2 + ',' + $_ + ',' + 'Global variable declared as Local.' EndIf Next EndIf Next
;============================================================================================= ; PASS 3 ; Scan the data and identify the variables that are used, verify that they are declared, and ; check for variable and macro usage inside of strings ;=============================================================================================
; reset the line counter & flags for the third pass $_Ctr = 0 $_Fn = 'Main' $_VF = 0 ; Variable Flag $_FF = 0 ; Function Flag
While $_Ctr <= UBound($_FData) $_Line = $_FData[$_Ctr] ; get working line $_Ctr = $_Ctr + 1 ; increment counter
If $_Line <> ''
$_SQF = 0 ; init per-line flags $_DQF = 0 $_PF = 0
; =============================================================================== ; filter out lines we don't want to process char by char
Select ; Check for function Start / End to get name to reference Case InStr($_Line, 'Function') = 1 $_Fn = Trim(Split(SubStr($_Line, 10), Chr(40))[0]) ; set active function name
Case InStr($_Line, 'EndFunction') = 1 $_Fn = 'Main' ; default active function to "main"
Case InStr($_Line, 'IsDeclared') ; do nothing - was processed earlier Case 1 ; valid line to parse
; =============================================================================== ; parse the line, char by char, looking for varnames, quotes, parens, and comments
$_VF = 0 ; clear the var active flag
$_E = Len($_Line) ; line end pointer For $_P = 1 to $_E ; char pointer $_C = SubStr($_Line, $_P, 1) ; get the active char
; =============================================================================== ; set the quote flags: 0=not active, 1=active alone, -1=active inside other quote If $_C = Chr(34) Or $_C = Chr(39) Select Case $_C = Chr(34) And $_DQF = 0 ; opening double quote $_DQF = IIf($_S
|
|
Top
|
|
|
|
#150240 - 2005-10-21 07:32 AM
Re: Script sanity checker
|
Kdyer
KiX Supporter
   
Registered: 2001-01-03
Posts: 6241
Loc: Tigard, OR
|
Hi Glenn!
Cool script! 
Ran it through our scripts and they look good.
Did run into a snag though with the following bit of code.. Code:
$x=WriteValue('HKLM\SYSTEM\CurrentControlSet\Services\PnSson\NetworkProvider', 'ProviderPath', 'C:\Program Files\Citrix\ICA Client\pnsson.dll','REG_SZ')
Quote:
Warning: Possible mismatched parenthesis. In function: CITRIXPASSTHROUGH Referenced on line: 301 PF=1 301: $x=WriteValue('HKLM\SYSTEM\CurrentControlSet\Services\PnSson\NetworkProvider', 'ProviderPath', Warning: Possible mismatched parenthesis. In function: CITRIXPASSTHROUGH Referenced on line: 302 PF=-1 302: 'C:\Program Files\Citrix\ICA Client\pnsson.dll','REG_SZ') 2 warnings generated, 1204 lines processed.
I don't see this as being a "showstopper" as I run the script with the lines broken to avoid long lines.
Thanks,
Kent
|
|
Top
|
|
|
|
#150241 - 2005-10-21 10:32 AM
Re: Script sanity checker
|
Richard H.
Administrator
   
Registered: 2000-01-24
Posts: 4946
Loc: Leatherhead, Surrey, UK
|
Quote:
Did run into a snag though with the following bit of code
To be fair, Glenn did say at the top of his post (my highlight):
Quote:
It also does some basic tests of matched quotes and parens (but not across multiple lines).
Unbalanced quotes and parenthese are a bugger to spot.
One way of avoiding them is to include pragmas which tell the checker to ignore errors. There is a powerful code checker for C called LINT, and even the best written code will generate a huge amount of warnings without the directives.
Another alternative is to look at the context and the final state. If you have an unbalanced opening parenthesis on a line the store the error. If you get an unbalanced closing parenthesis on a line then you can clear the error otherwise at the end of the file you repost the unbalanced parentheses.
Obviously an unbalanced closing parenthesis which is not in a string is always an error.
Unbalanced quotes are trickier. Two unbalanced quotes are really hard to deal with.
About the best guess you can make is to look at the text within the string, and if you spot strings which are only likely to be KiXtart code such as functions and variables then you can report the quote as unbalanced.
In the end though the most reliable solution is to report all the potential errors and allow the coder to determine whether they are real problems or not.
|
|
Top
|
|
|
|
#150242 - 2005-10-21 01:48 PM
Re: Script sanity checker
|
Glenn Barnas
KiX Supporter
   
Registered: 2003-01-28
Posts: 4402
Loc: New Jersey
|
Quote:
Unbalanced quotes are trickier.
You got that right! It took fair though, lots of testing, and three code revisions before I was satisfied with how they were handled, especially quote chars in strings.
Thanks for the feedback. I struggled with the unbalanced paren thing for quite a while.. and in the end, decided not to write a KiX Interpreter in Kix, which is about where I'd need to go to resolve them. The "free format" of KiX can be both a blessing and a curse.
One thought was to log every instance. If parens were closed within, say, 5 lines it would be OK, otherwise reported as a warning. Another was to look at the $_PF value at the end of the script, and only then report every mismatched Paren line. But - what if you missed a closing paren on line 17 and an opening paren on line 239? It would be balanced, but not correct.
So - at this point - I generate the warning and turn processing over to the carbon-based analog computer between your ears. They are warnings, not errors, just for this reason.
If someone has a good algorithm for this (or even ideas), go ahead and post it!
BTW - for those of you that use my KGen UDF resolver/linker, this code is now embedded in it. You can download it from my web site - Products / Admin Toolchest / Kix Development.
Glenn
_________________________
Actually I am a Rocket Scientist!
|
|
Top
|
|
|
|
#150243 - 2005-10-21 03:46 PM
Re: Script sanity checker
|
Richard H.
Administrator
   
Registered: 2000-01-24
Posts: 4946
Loc: Leatherhead, Surrey, UK
|
The rule to easily find unbalanced parentheses is actually very simple, it's just hard work to code 
A simple rule is that a set of parentheses can only contain nothing, an expression, or a list of comma seperated expressions. As soon as you get more than one expression which is not comma seperated you have an error. This is true whether the parentheses are on the same line or spread over multiple lines.
Code:
; Good 1: someFunction(a,b) ; Bad 1: someFunction(a b) ; Good 2: someFuntion( someOtherFunction( b ) )
; Bad 2: someFunction(a someOtherFunction b)
You know that Bad 2 is wrong, because it has the parameters "a" and "someOtherFunction" which are not comma seperated. It also has parameter "b" but you already know that you have a problem from the first two.
You could also report the closing parenthesis as bad, or you could ignore it in which case it would be picked up after the coder fixed the earlier error.
You will however need to determine what constitutes a parameter. For example this is perfectly acceptable code: Code:
a = foo ( bar ( ) )
So your parser will need to lose the redundant spaces which might otherwise cause you problems.
|
|
Top
|
|
|
|
#150244 - 2005-10-21 04:57 PM
Re: Script sanity checker
|
Kdyer
KiX Supporter
   
Registered: 2001-01-03
Posts: 6241
Loc: Tigard, OR
|
I know easier said than done..
Quote:
So your parser will need to lose the redundant spaces which might otherwise cause you problems.
This would be a good option for trim.
Kent
Edited by kdyer (2005-10-21 04:57 PM)
|
|
Top
|
|
|
|
#150249 - 2006-02-23 11:56 PM
Re: Script sanity checker
|
It_took_my_meds
Hey THIS is FUN
   
Registered: 2003-05-07
Posts: 273
Loc: Sydney, Australia
|
Thanks Glen, terrific work! 
However, at the risk of being accused of bad coding practices, what is the problem with declaring a global variable as a local variable? 
eg.
Code:
Global $i ;Global counter for something
Function SomeFunc($par1)
Dim $i ;internal to the function and doesn't change the global $i For $i = 0 to $par1 ;do something Next EndFunction
|
|
Top
|
|
|
|
#150250 - 2006-02-24 12:04 AM
Re: Script sanity checker
|
It_took_my_meds
Hey THIS is FUN
   
Registered: 2003-05-07
Posts: 273
Loc: Sydney, Australia
|
No biggie but it also mistakenly gives errors regarding no declaration of $. You simply cant declare $ globally when using option explicit as Ruud must do it internally. Try:
Code:
$=SetOption("Explicit","On") Global $
|
|
Top
|
|
|
|
#150253 - 2006-02-24 01:35 AM
Re: Script sanity checker
|
NTDOC
Administrator
   
Registered: 2000-07-28
Posts: 11631
Loc: CA
|
Quote:
I think it is best practice to use $
Well I suppose that's what's good about opinions, eveyone has one. I personally disagree with the idea of BEST PRACTICE
As for your example I do something like this
Break On Dim $SO,$Pause $SO=SetOption('Explicit','On') $SO=SetOption('NoVarsInStrings','On') $SO=SetOption('NoMacrosInStrings','On')
;run some code stuff
Get $Pause
To me that is VERY clear what it's being used for. Finding a bunch of $ all over the place in a large script and trying to fix or update the code is a nightmare.
|
|
Top
|
|
|
|
#150254 - 2006-02-24 01:59 AM
Re: Script sanity checker
|
It_took_my_meds
Hey THIS is FUN
   
Registered: 2003-05-07
Posts: 273
Loc: Sydney, Australia
|
Yes, everyone has their different styles and in the end the script runs the same so this discussion is rather moot. However, my point is that $SO is not being used for anything except as a bit bucket. Personally I can't see why you bother giving a variable a name if you are never going to reference it.
Edited by It_took_my_meds (2006-02-24 04:04 AM)
|
|
Top
|
|
|
|
Moderator: Arend_, Allen, Jochen, Radimus, Glenn Barnas, ShaneEP, Ruud van Velsen, Mart
|
1 registered
(mole)
and 759 anonymous users online.
|
|
|