Page 1 of 1

[WIP][App] LogSync - Load multiple log files and view side-by-side, synchronized by timestamps

Posted: 20 Jan 2017, 18:16
by evilC
My company writes some software that involves a server and a client, and part of my job is to debug issues in communication between them.
Hence lots of looking at logs, and trying to piece together the communication between the two, and CMTrace just wasn't doing it for me.
I had a google around, but turned up nothing, so I decided to see if I could hack something together.

Here is an initial proof-of-concept. There's a bunch of borrowed code - AutXYWH and some listview scrolling synch code I found and adapted, but as a start, it seems quite promising.

You will need two log files, the ones I used varied slightly in format:

Code: Select all

20 Jan 2017 12:50:34.702 [17308] INFO  - Some Client v3.0.0.12
12:50:14.510 Some Server: v3.0.0.24 (Jan 19 2017)
2017-01-20 12:54:32,438 INFO  Some other text
For now, it ignores the date part, so in the above example, the first example would just register as 12:50:34.702
Basically, as long as your log has HH:MM:SS.mil type timestamps, the logic should work, although the regex may need some tweaking

Image

Code: Select all

;OutputDebug DBGVIEWCLEAR
#SingleInstance force
Version := "1.0.1"
Gui, +Resize

; AHK_L + AutoXYWH based Autosize GUI
Gui, Add, Button, xm w300 gPick1 vPick1, Select...
Gui, Add, Button, x+10 w300 gPick2 vPick2, Select...
Gui, Add, Button, xm w610 gGo vGo, Sync!
Gui, Add, Text, xm w300 hwndhTitle1 vTitle1 Center, % A_Space
Gui, Add, Text, x+10 yp w300 hwndhTitle2 vTitle2 Center, % A_Space
Gui, Add, ListView, AltSubmit xm w300 h200 hwndhLV1 vLV1, Time|Text
Gui, Add, ListView, AltSubmit x+10 yp w300 h200 hwndhLV2 vLV2, Time|Text

; AHK_H based AutoSize GUI
;~ Gui, Add, Button, xm w300 aw1/2 gPick1, Select...
;~ Gui, Add, Button, x+10 w300 aw1/2 ax1/2 gPick2, Select...
;~ Gui, Add, Button, xm w610 aw gGo, Sync!
;~ Gui, Add, Text, xm w300 hwndhTitle1 aw1/2 Center, % A_Space
;~ Gui, Add, Text, x+10 yp w300 hwndhTitle2 aw1/2 ax1/2 axr Center, % A_Space
;~ Gui, Add, ListView, AltSubmit xm w300 h200 hwndhLV1 aw1/2 ah, Time|Text
;~ Gui, Add, ListView, AltSubmit x+10 yp w300 h200 hwndhLV2 aw1/2 ah ax1/2 axr, Time|Text


fn := Func("ListScrolled").Bind(1)
GuiControl, +g, % hLV1, % fn
fn := Func("ListScrolled").Bind(2)
GuiControl, +g, % hLV2, % fn

ListViews := [hLV1, hLV2]

Titles := [hTitle1, hTitle2]
RawLines := [[],[]]

Gui, Show, Hide	; Autosize set initial (smallest) size
Gui, Show, x0 y0 w1024 h768, % "Log Sync v"	Version ; Show and size up to a reasonable size

return

GuiClose:
	ExitApp

; #IfWinActive ahk_class AutoHotkeyGUI 
; Any Gui-specific hotkeys would go here 
; #IfWinActive

Pick1:
Pick2:
	index := SubStr(A_ThisLabel, 5, 1)
	FileSelectFile, file
	if ErrorLevel
		return
	Pick(index, file)
	return

Pick(index, file){
	global RawLines, ListViews, Titles
	GuiControl, , % Titles[index], % file
	
	RawLines[index] := ReadLog(file)
	Gui, ListView, % ListViews[1]
	LV_Delete()
	Gui, ListView, % ListViews[2]
	LV_Delete()
}

Go:
	SyncedLines := SyncLogs(RawLines)
	PopulateListViews(ListViews, SyncedLines)
	Gui, ListView, % ListViews[1]
	LV_ModifyCol()
	Gui, ListView, % ListViews[2]
	LV_ModifyCol()
	return

PopulateListViews(ListViews, SyncedLines){
	coords := GetProgressCoords(300, 100)
	Progress, % "b w300 h100 x" coords.x " y" coords.y, , Populating ListViews
	prog := 0
	for lv_index, hwnd in ListViews {
		Gui, ListView, % hwnd
		max := SyncedLines[lv_index].length()
		tick := 50 / max
		Loop % max {
			line := SyncedLines[lv_index, A_Index]
			LV_Add("", line.rawtime, line.text)
			prog += tick
			Progress, % round (prog)
		}
	}
	Progress, Off
}

; Logs are expected to be in one of two formats:
; 20 Jan 2017 12:50:34.702 [17308] INFO  - Some Client v3.0.0.12
; 12:50:14.510 Some Server: v3.0.0.24 (Jan 19 2017)
; For now, the Date part is thrown away...
; The regex should handle there being no milliseconds, but both logs should lack milliseconds in this instance.
ReadLog(filename){
		FileRead, log_text, % filename
		log_lines := []
		Loop, Parse, % log_text, `n, `r
		{
			RegExMatch(A_LoopField, "^([\w\s-]+)?(\d{2}):(\d{2}):(\d{2}).?(\d{3})?(.*)", matches)
			log_time := Trim(matches2 matches3 matches4 matches5)
			log_line := Trim(matches6)
			if (!log_time || !log_line)
				continue
			log_lines.push({time: log_time, text: log_line, rawtime: matches2 ":" matches3 ":" matches4 ":" matches5})
		}
		return log_lines
}

