3

Im trying to get a log system working in an application in MS Access 2016 whenever any user make any action such as login, editing etc.

So far the code I wrote is fairly simple by using open statement,

Public Sub WriteLog(ByVal strContent As String)
    fileLog = FreeFile
    Open "D:/log.txt" For Output As fileLog

    Print #fileLog, strContent
    Close #fileLog
End Sub

This is not good because I intent to write the log file in a shared network, which mean many users may simultaneously open the file to write. This will definitely throw error. I thought of doing some queuing to write to the file but havent come to any solution. Is it just impossible to do this?

Edited:

Recursively check if file open and write to file after the file is closed, a way to somehow 'queue' to write to a file. Maybe need to add some code to make sure a limit to recursively execute this function.

Function AvailableToWrite()

    ' Test to see if the file is open.
    If IsFileOpen("D:\log.txt") Then
        AvailableToWrite = IsFileOpen() ' Recursively check until file is closed
    Else
        AvailableToWrite = True
    End If

End Function

Function IsFileOpen(filename As String)
    Dim filenum As Integer, errnum As Integer

    On Error Resume Next
    filenum = FreeFile()
    ' Attempt to open the file and lock it.
    Open filename For Input Write As #filenum
    Close filenum
    errnum = Err           ' Save the error number that occurred.
    On Error GoTo 0        ' Turn error checking back on.

    ' Check to see which error occurred.
    Select Case errnum

        ' No error occurred.
        ' File is NOT already open by another user.
        Case 0
         IsFileOpen = False

        ' Error number for "Permission Denied."
        ' File is already opened by another user.
        Case 70
            IsFileOpen = True

    End Select

End Function
Salam.MSaif
  • 209
  • 3
  • 10
  • 2
    ***Ahh! The irony!*** You're using a *multi user database system*! Make the log file in an Access table that's ***meant*** for multiple users to share, not a lame text file! You could even make a batch file or set it up so the users could email or SMS their notes to the datrabase (with minimal work!) In fact I have one i use constantly, I'll give you the code, one minute! – ashleedawg Mar 31 '18 at 15:50
  • @ashleedawg customer im dealing with prefer to have a text file as the logging system, this is a pain. Plain impossible?, My customer is like the admin of the application, so they want a simple log file to read what the users are doing.. – Salam.MSaif Mar 31 '18 at 15:54
  • you can still export it to text any time you want. In fact, every time it's updated. But this way there won't be conflicts (ie., 2 users try to update at same time, so one gets lost). I'll show you , you'll have your text file still, one moment... – ashleedawg Mar 31 '18 at 15:57
  • @ashleedawg Thanks! Greatly appreciate this. I did some reading and in vba ms access one can lock the file while writing into it, does this related to , say, making a queue to writing to that file? – Salam.MSaif Mar 31 '18 at 16:04
  • if it's locked then noone else can write to it. (so one event can get lost, or could even crash the program)... THIS WAY, the table is never locked. So if I have the table open, and you add to it, I won't see the updates, but next time I close & re-open, I will see the current version (without missing anything) ...in fact technically in this case the TXT file can be deleted if you want, and it will just make a new one (with complete data) next time an event is logged. – ashleedawg Mar 31 '18 at 16:12
  • Test it out: make a sub run on 3 computers, loop to submit 100 items each all at once, see what happens. (I'm not multi user so I don't use it that way but I would be curious about the test!) I'm 99.9% sure it will be fine because its *the database* storing it, the text file is nothing. OH - I will add 2 more lines of code to it so that IF there is a problem writing the text file, it will just ignore it (since the data will be stored fine in the table, and the next log will re-create the text file.) – ashleedawg Mar 31 '18 at 16:22
  • Got 'er working? Let me know if you have any issues. – ashleedawg Mar 31 '18 at 20:08

2 Answers2

4

Usually, writing a line to text file takes only a split second.

Thus, you may simply, in a loop, catch the error if your function can't write to the file, wait a tiny random length split second, then try again until success.

Addendum

Method for a variable delay in case of a block before a new attempt is done:

' Function to run a sequence of updates at random intervals for a preset
' duration while handling any concurrency issue that may arise.
' Run the function concurrently in two or more instances of Microsoft Access.
'
' Output logs the updates and lists the errors encountered when an update
' collides with an ongoing update from (one of) the other instance(s).
'
' 2016-01-31. Gustav Brock, Cactus Data ApS, CPH.
'
Public Sub ConcurrencyAwareTest()

    Dim db          As DAO.Database
    Dim rs          As DAO.Recordset
    Dim fd          As DAO.Field

    Dim StopTime    As Single
    Dim Delay       As Single
    Dim Attempts    As Long
    Dim LoopStart   As Single
    Dim LoopEnd     As Single
    Dim Loops       As Long

    Dim SQL         As String
    Dim Criteria    As String
    Dim NewValue    As Boolean

    SQL = "Select * From " & TableName & ""
    Criteria = KeyName & " = " & CStr(KeyValue) & ""

    Set db = CurrentDb
    Set rs = db.OpenRecordset(SQL, dbOpenDynaset, dbSeeChanges)

    rs.FindFirst Criteria
    Set fd = rs.Fields(FieldName)

    ' Set time for the test to stop.
    StopTime = Timer + Duration
    ' Let SetEdit and GetUpdate print debug information.
    DebugMode = True

    ' At random intervals, call updates of the field until StopTime is reached.
    While Timer < StopTime

        ' Postpone the next update.
        Delay = Timer + Rnd / 100
        While Timer < Delay
            DoEvents
        Wend
        Loops = Loops + 1
        LoopStart = Timer
        Debug.Print Loops, LoopStart

        ' Perform update.
        NewValue = Not fd.Value
        Do
            ' Count the attempts to update in this loop.
            Attempts = Attempts + 1
            ' Attempt edit and update until success.
            SetEdit rs
                fd.Value = NewValue
        Loop Until GetUpdate(rs)

        LoopEnd = Timer
        ' Print loop duration in milliseconds and edit attempts.
        Debug.Print , LoopEnd, Int(1000 * (LoopEnd - LoopStart)), Attempts
        Attempts = 0

    Wend
    rs.Close

    DebugMode = False
    Set fd = Nothing
    Set rs = Nothing
    Set db = Nothing

End Sub

The purpose was to prove the concept described in the article here:

Handle concurrent update conflicts in Access silently

Gustav
  • 53,498
  • 7
  • 29
  • 55
  • I'm not sure that this answers the OP's original question. – ashleedawg Mar 31 '18 at 20:07
  • @ashleedawg: As the users will not only open the file but also close it again, it does. It's a classic method not invented by me. The question is: _many users may simultaneously open the file to write. This will definitely throw error. I thought of doing some queuing to write to the file but havent come to any solution._ – Gustav Mar 31 '18 at 20:42
  • uh - are you talking about the text file? They can delete that if they want, it will be regenerated constantly. The *file* in the question was the text file. He was concerned about the text file being open while he tried to `Print #` to it. Or are you talking about the database, that will have specific canned notes for the users to choose from? Access can handle multiple users, it will be alright, and nothing unexpected will occur. Even if there were a risk, there's a lot more a user can do wrong in a database than enter a quote is a log file. The function will not keep the data any safer. – ashleedawg Mar 31 '18 at 20:58
  • It's not like there's a risk of nefarious SQL Injection. Worst case result of your concern? A run-time error. Click "End" and try the entry again. Even that won't happen since I adjusted the code to remove quotes. Anyway this is a **pointless** debate because the OP understands how the code works, understands your concern, and he is satisfied with the result. ...and your "answer" *still* isn't an answer. – ashleedawg Mar 31 '18 at 21:00
  • @ashleedawg: Uhh ... you go East-West confused, it seems. Seems like you've never tried writing to a text file. It can be opened by, say, Notepad, and you can still write to it. No one demands you to lock the file while reading. – Gustav Mar 31 '18 at 21:57
  • @Gustav I was thinking like using a recursive function to check if the file is open. I will write it down, just a moment – Salam.MSaif Apr 01 '18 at 00:08
  • @Gustav I edited it in the question. How do you say about it? Tell me about your solution on 'in a loop, catch the error if your function can't write to the file, wait a tiny random length split second, then try again until success'. – Salam.MSaif Apr 01 '18 at 00:17
  • That might work. I had something even simpler in mind - just try again. I've added a more fancy method to the answer, one that waits a little before a new attempt. – Gustav Apr 01 '18 at 09:39
  • @Gustav I see your way is try again in a specified duration, your method is for checking recordset but maybe this concept can be implemented to the opening of text file yes? – Salam.MSaif Apr 01 '18 at 10:37
  • Yes, that was my idea. But, as said, you may just have to "try again" as the writing other user will end the operation and release the lock in a split second. – Gustav Apr 01 '18 at 17:19
