B4J Question Problem with Resumable Sub that recursively call itself.

max123

Well-Known Member
Licensed User
Longtime User
Hi all,

I've this code in my library (at bottom of post), the entry point here is the MinifyAllFilesOnFolderAsync sub,
the sub should recursively minimize all JS and CSS files in the specified folder SourceDir and save them in DestDir
mantaing the folder structure.

I call it from Main this way:
B4X:
Dim Compressor As YUICompressor
Compressor.Initialize(Me, "Compressor")
Compressor.MinifyAllFilesOnFolderAsync (File.DirTemp, File.DirApp, True)
Wait For Compressor_FolderComplete (Success As Boolean, Item As MinifiedFolderItem)

The MinifyAllFilesOnFolderAsync sub is called, then inside it, the sub MinifyAllFilesOnFolderInternalAsync is called,
a Wait For wait for all files are processed recursively and then the result return should return back to the calling sub (a Wait For in the Main)

It is called with:
B4X:
    MinifyAllFilesOnFolderInternalAsync (SourceDir, DestDir, UseMinPrefix)
    Wait For Compressor_FolderComplete (Success As Boolean, FolderItem As MinifiedFolderItem)

At this point all files are listed in the SourceDir folder, it process some files and recursively search files inside subfolfers and process them if they are JS or CSS, but then at some time the execution flow is not what I expect.

The sub MinifyAllFilesOnFolderInternalAsync call itself recursively if folder is found, but it should only terminate one time and call back the MinifyAllFilesOnFolderAsync to process it's return and then return it back to the Main with CallSubDelayed3 .

Some files are processed, but I never see the COMPLETED log, the sub never return back to calling sub.

Note that MinifyFileToFileAsync called to process a file when found, is also asyncronous sub, and it works well if called from the main.

In the MinifyAllFilesOnFolderInternalAsync I even use File.ListFilesAsync instead of File.ListFiles.

I followed it for 4 hours line by line in debug mode, but no luck.
The For Each f As String In Files should only terminate one time and here the sub should return back the non-Null value, that is a MinifiedFolderItem Type.
Instead it is called more times, even if the sub itself do not return back but continue it's execution, but at the end do not fires the event on the calling sub.

Please someone know what is wrong here ?
Many thanks.

B4X:
#Event: FolderComplete (Success As Boolean, Item As MinifiedFolderItem)

Type MinifiedFolderItem (SourceDir As String, DestDir As String, OriginalSize As Int, MinifiedSize As Int, Compression As Float)
 
Public Sub MinifyAllFilesOnFolderAsync (SourceDir As String, DestDir As String, UseMinPrefix As Boolean) As ResumableSub
    mFolderFilesOriginalSize = 0
    mFolderFilesMinifiedSize = 0
   