SyncLogs(RawLines){
	coords := GetProgressCoords(300, 100)
	Progress, % "b w300 h100 x" coords.x " y" coords.y, , Syncing Logs
	Lengths := [RawLines[1].length(), RawLines[2].length()]
	highest := Lengths[1]
	if (Lengths[2] > highest)
		highest := Lengths[2]
	tick := 100 / highest
	prog := 0
	CurrentLine := [1,1]
	SyncedLines := [[],[]]
	Loop {
		l1 := CurrentLine[1]
		l2 := CurrentLine[2]
		c1 := RawLines[1, l1]
		c2 := RawLines[2, l2]
		t1 := RawLines[1, l1].time
		t2 := RawLines[2, l2].time
		if (((t1) && t1 < t2) || t2 == ""){
			; Log 1 line earlier than Log 2 line
			SyncedLines[1].push(RawLines[1, l1])
			SyncedLines[2].push({time: 0, text: ""})
			CurrentLine[1]++
		} else if (((t2) && t2 < t1) || t1 == ""){
			; Log 2 line earlier than Log 1 line
			SyncedLines[1].push({time: 0, text: ""})
			SyncedLines[2].push(RawLines[2, l2])
			CurrentLine[2]++
		} else if (t1 == t2) {
			; Log 1 & Log 2 line at same time
			SyncedLines[1].push(RawLines[1, l1])
			SyncedLines[2].push(RawLines[2, l2])
			CurrentLine[1]++
			CurrentLine[2]++
		} else {
			;~ debug := 1
			msgbox ERROR
		}
		prog += tick
		Progress, % round(prog)
		Progress, , % "1: Current=" CurrentLine[1] ", max=" Lengths[1] "`n2: Current=" CurrentLine[2] ", max=" Lengths[2]
	} until (CurrentLine[1] >= Lengths[1] && CurrentLine[2] >= Lengths[2])
	Progress, Off
	return SyncedLines
}

GetProgressCoords(pw, ph){
	Gui, +HwndhMain
	WinGetPos, x, y, w, h, % "ahk_id " hMain
	px := (x + (w / 2)) - (pw / 2)
	py := y + (h / 2) - (ph / 2)
	return {x: px, y: py}
}
; -------------- End of SyncLogs-specific code ---------------------------

; code from https://autohotkey.com/board/topic/38906-scroll-3-listviews-simultaneaous/
; Handles ListView scroll position synchronization
ListScrolled(this_lv){
	global ListViews
	;OutputDebug % "AHK| " A_TickCount " - lv: " this_lv ", event: " A_GuiEvent ", erorlevel: " ErrorLevel 
	other_lv := 3 - this_lv
	;~ if down||up	   	;i.e the scrollevent is triggered by the mousewheel or the up/down buttons
		;~ return		;therefore: do not use the lines below to update the other listviexs
	;critical 
	
	if (A_GuiEvent == "S") 
	{ 
		startlogged = 1
		;start scroll 
		
		ScrollCount++ 
	}
	
	if (A_GuiEvent == "I" && ErrorLevel == "SF"){
		; Keyboard scroll
		sa := LVC_getTopIndex(ListViews[this_lv]) - LVC_getTopIndex(ListViews[other_lv])
		lh := 20	; ToDo - why does LVC_getItemHeight not seem to work?
		LVC_Scroll(ListViews[other_lv], sa * lh)
		return
	}
	
	else if (A_GuiEvent == "s") 
	{ 
	
		if !startlogged
			ScrollCount++

		if !ScrollCount 
			return 
			
		ScrollCount-- 

		if (!ScrollCount) 
		{ 
				loop,2
					{
					;if ("ListView" A_Index = A_GuiControl) 
					if (A_Index == this_lv){
						continue
					}
					sa := LVC_getTopIndex(ListViews[this_lv]) - LVC_getTopIndex(ListViews[other_lv])
					lh := 20	; ToDo - why does LVC_getItemHeight not seem to work?
					LVC_Scroll(ListViews[other_lv], sa * lh)
					}
			} 
	} 
}

/* 
Retrieves the index of the topmost visible item when in list or report view. 

Returns the index of the item if successful, 
    or zero if the list-view control is in icon or small icon view. 
*/ 
LVC_getTopIndex(ThisControl) 
{ 
    ;LVM_GETTOPINDEX = LVM_FIRST + 39 = 0x1027 
    SendMessage, 0x1027, 0, 0, , ahk_id %ThisControl% 

    return ErrorLevel + 1 
}

LVC_Scroll(ThisControl, pxls) 
{ 
    ;if pxls > 0, scrolls down 
    ;if pxls < 0, scrolls up 
    
    ;LVM_SCROLL = LVM_FIRST + 20 = 0x1014 
    SendMessage, 0x1014, 0, pxls, , ahk_id %ThisControl% 
} 

/* 
Returns the height for an item in the list-view control. 
    (all items, in the same control, have the same height) 

If the list-view has no items, this function returns 0 
*/ 
LVC_getItemHeight(ThisControl) 
{ 
    LVC_getItemRect(ThisControl, 1, ItemBounds) 

    Top := NumGet(ItemBounds, 4, "int") 
    Bottom := NumGet(ItemBounds, 12, "int") 
    
    return Bottom - Top 
} 

;Retrieves the bounding rectangle for all or part of an item in the current view. 
LVC_getItemRect(ThisControl, iItem, ByRef ItemBounds, boundType = 0) 
{ 
    /* 
    Valid values for boundType: 

    LVIR_BOUNDS = 0 (default) 
        Returns the bounding rectangle of the entire item, 
        including the icon and label. 
    LVIR_ICON = 1 
        Returns the bounding rectangle of the icon or small icon. 
    LVIR_LABEL = 2 
        Returns the bounding rectangle of the item text. 
    LVIR_SELECTBOUNDS = 3 
        Returns the union of the LVIR_ICON and LVIR_LABEL rectangles, 
        but excludes columns in report view. 
    */ 

    ;creates a new rect structure, 
    ;and sets boundType as the LEFT member (as required for message) 
    VarSetCapacity(ItemBounds, 16), NumPut(boundType, ItemBounds) 

    ;LVM_GETITEMRECT = LVM_FIRST + 14 = 0x100E 
    SendMessage, 0x100E, iItem - 1, &ItemBounds, , ahk_id %ThisControl% 

    return ErrorLevel 
} 