3

Table Structure:

table


Procedure to log events

Sub WriteLog(Optional note As String)
   'add event to log
    DoCmd.SetWarnings False
    DoCmd.RunSQL "INSERT INTO tblLog (logNote) SELECT """ & Replace(note,"""","'") & """"
    DoCmd.SetWarnings True

    'export to text file
    On Error Resume Next 'ignore error
    DoCmd.TransferText acExportDelim,,"tblLog","c:\LogFile.txt",True
    On Error Goto 0 'back to normal error handling
    Debug.Print "Wrote to log & updated text file."
End Sub

Usage:

WriteLog "Your note here" : saves a record with current date/time plus "Your note here"
WriteLog : saves a record with only date/time


Example of (my) filled table:

(Click to Englarge)
img


Example of text file:

It's comma-separated by default (so it could be opened in Excel if you wanted) but with a couple extra steps could be exported in "fixed width" instead, by creating a specification and also using the acExportFixed flag.

log

ashleedawg
  • 20,365
  • 9
  • 72
  • 105
  • @SalamMSaif - does this make sense? – ashleedawg Mar 31 '18 at 16:13
  • Presumable the Default Value for your `logDateTime` field is set to `Now()` given that you are not explicitly populating this field in your SQL statement. – Lee Mac Mar 31 '18 at 16:21
  • @LeeMac I'm sorry, good catch - no the table does that itself... On the field for `logDateTime` enter `Default Value` of `=Now()` – ashleedawg Mar 31 '18 at 16:25
  • @LeeMac i will change the image for the table with all the details – ashleedawg Mar 31 '18 at 16:27
  • No worries! You have proposed a good solution and I enjoy the consistent quality of your posts. – Lee Mac Mar 31 '18 at 16:28
  • 1
    @LeeMac Thanks :) I have an absolutely terrible memory (like, handicapping) so I have to stay super organized with this kind of stuff, and any time I get a bit of code that works, I keep a copy of it carefully organized away in my "snippets" folder (such as the log file) and then I only need to copy/paste next time, otherwise I'm Googling the same thing over and over and over... Also I like answering questions for others because **at least** half the time, I am learning (or re-learning) something along the way! – ashleedawg Mar 31 '18 at 16:32
  • @LeeMac ...but it ***hurt*** me when you said you were going to use a text file like that... it's the equivalent of using a calculator to do the math for Excel.... (and don't laugh - I answered a question a couple days ago for someone who **was** using a **calculator** for ***averages*** because they didn't think Excel was capable. **;-(** I cried a little.) – ashleedawg Mar 31 '18 at 16:36
  • Nice, but `WriteLog "Bobby says ""Hello"""` - kaboom. -- Use Gustav's [`CSql()` function](https://stackoverflow.com/a/36494189/3820271) when concatenating variables with SQL. It handles string and other variables. -- `CurrentDb.Execute "INSERT INTO tblLog ( logNote ) VALUES (" & CSql(note) & ")"`. – Andre Mar 31 '18 at 18:33
  • LOL ya I suppose, Andre, but maybe you don't understand the purpose and scope of this function. I am quite confident that there will be no hard-coded Hello Bobby's... and if there was, no Kaboom, just a "click `End`". There's probably more risk of the plug being tripped over. Just to make sure: **@Salam - in case you were planning on it, don't use quad-quotes (`""like this"") in the log message.** I also made a change to convert them to `'`. Problem solved. – ashleedawg Mar 31 '18 at 20:03
  • @Andre - Gustav's `CSql` look handy; I've made a note of it. But it's unnecessary to quadruple the length of this code for no reason. Users don't have control over the log messages. This is intended to be a simple solution, not a complex one that covers every possibly issue. You know what else mine's missing? *Error Handling*... uh oh. :-) – ashleedawg Mar 31 '18 at 20:06
  • @ashleedawg love the concept! I will definitely try this, i dont event have to worry text file being deleted and all info is lost!, and maybe I will add some mkdir code to export it to text file based on the current date like 2018/04/01.txt – Salam.MSaif Mar 31 '18 at 23:48
  • Note that using parameters (how? See [here](https://stackoverflow.com/a/49509616/7296893)) is often preferred above other solutions for passing values to an SQL statement. For example, your current solution will replace double with single quotes, and insert empty strings instead of `Null`. CSql has it's own set of oddities. – Erik A Apr 01 '18 at 10:17