'''    Wait For (MinifyAllFilesOnFolderInternalAsync (SourceDir, DestDir, UseMinPrefix)) Complete (FolderItem As MinifiedFolderItem)
    MinifyAllFilesOnFolderInternalAsync (SourceDir, DestDir, UseMinPrefix)
    Wait For Compressor_FolderComplete (Success As Boolean, FolderItem As MinifiedFolderItem)
   
    Log("======================== COMPLETED ========================")

    If Success Then
        FolderItem.SourceDir = SourceDir
        FolderItem.DestDir = DestDir
        FolderItem.OriginalSize = mFolderFilesOriginalSize
        FolderItem.MinifiedSize = mFolderFilesMinifiedSize
        FolderItem.Compression = MapFloat(mFolderFilesMinifiedSize, 0, mFolderFilesOriginalSize, 100, 0)
        If SubExists(mModule, mEventName & "_FolderComplete") Then
            CallSubDelayed3(mModule, mEventName & "_FolderComplete", True, FolderItem)
        End If
    Else
        If SubExists(mModule, mEventName & "_FolderComplete") Then
            CallSubDelayed3(mModule, mEventName & "_FolderComplete", False, Null)
        End If
    End If
   
    Return True
End Sub

Private Sub MinifyAllFilesOnFolderInternalAsync (SourceDir As String, DestDir As String, UseMinPrefix As Boolean) As ResumableSub
   
    ' Add error handling for file existence check
    If File.Exists(SourceDir, "") = False Or File.IsDirectory(SourceDir, "") = False Then
        Log("The SourceDir folder does not exist: " & SourceDir)
        CallSubDelayed3(Me, mEventName & "_FolderComplete", False, Null)
        Return True ' Ensure proper termination
    End If
   
    If DestDir = "" Then DestDir = SourceDir
   
    Log(" ")
    Log("CURRENT SOURCE FOLDER: " & SourceDir)
    Log("  CURRENT DEST FOLDER: " & DestDir)
    Log(" ")
   
'    Dim Files As List = File.ListFiles(SourceDir)

    ' Handle asynchronous file listing
    Wait For (File.ListFilesAsync(SourceDir)) Complete (Success As Boolean, Files As List)
   
    If Success Then
        For Each f As String In Files
       
            If File.IsDirectory(SourceDir, f) = False Then
                Log("FOUND FILE: " & File.Combine(SourceDir, f))
           
                Dim fName As String
           
                ' Touch .js and .css files only, ignore all other extentions
           
                If f.EndsWith(".js") Then
                    If UseMinPrefix Then
                        If f.ToLowerCase.Contains(".min") = False Then
                            fName = f.SubString2(0, f.IndexOf(".js")) & ".min.js"
                        Else
                            Log("JS File already minified. Skip it: " & File.Combine(SourceDir, f))
                            Continue
                        End If
                    Else
                        fName = f
                    End If
                    Log(" ") : Log(">>>>>>>>> PROCESS JS FILE: " & File.Combine(SourceDir, f))
                    Log("Minified JS file name: " & fName)
                    mInternalOperation = True
                    '''                    Wait For (MinifyFileToFileAsync (SourceDir, f, DestDir, fName)) Complete (Success As Boolean)
                    MinifyFileToFileAsync (SourceDir, f, DestDir, fName)
                    Wait For Compressor_FileComplete (Success As Boolean, jsItem As MinifiedFileItem)
                    Log("JS Returned from MinifyFileToFileAsync")
                    If Success Then
                        mFolderFilesOriginalSize = mFolderFilesOriginalSize + jsItem.OriginalSize
                        mFolderFilesMinifiedSize = mFolderFilesMinifiedSize + jsItem.MinifiedSize
                    Else
                        CallSubDelayed3(Me, mEventName & "_FolderComplete", False, Null)
                    End If
                Else If f.EndsWith(".css") Then
                    If UseMinPrefix Then
                        If f.ToLowerCase.Contains(".min") = False Then
                            fName = f.SubString2(0, f.IndexOf(".css")) & ".min.css"
                        Else
                            Log("CSS File already minified. Skip it: " & File.Combine(SourceDir, f))
                            Continue
                        End If
                    Else
                        fName = f
                    End If
                    Log(" ") : Log(">>>>>>>>> PROCESS CSS FILE: " & File.Combine(SourceDir, f))
                    Log("Minified CSS file name: " & fName)
                    mInternalOperation = True
                    '''                    Wait For (MinifyFileToFileAsync (SourceDir, f, DestDir, fName)) Complete (Success As Boolean)
                    MinifyFileToFileAsync (SourceDir, f, DestDir, fName)
                    Wait For Compressor_FileComplete (Success As Boolean, cssItem As MinifiedFileItem)
                    Log("CSS Returned from MinifyFileToFileAsync")
                    If Success Then
                        mFolderFilesOriginalSize = mFolderFilesOriginalSize + cssItem.OriginalSize
                        mFolderFilesMinifiedSize = mFolderFilesMinifiedSize + cssItem.MinifiedSize
                    Else
'                        If SubExists(Me, mEventName & "_FolderComplete") Then
                        CallSubDelayed3(Me, mEventName & "_FolderComplete", False, Null)
                        Return True
'                        End If                       
                    End If
                End If
            Else
                Log("FOUND FOLDER: " & File.Combine(SourceDir,f))
               
                ' Recursive call to process subfolders
                ''' Wait For (MinifyAllFilesOnFolderInternalAsync (File.Combine(SourceDir, f), File.Combine(DestDir, f), UseMinPrefix)) Complete (FolderItem As MinifiedFolderItem) ' << RECURSIVELY CALL ITSELF
                MinifyAllFilesOnFolderInternalAsync (File.Combine(SourceDir, f), File.Combine(DestDir, f), UseMinPrefix)
                Wait For Compressor_FolderComplete (Success As Boolean, FolderItem As MinifiedFolderItem) ' << RECURSIVELY CALL ITSELF
                Log("Returned from MinifyAllFilesOnFolderInternalAsync")
                                   
                If Success = False Then
                    CallSubDelayed3(Me, mEventName & "_FolderComplete", False, Null)
                    Return True ' Ensure proper termination
                End If
                                           