; =================================================================================
; Function: AutoXYWH
;   Move and resize control automatically when GUI resizes.
; Parameters:
;   DimSize - Can be one or more of x/y/w/h  optional followed by a fraction
;             add a '*' to DimSize to 'MoveDraw' the controls rather then just 'Move', this is recommended for Groupboxes
;   cList   - variadic list of ControlIDs
;             ControlID can be a control HWND, associated variable name, ClassNN or displayed text.
;             The later (displayed text) is possible but not recommend since not very reliable 
; Examples:
;   AutoXYWH("xy", "Btn1", "Btn2")
;   AutoXYWH("w0.5 h 0.75", hEdit, "displayed text", "vLabel", "Button1")
;   AutoXYWH("*w0.5 h 0.75", hGroupbox1, "GrbChoices")
; ---------------------------------------------------------------------------------
; Version: 2015-5-29 / Added 'reset' option (by tmplinshi)
;          2014-7-03 / toralf
;          2014-1-2  / tmplinshi
; requires AHK version : 1.1.13.01+
; =================================================================================
AutoXYWH(DimSize, cList*){       ; http://ahkscript.org/boards/viewtopic.php?t=1079
  static cInfo := {}
 
  If (DimSize = "reset")
    Return cInfo := {}
 
  For i, ctrl in cList {
    ctrlID := A_Gui ":" ctrl
    If ( cInfo[ctrlID].x = "" ){
        GuiControlGet, i, %A_Gui%:Pos, %ctrl%
        MMD := InStr(DimSize, "*") ? "MoveDraw" : "Move"
        fx := fy := fw := fh := 0
        For i, dim in (a := StrSplit(RegExReplace(DimSize, "i)[^xywh]")))
            If !RegExMatch(DimSize, "i)" dim "\s*\K[\d.-]+", f%dim%)
              f%dim% := 1
        cInfo[ctrlID] := { x:ix, fx:fx, y:iy, fy:fy, w:iw, fw:fw, h:ih, fh:fh, gw:A_GuiWidth, gh:A_GuiHeight, a:a , m:MMD}
    }Else If ( cInfo[ctrlID].a.1) {
        dgx := dgw := A_GuiWidth  - cInfo[ctrlID].gw  , dgy := dgh := A_GuiHeight - cInfo[ctrlID].gh
        For i, dim in cInfo[ctrlID]["a"]
            Options .= dim (dg%dim% * cInfo[ctrlID]["f" dim] + cInfo[ctrlID][dim]) A_Space
        GuiControl, % A_Gui ":" cInfo[ctrlID].m , % ctrl, % Options
} } }

; AutoXYWH Gui Sizing for AHK_L
GuiSize:
	If (A_EventInfo = 1) ; The window has been minimized.
		Return
	AutoXYWH("w.5", "Pick1")
	AutoXYWH("w.5 x.5", "Pick2")
	AutoXYWH("w", "Go")
	AutoXYWH("w.5", "Title1")
	AutoXYWH("w.5 x.5", "Title2")
	AutoXYWH("w.5 h", "LV1")
	AutoXYWH("w.5 x.5 h", "LV2")
	return

Re: [WIP][App] LogSync - Load two log files and synchronize the view

Posted: 20 Jan 2017, 21:10
by guest3456
so the sync is based on the time of the log entry?

Re: [WIP][App] LogSync - Load two log files and synchronize the view

Posted: 21 Jan 2017, 08:01
by evilC
yup
It Looks at the first line of each log, if one is lower it adds it to the listview, and adds a blank line to the other listview.
Only if the timestamps are identical will you get something on the same row in both listviews.

Re: [WIP][App] LogSync - Load two log files and synchronize the view

Posted: 21 Jan 2017, 09:29
by evilC
Updated code.

1.0.1
Improved log sync logic, it now successfully syncs more logs.
Added Progress window when syncing
Added support for keyboard scrolling - arrow keys, pgup/dn, home etc.

I don't really understand what most of the code in ListScrolled() is doing, eg I am not really sure what startlogged is for, any ideas?

Re: [WIP][App] LogSync - Load two log files and synchronize the view

Posted: 21 Jan 2017, 10:59
by SnowFlake
pretty cool :D

Re: [WIP][App] LogSync - Load two log files and synchronize the view

Posted: 21 Jan 2017, 12:12
by evilC
Would love to expand this to be able to support more than two logs, the app I am working on has 7 really important ones, and to be able to view the communication between all the services would be awesome.

The hurdles to doing this that I see are:
Implementing a dynamic gui (Allow adding of new ListViews). I have done it in AHK_H, but am not sure if AutoXYWH will cope with guis in guis etc.
Synchronizing that many listviews (I think the code I have will handle it, the code it derived from used 3 listviews).

If possible, I would also like to implement tailing of live logs.

Searching will also need to be implemented, and I would also like to implement line highlighting based upon regexes. (Highlight line this color if regex matches).

If anyone has any suggestions, I would love to hear them.

Re: [WIP][App] LogSync - Load two log files and synchronize the view

Posted: 21 Jan 2017, 13:28
by Helgef
Looks good.
evilC wrote:If anyone has any suggestions, I would love to hear them.
Maybe you could use only one listview? Add a column for each log. It will save space when syncing many logs. You'll need only one Column for timestamps, and only one scroll bar.
log.png
log.png (50.7 KiB) Viewed 4574 times

Re: [WIP][App] LogSync - Load two log files and synchronize the view

Posted: 21 Jan 2017, 14:03
by guest3456
evilC wrote: Implementing a dynamic gui (Allow adding of new ListViews). I have done it in AHK_H, but am not sure if AutoXYWH will cope with guis in guis etc.
why are you adamanat about maintaining AHK_L compatibility? why not just make the script for AHK_H only? plenty of scripts were AHK_L only, back in the day

Re: [WIP][App] LogSync - Load two log files and synchronize the view

Posted: 21 Jan 2017, 19:01
by evilC
Helgef wrote: Maybe you could use only one listview? Add a column for each log. It will save space when syncing many logs. You'll need only one Column for timestamps, and only one scroll bar.
log.png
Hmm, interesting idea. takes all the complexity of that part out.
I am not 100% sure it would work though, as AFAIK in a ListView, all lines must be the same height, so you cannot have wrapping.
This may present an issue without a horizontal scrollbar for each log.
guest3456 wrote:
evilC wrote: Implementing a dynamic gui (Allow adding of new ListViews). I have done it in AHK_H, but am not sure if AutoXYWH will cope with guis in guis etc.
why are you adamanat about maintaining AHK_L compatibility? why not just make the script for AHK_H only? plenty of scripts were AHK_L only, back in the day
Not adamant, but I am aware that most people don't use it, and if there are other alternatives that do not add too much complexity, I would like to take them.

Re: [WIP][App] LogSync - Load two log files and synchronize the view

Posted: 22 Jan 2017, 08:03
by Helgef
evilC wrote: This may present an issue without a horizontal scrollbar for each log.
You can drag the column width, and double click the edge to expand to fit content.
Just some other random ideas, another way to orginise the view, add a listview below to display the (in this case double-clicked) selected row.
2017-01-22_13-54-40.gif
2017-01-22_13-54-40.gif (696.93 KiB) Viewed 4510 times

Re: [WIP][App] LogSync - Load two log files and view side-by-side, synchronized by timestamps

Posted: 22 Jan 2017, 10:12
by evilC
I couldn't be bothered to debug what ListScrolled was doing, so I sat down and coded my own version.

Seems like a much simpler way of doing things, although it tries to scroll with every interaction. I just need to have it cache the positions and only scroll if it needs to.
Edit: It now only issues scrolls when it needs to.

Code: Select all

ListScrolled(this_lv){
	global ListViews
	static current_lv := 0, lv_positions := [1,1]
	; If an LV gets focus, or is scrolled when no LV has focus, consider it the "current" LV
	if (A_GuiEvent == "F" || (current_lv == 0 && A_GuiEvent == "S"))
		current_lv := this_lv
	if (current_lv != this_lv)
		return	; This event is the result of a programatically issued scroll - ignore
	other_lv := 3-this_lv, tp := LVC_getTopIndex(ListViews[this_lv]), op := LVC_getTopIndex(ListViews[other_lv])
	if (tp != lv_positions[this_lv] || op != lv_positions[other_lv]){
		lh := 20	; ToDo - why does LVC_getItemHeight not seem to work?
		sa := (tp - op)
		LVC_Scroll(ListViews[other_lv], sa * lh)
		
		lv_positions[this_lv] := tp
		lv_positions[other_lv] := op
	}
}

Re: [WIP][App] LogSync - Load two log files and view side-by-side, synchronized by timestamps

Posted: 22 Jan 2017, 12:02
by evilC
So I got to thinking about the fundamental way the list synchronization was handled - basically flags would be set to indicate the "Current LV", and any scroll messages coming in from other LVs get ignored.
The problem arises when the GUI and lv 1 already has focus, and you drag the scrollbar of lv2.
To the existing code, this was indistinguishable from it seeing lv2 scrolled programatically as a result of the user moving lv1.

So I tried a more elegant approach - when you programatically scroll a lv, turn off it's g-label, do the scroll, then turn the g-label back on.

Also, I wrapped the whole thing up into a class, and now you can have any number of listviews.
Plus, when you select a line in one LV, it becomes selected in all LVs

Image

Code: Select all

#SingleInstance force
;OutputDebug DBGVIEWCLEAR
num_lvs := 10
sv := new SyncedView()
lvs := []
Loop % num_lvs {
	if (A_Index == 1 || A_Index == 6)
		xyopt := "xm"
	else
		xyopt := "x+10 yp"
	lvs.push(sv.Add(xyopt " w100 h200", "Time|Text"))
}