'                Continue  ' Should this be used to skip next iteration ???
            End If
        Next
    Else
        Log("ERROR. CANNOT LIST FILES")
        '        Return Null
        CallSubDelayed3(Me, mEventName & "_FolderComplete", False, Null)
        Return True ' Ensure proper termination
    End If
   
    ' Call FolderComplete event only once after processing all files and subfolders
    Dim FolderItem As MinifiedFolderItem
    FolderItem.Initialize
   
    '    Sleep(1)
    Log(" ") : Log ("FOLDER MINIFICATION END. SHOULD ONLY CALLED AT END WHEN PROCESSED RECURSIVELY FOLDERS, SUBFOLDERS AND FILES ONE BY ONE WITH For Each")  ' <<<<< THIS IS CALLED MORE TIMES BUT SHOULD ONLY CALLED ONE TIME
    CallSubDelayed3(Me, mEventName & "_FolderComplete", True, FolderItem)
   
    Return True ' Ensure proper termination
End Sub
 
Last edited:

max123

Well-Known Member
Licensed User
Longtime User
Please, any help is appreciated.....

Is something that have to do with this part of code that call the sub itself recursively, because if I comment it, all the files are successfully processed in the main folder root and here the COMPLETE log magically appears in the log. So how to manage it ?
B4X:
            Else
                Log("FOUND FOLDER: " & f)
         
                ''' Wait For (MinifyAllFilesOnFolderInternalAsync (File.Combine(SourceDir, f), File.Combine(DestDir, f), UseMinPrefix)) Complete (FolderItem As MinifiedFolderItem) ' << RECURSIVELY CALL ITSELF
                MinifyAllFilesOnFolderInternalAsync (File.Combine(SourceDir, f), File.Combine(DestDir, f), UseMinPrefix) ' << RECURSIVELY CALL ITSELF
                Wait For Compressor_FolderComplete (Success As Boolean, FolderItem As MinifiedFolderItem)
         
'                If Success = False Then
                ''    If SubExists(Me, mEventName & "_FolderComplete") Then
'                       CallSubDelayed3(Me, mEventName & "_FolderComplete", False, FolderItem)
                ''    End If
'                End If
                         
                Log("Returned from MinifyAllFilesOnFolderInternalAsync")
'                Continue  ' Should this be used to skip next iteration ???
            End If
And this is the log:
 
Last edited:
Upvote 0

max123

Well-Known Member
Licensed User
Longtime User
Ok. this forum seem dead, noone replies...
 
Upvote 0

Daestrum

Expert
Licensed User
Longtime User
It's probably that members (including myself) are finding it hard to understand the problem.

Maybe make a small example app, which doesn't read files and use minify etc, but simply adds a value to a global variable. That way we wouldnt have to make an elaborate setup just to try and help.
 
Upvote 0

OliverA

Expert
Licensed User
Longtime User
One thing I noticed is that in MinifyAllFilesOnFolderAsync you have
B4X:
    MinifyAllFilesOnFolderInternalAsync (SourceDir, DestDir, UseMinPrefix)
    Wait For Compressor_FolderComplete (Success As Boolean, FolderItem As MinifiedFolderItem)

How do you know that the callback is prefixed with Compressor? Since the callback code in MinifyAllFilesOnFolderInternalAsync is

B4X:
CallSubDelayed3(Me, mEventName & "_FolderComplete", False, Null)
?

MinifyAllFilesOnFolderAsync also uses mEventName for the callback therefore something is wrong with using Compressor_ as a prefix for the Wait For. If MinifyAllFilesOnFoldersInternalAsync is just a private function/method of the class, then use

B4X:
CallSubDelayed3(Me, "Compressor_FolderComplete", False, Null)

In MinifyAllFilesOnFolderInternalAsync

Note:
I'm only focusing on the 2nd parameter of the CallSubDelayed3 method here.
 
Upvote 0

max123

Well-Known Member
Licensed User
Longtime User
Thanks both @Daestrum and @OliverA, I now changed my code and I have a full recursion on ResumableSubs.
Actually I processed 120 files inside main folder and nested sub-folders, but something is already wrong in my code.

@OliverA this on my actual code works well, and call back the calling sub MinifyAllFilesOnFolderAsync:
B4X:
CallSubDelayed3(Me, mEventName & "_AllFolderComplete", True, mFolderItem)

But you are absolutely right, this works for me just because in the Main I initialize the class with Compressor event.
Many thanks for the tip, I will change it in my code. ? ? ?

Actually I have 2 problems.

The first (big) problem is that Resumable Sub call itself recursively, but never reach the end of a sub to return back to a calling sub that then returs to the Main.
To solve this I used a global list, then add all folder names in the root, then any folder I process, if it is not a sub-folder I remove this from the List.
After that I check if List size reached zero size, if not I will continue to next files and folders, by calling a _FolderComplete inside the same sub, if size reached I will return back to a calling sub that now I changed to this:
B4X:
    MinifyAllFilesOnFolderInternalAsync (SourceDir, DestDir, UseMinPrefix)
    Wait For Compressor_AllFolderComplete (Success As Boolean, FolderItem As MinifiedFolderItem)
This works but I'm sure it is not the right way to do it. At the end when there are no folders and files to process, it should go to the end of a sub and finally call the calling sub.

The second problem I have is that all recursively folders and sub-folders are scanned, every JS and CSS files are regulary processed, but this not happen in just one folder when I have an sub-folder with inside a file that don't need to be processed. Here when returning back, this line do not more call the _FolderComplete event and just blocks here, no errors at all but it just blocks the process I think in an infinite loop.
B4X:
CallSubDelayed3(Me, mEventName & "_FolderComplete", True, Null)

As Daestrum says, may I have to create a small project to reproduce it, by just replacing the sub that process files to always return true.
 
Last edited:
Upvote 0

max123

Well-Known Member
Licensed User
Longtime User
So, I reproduced it in a separate small project, it works same that my original project.

Here I show both my problems:

- First problem is the end of a MinifyAllFilesOnFolderInternalAsync sub that is never reached, as I explained I use a global list to know when there are no more folders and files to process, but this is not good, it should reach the end of a sub without use a list to know that threre are no more folders and files to process.

- Second problem, I placed in the InputFolder (to unzip and place on Objects folder) a rpm folder that block the process, by removing it from InputFolder, all other folders and files are processed and the sub MinifyAllFilesOnFolderInternalAsync return back a value to calling sub MinifyAllFilesOnFolderAsync, that successfully return back to the Main and all seem works fine here.
I've tried to process a lot of folders and sub-folders and return back right values to the main, only this rpm folder blocks, it contains a changes folder with just inside a file that have not to be processed. In other contests folder recursion seem to happen without problems.

Because is difficult to me working with folders and subfolders and manage files directly from DirAsset without copy folder by folder and file by file, I've attached the InputFolder zip, just unzip it in the B4J project Objects folder, it contains 4 folders with inside some js and css files and some html files, just to test files that need and not need to be processed, even not to be copied.

When you execute, the \Objects\OutputFolder will be created and all subfolders and files are saved here mantaining the same folder structure, but just renaming files by adding .min extension.

This do not reach the end, because rpm folder block the process. Try to remove rpm folder from \Objects\InputFolder and run the project again, this time it works and all subs returns back to the main with the MinifiedFolderItem filled with values.
 

Attachments

  • InputFolder.zip
    22.9 KB · Views: 96
  • RecursiveResumableSub.zip
    14 KB · Views: 124
Last edited:
Upvote 0

OliverA

Expert
Licensed User
Longtime User
Here I show both my problems:
Using Method/WaitFor/CallSubDelayed way of handling things jams up after so many recursive calls. I'm attaching a project with 3 classes. The first class, TraverseFolders uses the same process as yours. It jams up after processing so many directories/files. TraverseFolders2 rewrites everything to use Subs defined As ResumableSub without using CallSubDelayed. TraverseFolder3 uses Method/WaitFor/CallSubDelayed for the public-facing method (allowing for the #Event definition) and declares all private Subs As ResumableSub. Both TraverseFolder2 and TraverseFolder3 have no issues processing folders with large counts of sub-folders/files.
 

Attachments

  • TraverseFolders_20240509_v1.zip
    13.2 KB · Views: 130
Upvote 0

max123

Well-Known Member
Licensed User
Longtime User
Many Thanks @OliverA I will try it. ?

EDIT: I tried it just now, placing my four folders in c:\temp, only TraverseFolder2 seem to work as expected, and it is the only one that (when all folders and files are processed) return back to the Main. The TraverseFolder1 blocks as my code in rpm sub, TraverseFolder3 seem to process all files, but at the end do not return to the Main. I will study your code better in debug mode using breakpoints. Thanks
 
Last edited:
Upvote 0

OliverA

Expert
Licensed User
Longtime User
TraverseFolder3 seem to process all files, but at the end do not return to the Main
I discovered a bug in TraverseFolder3. Try it with this version.
 

Attachments

  • TraverseFolders_20240510_v2.zip
    13.2 KB · Views: 99
Upvote 0

OliverA

Expert
Licensed User
Longtime User
That did not fix the issue. Interestingly, it looks like it gets stuck in the last recursive call of TraverseAsyncInternal, without returning to TraverseAsync, and therefore the CallSubDelayed3 to Main is never executed.
 
Upvote 0

max123

Well-Known Member
Licensed User
Longtime User
Mmmm, Thanks. I will try it. So seem there is no way to just use CallSubDelayed as I do in my code ?
Now I will sudy better the code flow.
 
Upvote 0

OliverA

Expert
Licensed User
Longtime User
So seem there is no way to just use CallSubDelayed as I do in my code ?
The final return to Main can be a CallSubDelayed. I fixed the bug (it was a silly one). TraverseFolders3 works correctly now.
 

Attachments

  • TraverseFolders_20240510_v3.zip
    13.2 KB · Views: 72
Upvote 0

max123

Well-Known Member
Licensed User
Longtime User
Now even FolderTraverse3 seem to works to me, it returns to the main, here a screenshoot.
 

Attachments

  • Screenshot 2024-05-10 122542.png
    70.7 KB · Views: 81
Upvote 0

max123

Well-Known Member
Licensed User
Longtime User
It looked too good, unfortunately my sub rpm still blocks and doesn't allow me to return to the Main.
There is still something wrong in FolderTraverse.
I will check better the code flow.
 

Attachments

  • Screenshot 2024-05-10 124343.png
    105.7 KB · Views: 86
Last edited:
Upvote 0

OliverA

Expert
Licensed User
Longtime User
There is still something wrong in FolderTraverse1
As expected. Using CallSubDelayed recursively does not seem to work. Only FolderTraverse2 and FolderTraverse3 work. 2 defines all involved methods As Resumable Sub. 3 uses CallSubDelayed to take advantage of the #Event attribute that can be used to define events for a class. I may make a separate post later in the forum to inquire why recursively using CallSubDelayed does not work.
 
Upvote 0

max123

Well-Known Member
Licensed User
Longtime User
If you want to know because this happen, you will have to place this:
B4X:
Public Sub TraverseAsync(SourceDir As String)As ResumableSub
    cProcessFile = 0
    dProcessFile = 0
so the counts reset any time and all three traverse can be compared in the log

I will try to compare Traverse and Traverse2 and if I succcess I will post a screenshot.
 
Last edited:
Upvote 0

max123

Well-Known Member
Licensed User
Longtime User
Here the log difference between FolderTraverse, FolderTraverse2 and FolderTraverse3 classes. The first do nort return back, but this strangely seem to happen only with this rpm folder, it is the only one of 3 folders that have a subfolder with inside a file.
 

Attachments

  • Screenshot 2024-05-10 140722.png
    91.4 KB · Views: 73
Last edited:
Upvote 0

OliverA

Expert
Licensed User
Longtime User
Here the log difference between FolderTraverse2 and FolderTraverse, the first completes and return to the Main, the second do not.
Correct. There seems to be an issue with recursion and CallSubDelayed. The solution, for now, is to use ResumableSubs (as used in TraverseFolders2 and TraverseFolders3).
 
Upvote 0

max123

Well-Known Member
Licensed User
Longtime User
@OliverA I want to use your TraverseFolder3 method in my library, but I've difficult to adapt my Subs to even return a List back,
because the main sub called (the one that process files) is a Public sub to just process one file, and it cannot return a List, already return Success and MinifiedFileItem.

Can I just use a global List without return it back from the ProcessFile and then from TraverseAsyncInternal ?

May I can just fill and clear it when necessary without return back from the Subs.

Now I will try to creare TraverseFolders4 with global list.
 
Last edited:
Upvote 0
Cookies are required to use this site. You must accept them to continue using the site. Learn more…