Loop 200 {
	t := A_Index
	Loop % num_lvs {
		lvs[A_Index].Add("", t,  "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
	}
}

Loop % num_lvs {
	lvs[A_Index].AutoAdjust()
}

Gui, Show, x0 y0
sv.SetSyncState(1)
return


GuiClose:
	ExitApp
	return



class SyncedView {
	ListViews := []
	LVTopIndexes := []
	CurrentLV := 0
	CurrentLine := 0
	
	Add(options, fields){
		lv_count := this.ListViews.length()
		new_lv := new this.ListView(this, this.ListEvent.Bind(this, ++lv_count), options, fields)
		if (lv_count > 1){
			for i, lv in this.ListViews {
				if (new_lv.Height != lv.Height){
					msgbox % "ListView " lv_count " is not the same height as listivew " i
					ExitApp
				}
				if (new_lv.LineHeight != lv.LineHeight){
					msgbox % "LineHeights do not match"
					ExitApp
				}
			}
		}
		this.LineHeight := this.ListViews[1].LineHeight
		this.ListViews.push(new_lv)
		this.LVTopIndexes.push(1)
		return new_lv
	}
	
	SetSyncState(state){
		if (state){
			for i, lv in this.ListViews {
				if (i == 1){
					num_rows := lv.Rows
				} else {
					if (lv.Rows != num_rows){
						msgbox % "Row count for listview " i "does not match"
						return 0
					}
				}
			}
		}
		this._SetCallbackState(state)
		return 1
	}
	
	_SetCallbackState(state, ignore := 0){
		for i, lv in this.ListViews {
			if (ignore == i)
				continue
			lv.SetCallbackState(state)
		}
	}
	
	GetTopRows(){
		out := []
		for i, lv in this.ListViews {
			out.push(lv.GetTopRow())
		}
		return out
	}
	
	ListEvent(this_lv){
		Critical
		GuiEvent := A_GuiEvent, EL := ErrorLevel, EventInfo := A_EventInfo
		; Filter out any events we are not interested in.
		if (!(GuiEvent == "Normal" || GuiEvent == "s" || GuiEvent == "F" || GuiEvent == "K" || GuiEvent == "I"))
			return
		this.CurrentLV := this_lv
		;OutputDebug % "AHK| this_lv: " this_lv ", current: " this.CurrentLV ", A_GuiEvent: " GuiEvent ", A_EventInfo: " EventInfo 
		
		; Disable events for all other listviews
		this._SetCallbackState(0, this_lv)
		
		; Select new rows
		if (GuiEvent == "I" && EL == "SF"){
			new_selected_row := EventInfo 
			;OutputDebug % "AHK| new_selected_row: " new_selected_row
			for i, lv in this.ListViews {
				if (i == this_lv)
					continue
				lv.SelectRow(new_selected_row)
			}
		}

		top_rows := this.GetTopRows()
		this_lv_top_row := top_rows[this_lv]

		; Scroll ListViews
		if (this_lv_top_row != this.LVTopIndexes[this_lv]){
			for i, row in top_rows {
				if (i == this_lv)
					continue
				
				if (row != this_lv_top_row){
					this.ListViews[i].ScrollRows(this_lv_top_row - row)
					this.LVTopIndexes[i] := this_lv_top_row
				}
			}
		}
		; Re-Enable events for all other listviews
		this._SetCallbackState(1, this_lv)
	}
	
	class ListView {
		State := 0
		LineHeight := 0
		Rows := 0
		SelectedRow := 0
		
		__New(parent, callback, options, fields){
			this.callback := callback
			this.parent := parent
			Gui, Add, ListView, % "-Multi AltSubmit hwndhwnd " options, % fields
			this.hwnd := hwnd
			this._SetLineHeight()
			this.Height := this.GetSize().h
		}
		
		Add(options, aParams*){
			this.SetCurrentLV()
			LV_Add(options, aParams*)
			this.Rows++
		}
		
		_SetLineHeight(){
			if (h := this._GetRowHeight()){
				this.LineHeight := h
			} else {
				this.SetCurrentLV()
				LV_Add("","test")
				this.LineHeight := this._GetRowHeight()
				LV_Delete()
			}
		}

		/* 
		Returns the height for an item in the list-view control. 
			(all items, in the same control, have the same height) 

		If the list-view has no items, this function returns 0 
		*/ 
		_GetRowHeight(){ 
			VarSetCapacity(ItemBounds, 16), NumPut(boundType, ItemBounds) 
			;LVM_GETITEMRECT = LVM_FIRST + 14 = 0x100E 
			SendMessage, 0x100E, 0, &ItemBounds, , % "ahk_id " this.hwnd
			Top := NumGet(ItemBounds, 4, "int") 
			Bottom := NumGet(ItemBounds, 12, "int") 
			return Bottom - Top 
		} 

		SetCallbackState(state){
			if (state == this.State)
				return
			this.State := state
			fn := this.callback
			opt := (state ? "+g" : "-g")
			GuiControl, % opt, % this.hwnd, % fn
		}
		
		GetTopRow(){
			SendMessage, 0x1027, 0, 0, , % "ahk_id " this.hwnd
			return ErrorLevel + 1
		}
		
		ScrollRows(rows){
			SendMessage, 0x1014, 0, % this.LineHeight * rows, , % "ahk_id " this.hwnd
		}
		
		GetSize(){
			GuiControlGet, out, Pos, % this.hwnd
			return {x: outx, y: outy, w: outw, h: outh}
		}
		
		AutoAdjust(){
			this.SetCurrentLV()
			LV_ModifyCol()
		}
		
		SetCurrentLV(){
			Gui, ListView, % this.hwnd
		}
		
		SelectRow(row){
			this.SetCurrentLV()
			if (this.SelectedRow){
				this.SelectedRow := 0
				LV_Modify(this.SelectedRow, "-Select")
			}
			this.SelectedRow := row
			LV_Modify(row, "Select")
		}
			
	}
}

Re: [WIP][App] LogSync - Load two log files and view side-by-side, synchronized by timestamps

Posted: 23 Jan 2017, 10:45
by evilC
Aww yeah, I just figured out how to deal with logs missing dates (ie having only HH:MM:SS timestamps).
Scan the first line of each log, if it is missing a date, get the file creation time. If the time portion matches the timestamp of the first line of the log (it usually does), then you can be sure that the date portion of the file creation time is correct.
Then, iterate thru each line of the log, adding in the date. If the time of the current line is lower than the time of the previous line, then the day has ticked over, so add 1 day to the date we got from the creation time.

So, given a log file, we can generally work out the date portion for every line of the log, without any other files.
The above system would only fail for a log covering 3 or more days where you had nothing logged for a whole day.
This is not gonna happen in my use-case, so screw everyone else :P

Re: [WIP][App] LogSync - Load two log files and view side-by-side, synchronized by timestamps

Posted: 26 Jan 2017, 17:56
by evilC
So I decided to set the bar higher.
Why only 2 logs, when you can have as many as you want?

Here is a POC for the new version. It is AHK_H ONLY. It may well stay that way also.
Be aware that for the moment, you must call LogSync.AddParser once for each of the filenames you might load, and tell it which parser to use.
This will be guified at some point.
Parsers are classes that read the log, chunk it into lines, extract the date and ensure that it is in the correct format.
Three parsers are provided. Samples of the kind of log they can process are commented next to them.

For now, the time column is left in the raw format used for processing. I am thinking of adding options to use the original string, or convert it to some human readable - Useful eg if you want to convert all the timestamps to a localized, consistent format.
I am thinking of maybe event adding time parser classes or something so you can customize that part easily too.

At some point, parsers will probably be dynamically included from a plugins folder, like I do in UCR. This will probably mean that this version will remain AHK_H only.
However, in this case I can just package it as an EXE, so non AHK_H users can still use it. Everything will remain uncompiled, I will just ship a renamed AHK_H Autohotkey.exe with it.

Image

Code: Select all

#SingleInstance, force	; Enable for debugging
;#SingleInstance, Off	; Allow running of multiple copies

LogSync := new CLogSync()

; For now, manual mappings must be made to tell LogSync which parser to use for which filename
LogSync.AddParser("Tachyon.ConsumerAPI.log", "DefaultLogParser")
LogSync.AddParser("Tachyon.Workflow.log", "DefaultLogParser")
LogSync.AddParser("Tachyon.Agent.log", "DDMmmYYYYParser")
LogSync.AddParser("Tachyon.Switch.log", "DateFromFileCreationParser")

; Files can be added via the GUI, but when developing, it's easier to add them programatically
;LogSync.AddLog("D:\Data\code\AHK\LogSync\E2E Fail\Tachyon.ConsumerAPI.log")
;~ LogSync.AddLog("D:\Data\code\AHK\LogSync\E2E Fail\Tachyon.Workflow.log")
;~ LogSync.AddLog("D:\Data\code\AHK\LogSync\E2E Fail\Tachyon.Agent.log")
;~ LogSync.AddLog("D:\Data\code\AHK\LogSync\E2E Fail\Tachyon.Switch.log")
return

GuiClose(hwnd) {
	global LogSync
	LogSync.GuiClosed(hwnd)
}

Class CLogSync {
	Hwnds := {}
	Logs := []
	NumLogs := 0
	
	__New(){
		this.BuildGui()
		this.GuiShowHide(1)
	}
	
	AddParser(name, value){
		this.Parsers[name] := value
	}
	
	AddLog(fullpath){
		if (!FileExist(fullpath))
			return false
		SplitPath, fullpath , filename, filepath
		fileinfo := {fullpath: fullpath, filename: filename}
		class_name := this.GetLogParser(fileinfo)
		
		if (class_name != 0){
			pl := this.ParseLog(fileinfo, class_name)
			parsed_log := new this.ParsedLog(pl)
		} else {
			parsed_log := 0
		}
		
		l := fileinfo.clone()
		l.Parsed := {NumRows: parsed_log.Length(), Rows: parsed_log}
		this.Logs.push(l)
		this.NumLogs := this.Logs.Length()
		
		this.AddLineToListView(class_name, filename, filepath)
		
		return true
	}
	
	GetLogParser(fileinfo){
		if (this.Parsers.HasKey(fileinfo.filename)){
			return this.Parsers[fileinfo.filename]
		} else {
			return 0
		}
	}
	
	ParseLog(fileinfo, class_name){
		log_parser := new %class_name%()
		return log_parser.ParseLog(fileinfo.fullpath)
	}
	
	SyncLogs(){
		synced_logs := []
		parsed_logs := []
		top_lines := []
		log_count := this.Logs.Length()
		max_lines := 0
		added_lines := 0

		; Initialize arrays etc
		for li, log_obj in this.Logs {
			synced_logs.push([])
			completed_logs.push(0)
			max_lines += log_obj.Parsed.NumRows
			p := log_obj.Parsed.Rows
			p.Reset()
			top_lines.push(p.GetNextLine())
			parsed_logs.push(p)
		}
		
		tick := 100 / max_lines
		
		coords := this.GetDialogCoords(300, 100)
		Progress, % "b w300 h50 x" coords.x " y" coords.y, , Syncing Logs
		
		Loop {
			; Find which log has the earliest time
			lowest_time := 0
			lowest_index := 0
			completed_logs := []
			for li, top_line in top_lines {
				if (!lowest_time || (top_line.time && (top_line.time < lowest_time))){
					lowest_time := top_line.time
					lowest_index := li
				}
			}
			
			; Iterate through all top_lines, and if it's line matches that time, include it...
			; ... and advance the next top_line for that log.
			; Else add a blank line
			for li, top_line in top_lines {
				if (top_line.time == lowest_time){
					synced_logs[li].push(top_line)
					top_lines[li] := parsed_logs[li].GetNextLine()
					added_lines++
					Progress, % round(tick * added_lines)
				} else {
					synced_logs[li].push({time: "", text: ""})
				}
			}
			
			; Detect End condition
			incomplete_count := log_count
			Loop % log_count {
				if (completed_logs[A_Index]){
					incomplete_count--
				} else {
					c := parsed_logs[A_Index].IsAtEnd()
					if (c){
						incomplete_count--
						completed_logs[A_Index] := 1
					}
				}
			}
			
			if (incomplete_count == 0){
				break
			}
			
		}
		
		Progress, Off
		this.CreateOutputWindow(log_count)
		
		this.PopulateListViews(synced_logs)
		
		this.sv.SetSyncState(1)
	}
	
	; A log which has had datetimes extracted, blank lines removed etc.
	; Provides helper functions to assist in syncing with other parsed logs.
	Class ParsedLog {
		__New(log_lines){
			this.LogLines := log_lines
			this.Reset()
			this.LineCount := this.LogLines.Length()
		}
		
		Reset(){
			this.CurrentLine := 1
		}
		
		GetNextLine(){
			if (this.LineCount >= this.CurrentLine)
				return this.LogLines[this.CurrentLine++]
			else
				return
		}
		
		IsAtEnd(){
			return this.CurrentLine == this.LineCount + 1
		}
	}
	
	GuiClosed(hwnd){
		if (hwnd == this.hwnds.GuiMain){
			ExitApp
		}
	}
	
	; Gui Stuff ==========================================================
	BuildGui(){
		Gui +HwndhMain
		this.hwnds.GuiMain := hMain
		this.hwnds.LVLogList := this.AddControl("ListView", "w500 h100", "Timestap Parser|Filename|Path")
		this.hwnds.BtnAddLog := this.AddControl("Button", "xm w500", "Add Log...")
		fn := this.AddLogClicked.Bind(this)
		GuiControl, +g, % this.hwnds.BtnAddLog, % fn
		
		;~ this.hwnds.BtnCheck := this.AddControl("Button", "xm w300", "Check ")
		;~ fn := this.SyncClicked.Bind(this)
		;~ GuiControl, +g, % this.hwnds.BtnSync, % fn
		
		this.hwnds.BtnSync := this.AddControl("Button", "xm w500", "Sync")
		fn := this.SyncClicked.Bind(this)
		GuiControl, +g, % this.hwnds.BtnSync, % fn
	}
	
	PopulateListViews(synced_logs){
		coords := this.GetDialogCoords(300, 100)
		line_count := 0
		for i, log_arr in synced_logs {
			line_count += log_arr.Length()
		}
		tick := 100 / line_count
		prog := 0
		Progress, % "b w300 h50 x" coords.x " y" coords.y, , Populating ListViews
		
		for i, log_arr in synced_logs {
			Gui, ListView, % this.hwnds.OutputWindow.ListViews[i]
			for li, log_obj in log_arr {
				;LV_Add("", log_obj.time, log_obj.text)
				this.sv.AddLine(i, "", log_obj.time, log_obj.text)
				prog += tick
				Progress, % prog
			}
			LV_ModifyCol()
		}
		Progress, Off
		coords := this.GetDialogCoords(850, 400)
		Gui, % this.hwnds.OutputWindow.MainGui ":Show", Hide	; Set initial size
		Gui, % this.hwnds.OutputWindow.MainGui ":Show", % "w850 h400 x" coords.x " y" coords.y
		;~ coords := this.GetDialogCoords(1000, 800)
		;~ Gui, % this.hwnds.OutputWindow.MainGui ":Show", % "w1000 h800 x" coords.x " y" coords.y
	}
	
	CreateOutputWindow(numlogs){
		if (IsObject(this.hwnds.OutputWindow)){
			Gui, % this.hwnds.OutputWindow.MainGui ":Destroy"
		}
		Gui, New, hwndhwnd
		Gui +Resize
		hwndobj := {MainGui: hwnd, ListViews: []}
		
		sv := new SyncedView()
		this.sv := sv

		aw := "1/" numlogs
		Loop % numlogs {
			if (A_Index == 1){
				pos := "xm"
				ax := " "
			} else {
				pos := "x+10 yp"
				ax := " ax" aw
			}
			sv.Add(pos " w200 h200 ah aw" aw ax, "Time|Text")
		}
		this.hwnds.OutputWindow := hwndobj
	}
	
	GuiShowHide(state, options := ""){
		Gui, % (state ? "Show" : "Hide"), % options
	}
	
	AddLineToListView(parser, filename, filepath){
		Gui, ListView, % this.hwnds.LVLogList
		LV_Add("", (parser ? parser : "NONE"), filename, filepath)
		LV_ModifyCol()
	}
	
	AddLogClicked(){
		FileSelectFile, filename
		if ErrorLevel
			return
		this.AddLog(filename)
	}
	
	SyncClicked(){
		this.SyncLogs()
	}
	
	AddControl(aParams*){
		Gui, Add, % aParams[1], % "hwndhwnd " aParams[2], % aParams[3]
		return hwnd
	}
	
	GetDialogCoords(pw, ph){
		WinGetPos, x, y, w, h, % "ahk_id " this.hwnds.GuiMain
		px := (x + (w / 2)) - (pw / 2)
		py := y + (h / 2) - (ph / 2)
		x := px, y := py
		if (x < 0)
			x := 0
		if (y < 0)
			y := 0
		return {x: x, y: y}
	}
}

/*
Default parser - all numeric, decreasing significance format
eg 2017/01/23 11:34:25.163
Including...

2017-01-23 11:34:25,163 - Tachyon Consumer API
*/
Class DefaultLogParser {
	Regex := ""
	
	ParseLog(filename){
		raw_lines := this.ChunkLog(filename)
		if (raw_lines.Length() == 0)
			return 0
		return this.ParseLines(raw_lines)
	}
	
	ChunkLog(filename){
		FileRead, log_text, % filename
		RawLines := []
		Loop, Parse, % log_text, `n, `r
		{
			RawLines.push(A_Loopfield)
		}
		return RawLines
	}
	
	BuildRegex(){
		static g2d := "(\d{2})", g3d := "(\d{3})", g4d := "(\d{4})"
		static DateSep := "[-\/\\ ]", TimeSep := "[:\.,]", DateTimeSep := " ", TimeTextSep := " "
		this.Regex := "^" g4d DateSep g2d DateSep g2d DateTimeSep g2d TimeSep g2d TimeSep g2d TimeSep g3d TimeTextSep "(.*)"
	}
	
	ParseLines(raw_lines){
		this.BuildRegex()
		parsed_lines := []
		for line_num, raw_line in raw_lines {
			parsed_line := this.ParseLine(raw_line)
			if (parsed_line.text == "" || !parsed_line.time)
				continue
			parsed_lines.push(parsed_line)
		}
		return parsed_lines
	}
	
	ParseLine(raw_line){
		RegExMatch(raw_line, this.Regex, m)
		return {time: Trim(m1 m2 m3 m4 m5 m6 m7), text: Trim(m8)}
	}
}

/*
Agent
23 Jan 2017 11:38:52.995 [19416] TRACE - Unsubscribing Windows Service monitor for 'WSService'

Switch
11:40:59.772 DevStat_1: Closing sock=2928 () err=10054 "Dropped" (Core DevStat_1)

ConsumerAPI
2017-01-23 11:34:25,163 WARN  Tachyon.Server.Services.Consumer.ResponseProvider - Instruction ID 1004 is not in valid state for response retrieval - status == 1

^(.*)?(\d{2}):(\d{2}):(\d{2}).?(\d{3})?(.*)

^(.*)?(\d{2}:\d{2}:\d{2}[\.,]\d{3})(.*)

^(\d{4})[-\/\\ ](\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})[,\.](\d{3})

*/

/*
UK style increasing significance for date, month as 3 letter abbreviation
Including...

23 Jan 2017 11:38:52.995 Tachyon Agent
*/
Class DDMmmYYYYParser extends DefaultLogParser {
	BuildRegex(){
		static g2d := "(\d{2})", g3d := "(\d{3})", g3w := "(\w{3})", g4d := "(\d{4})"
		static DateSep := " ", TimeSep := "[:\.,]", DateTimeSep := " ", TimeTextSep := " "
		this.Regex := "^" g2d DateSep g3w DateSep g4d DateTimeSep g2d TimeSep g2d TimeSep g2d TimeSep g3d TimeTextSep "(.*)"
	}
	
	ParseLine(raw_line){
		static months := {jan: "01", feb: "02", mar: "03", apr: "04", may: "05", jun: "06", jul: "07", aug: "08", sep: "09", oct: "10", nov: "11", dec: "12"}
		RegExMatch(raw_line, this.Regex, m)
		return {time: Trim(m3 months[m2] m1 m4 m5 m6 m7), text: Trim(m8)}
	}
}

/*
Log is missing dates.
Infer starting date from file creation time of log
For subsequent lines, if the timestamp is earlier than the previous line, then add 1 day
*/
class DateFromFileCreationParser extends DefaultLogParser {
	BuildRegex(){
		static g2d := "(\d{2})", g3d := "(\d{3})", g4d := "(\d{4})"
		static TimeSep := "[:\.,]", DateTimeSep := " ", TimeTextSep := " "
		this.Regex := g2d TimeSep g2d TimeSep g2d TimeSep g3d TimeTextSep "(.*)"
	}
	
	ParseLog(filename){
		FileGetTime, file_time, % filename, C
		this.CurrentDay := SubStr(file_time, 1, 8)
		this.LastTime := 0
		return base.ParseLog(filename)
	}
	
	ParseLine(raw_line){
		RegExMatch(raw_line, this.Regex, m)
		time := Trim(m1 m2 m3 m4)
		if (this.LastTime){
			if (time < this.LastTime){
				this.CurrentDay += 1, Days
			}
		}
		return {time: this.CurrentDay time, text: Trim(m5)}
	}
}

; ===========================================================================================================================================================
; LIBRARY TO HANDLE SYNCING OF LISTVIEWS
class SyncedView {
	ListViews := []
	LVTopIndexes := []
	CurrentLV := 0
	CurrentLine := 0
	
	Add(options, fields){
		lv_count := this.ListViews.length()
		new_lv := new this.ListView(this, this.ListEvent.Bind(this, ++lv_count), options, fields)
		if (lv_count > 1){
			for i, lv in this.ListViews {
				if (new_lv.Height != lv.Height){
					msgbox % "ListView " lv_count " is not the same height as listivew " i
					ExitApp
				}
				if (new_lv.LineHeight != lv.LineHeight){
					msgbox % "LineHeights do not match"
					ExitApp
				}
			}
		}
		this.LineHeight := this.ListViews[1].LineHeight
		this.ListViews.push(new_lv)
		this.LVTopIndexes.push(1)
		return new_lv
	}
	
	AddLine(lv, options, aParams*){
		this.ListViews[lv].Add(options, aParams*)
	}
	
	SetSyncState(state){
		if (state){
			for i, lv in this.ListViews {
				if (i == 1){
					num_rows := lv.Rows
				} else {
					if (lv.Rows != num_rows){
						msgbox % "Row count for listview " i "does not match"
						return 0
					}
				}
			}
		}
		this._SetCallbackState(state)
		return 1
	}
	
	_SetCallbackState(state, ignore := 0){
		for i, lv in this.ListViews {
			if (ignore == i)
				continue
			lv.SetCallbackState(state)
		}
	}
	
	GetTopRows(){
		out := []
		for i, lv in this.ListViews {
			out.push(lv.GetTopRow())
		}
		return out
	}
	
	ListEvent(this_lv){
		Critical
		GuiEvent := A_GuiEvent, EL := ErrorLevel, EventInfo := A_EventInfo
		; Filter out any events we are not interested in.
		if (!(GuiEvent == "Normal" || GuiEvent == "s" || GuiEvent == "F" || GuiEvent == "K" || GuiEvent == "I"))
			return
		this.CurrentLV := this_lv
		;OutputDebug % "AHK| this_lv: " this_lv ", current: " this.CurrentLV ", A_GuiEvent: " GuiEvent ", A_EventInfo: " EventInfo 
		
		; Disable events for all other listviews
		this._SetCallbackState(0, this_lv)
		
		; Select new rows
		if (GuiEvent == "I" && EL == "SF"){
			new_selected_row := EventInfo 
			;OutputDebug % "AHK| new_selected_row: " new_selected_row
			for i, lv in this.ListViews {
				if (i == this_lv)
					continue
				lv.SelectRow(new_selected_row)
			}
		}

		top_rows := this.GetTopRows()
		this_lv_top_row := top_rows[this_lv]

		; Scroll ListViews
		if (this_lv_top_row != this.LVTopIndexes[this_lv]){
			for i, row in top_rows {
				if (i == this_lv)
					continue
				
				if (row != this_lv_top_row){
					this.ListViews[i].ScrollRows(this_lv_top_row - row)
					this.LVTopIndexes[i] := this_lv_top_row
				}
			}
		}
		; Re-Enable events for all other listviews
		this._SetCallbackState(1, this_lv)
	}
	
	class ListView {
		State := 0
		LineHeight := 0
		Rows := 0
		SelectedRow := 0
		
		__New(parent, callback, options, fields){
			this.callback := callback
			this.parent := parent
			Gui, Add, ListView, % "-Multi AltSubmit hwndhwnd " options, % fields
			this.hwnd := hwnd
			this._SetLineHeight()
			this.Height := this.GetSize().h
		}
		
		Add(options, aParams*){
			this.SetCurrentLV()
			LV_Add(options, aParams*)
			this.Rows++
		}
		
		_SetLineHeight(){
			if (h := this._GetRowHeight()){
				this.LineHeight := h
			} else {
				this.SetCurrentLV()
				LV_Add("","test")
				this.LineHeight := this._GetRowHeight()
				LV_Delete()
			}
		}

		/* 
		Returns the height for an item in the list-view control. 
			(all items, in the same control, have the same height) 

		If the list-view has no items, this function returns 0 
		*/ 
		_GetRowHeight(){ 
			VarSetCapacity(ItemBounds, 16), NumPut(boundType, ItemBounds) 
			;LVM_GETITEMRECT = LVM_FIRST + 14 = 0x100E 
			SendMessage, 0x100E, 0, &ItemBounds, , % "ahk_id " this.hwnd
			Top := NumGet(ItemBounds, 4, "int") 
			Bottom := NumGet(ItemBounds, 12, "int") 
			return Bottom - Top 
		} 

		SetCallbackState(state){
			if (state == this.State)
				return
			this.State := state
			fn := this.callback
			opt := (state ? "+g" : "-g")
			GuiControl, % opt, % this.hwnd, % fn
		}
		
		GetTopRow(){
			SendMessage, 0x1027, 0, 0, , % "ahk_id " this.hwnd
			return ErrorLevel + 1
		}
		
		ScrollRows(rows){
			SendMessage, 0x1014, 0, % this.LineHeight * rows, , % "ahk_id " this.hwnd
		}
		
		GetSize(){
			GuiControlGet, out, Pos, % this.hwnd
			return {x: outx, y: outy, w: outw, h: outh}
		}
		
		AutoAdjust(){
			this.SetCurrentLV()
			LV_ModifyCol()
		}
		
		SetCurrentLV(){
			Gui, ListView, % this.hwnd
		}
		
		SelectRow(row){
			this.SetCurrentLV()
			if (this.SelectedRow){
				this.SelectedRow := 0
				LV_Modify(this.SelectedRow, "-Select")
			}
			this.SelectedRow := row
			LV_Modify(row, "Select")
		}
	}
}

Re: [WIP][App] LogSync - Load two log files and view side-by-side, synchronized by timestamps

Posted: 09 Nov 2017, 08:10
by elmo
Hello EvilC,

This looks quite valuable. Am I reading correctly that it will not run under Autohotkey_L ?

May I trouble you for some insight as to the reason ?

I have been pondering Autohotkey_H but it seems confusing to implement; this might be the reason to try installing it again.

Thank you in advance for your time and attention.

Re: [WIP][App] LogSync - Load two log files and view side-by-side, synchronized by timestamps

Posted: 09 Nov 2017, 08:47
by evilC
The ListViews in the GUI are dynamically resizable.
AHK_H has built-in support for dynamically sizing GuiControls, AHK_L does not.
It could be done in AHK_L, but you would need the likes of AutoXYWH or Anchor

Re: [WIP][App] LogSync - Load two log files and view side-by-side, synchronized by timestamps

Posted: 09 Nov 2017, 10:20
by elmo
So appreciate the timely response.
I understand and that makes sense.
To date AutoXYWH has worked nicely and no familiarity with Anchor.
Hmmm, while I have you on the line, is there a definitive AHK_H deployment guide that you could point me to?
For some reason "grokking" the sequence to switch over, especially if one wishes to preserve their code, has eluded me.
If not, that is cool; thank you again for your response.

Re: [WIP][App] LogSync - Load two log files and view side-by-side, synchronized by timestamps

Posted: 09 Nov 2017, 10:49
by evilC
All you need to do to "Install" AHK_H is to replace AutoHotkey.exe with the appropriate one from the AHK_H zip, and drop msvcr100.dll into your AHK folder.

You can also use AHK-EXE-Swapper

Drop the AHK_H zip into the import folder
Click Refresh
Select the zip from the bottom pane
Click Import
Select the AHK_H version from the upper list
Click "Replace with selected version"

Re: [WIP][App] LogSync - Load two log files and view side-by-side, synchronized by timestamps

Posted: 09 Nov 2017, 17:00
by elmo
jumpin jehosephat !!!
must have been kharmatic thang that caused me to ask the question about AHK_H deployment.
perhaps if i can get through this initial switch then i will be able to press forward with re-compiling with a different password.
thank you so much for your guidance.

Re: [WIP][App] LogSync - Load multiple log files and view side-by-side, synchronized by timestamps

Posted: 09 Jan 2018, 13:31
by evilC
I have stopped development of this version of LogSync, I am re-writing it in C#

The new project is here: https://github.com/evilC/LogSync