Tek-Tips is the largest IT community on the Internet today!

Members share and learn making Tek-Tips Forums the best source of peer-reviewed technical information on the Internet!

  • Congratulations gkittelson on being selected by the Tek-Tips community for having the most helpful posts in the forums last week. Way to Go!

Close an Open File, Not a Table 1

Status
Not open for further replies.

stanlyn

Programmer
Sep 3, 2003
945
US
Hi,

Assume that I opened a file with =FOPEN('d:\test.jpg',0)
Notice that I did not assign a variable to get the file's handle.

What commands could I use to close the file without knowing the handle?

Is there any commands where I can pass the filename in and close it?

I know the file is open by vfp as Win8 reports it as in use by vfp9 when I try to delete it at the OS level, and I'm looking for a way to close it, release it, unlock it from within vfp. If I close and restart vfp, the file can be deleted.

I've googled all over and cannot find any resource that shows how to close it, and many, many ways to test if it is open, but no way to close it if it's open.

Is there way of getting the handle to the open file?

How big is an average size handle, more than 2 digits?

Would something like this work?

x=1
do while x < 100
fclose(x) &&spinning through numbers 1-99
x=x+1
endo

What effects should I see with buffered vs unbuffered mode options for the fopen() function? Where is that discussed in detail?

Thanks,
Stanley
 
Hi Dan,

>> You have answered your own question but you don't recognize it.

I don't think so... Nowhere in my code do I issue a fopen() command, therefore not getting the opportunity ti gets handle. I have repeatedly made this statement 5-6 times in my posts. Now that we don't know what the handle is, "how do we programmatically get it in order to issue a fclose() command. I stated that the vfp team forgot to include a fclose(lcFilename), or a close(lcFileName), or a release (lcFileName) or any other way to deal with this issue. They can test if the file is open, but cannot explicitly close it.

Now, please prove me wrong,
Stanley
 
Hi Olaf,

>> If you open a file you know its handle and close it that way. Instead of trying to find code, that could work without the handle simply use it.

I don't use fopen() anywhere... See code above and explanation on why I don't have the handle...

Thanks,
Stanley

 
Hi Scott,

>> Prehaps I'm confused, but if you have the code available, why not just put:
>> lnFileHandle =FOPEN('d:\test.jpg',0)
>> Find that line, put a variable in front of it???

See above post with all the code to see why I don't have the handle...


>> Or is this a compiled .APP that you don't have source for???

No, only a .prg, and yes, the source is above...

Thanks,
Stanley
 
Hi Mike,

>> So you could look for "User-opened files", then for the name of your file; then for the next occurrence
>> of "Handle=", then for the subsequent integer. But it wouldn't be foolproof, and, as Vilhelm-Ion
>> points out, it wouldn't work with long filenames or paths as these would be abbreviated.

The solution must be able to deal with today's file systems which are loaded with long names and embedded spaces. So, looks like we something else.

How could we convert gcFileName (a long and space embedded filename) into the abbreviated version and look for that? How much overhead would that induce?

Whats next,
Stanley
 
More info...

The same FileEncrypt procedure works without issue if I remove the "dealing with readonly files". Here is a modified version that works...

Code:
Procedure FileEncrypt
	lnSuccess = goCrypt.CkEncryptFile(Alltrim(gcFileName), 'a.tmp')

	If (lnSuccess <> 1)
		=Messagebox(goCrypt.LastErrorText, 0, 'En-Cryption Error', 30000)
		llEncFailed = .T.
	Else
		lcOutFile = Strtofile(Filetostr('a.tmp') + lcKey1, (gcFileName), 0)
		Delete File 'a.tmp'
	Endif
Endproc

Thanks,
Stanley
 
Is it that the server or system's buffering or caching needs time to refresh or clear? That's what I'm thinking is the issue since it doesn't happen as you (much slower) step through the debugger. I've worked with busy file servers and web servers and sometimes we'd get errors that had no good explanation. So we set the problem code within TRY|CATCH|ENDTRY framework and if it failed then in the CATCH section we'd pause/wait a brief time and then retry the code again. It usually worked the second time around.

There's also several caching, buffering, optimizing, opportunistic settings that could be tweaked if it is still an issue. Many threads here on those issues.
 
Hi dbMark,

>> Is it that the server or system's buffering or caching needs time to refresh or clear?
Yes, that is how it appears to me, as anything that slows it down makes it work... And its not consistant as some work and some don't.

?? TRY|CATCH|ENDTRY
I worked a little until I discovered last week you cannot do a retry within it. I could set up an on error routine that could issue the retry, which is doable. I'd like to use this TRY|CATCH|ENDTRY stuff, but I don't see the point if one cannot do something as simple as a retry. What would your framework look like, and I'll try it.

Thanks,
Stanley
 
Assume that I opened a file with =FOPEN('d:\test.jpg',0)

Nowhere in my code do I issue a fopen() command,

OK, so you ask about FOPEN() and then say you're not using FOPEN().

It's been WAY too long a week to wade into this now. An adult beverage is calling my name.
 
Hi Dan,

>> OK, so you ask about FOPEN() and then say you're not using FOPEN().

Sure, as my results are almost the same. Issuing fopen() allows us to create the condition that we are trying to escaoe/recover from. My situation is worse than isuing fopen() as fopen() will open the file and keep it open, whereas my locked file automatically unlocks if I slow it down.

Surely people don't make suggestions by reading only the 1st line of a question... The line you quoted was the very first line and was setting up the senerio for the question/s asked in the next couple of lines.

Stanley said:
Assume that I opened a file with =FOPEN('d:\test.jpg',0)
Notice that I did not assign a variable to get the file's handle.

What commands could I use to close the file without knowing the handle?

Is there any commands where I can pass the filename in and close it?

I know the file is open by vfp as Win8 reports it as in use by vfp9 when I try to delete it at the OS level, and I'm looking for a way to close it, release it, unlock it from within vfp. If I close and restart vfp, the file can be deleted.

I've googled all over and cannot find any resource that shows how to close it, and many, many ways to test if it is open, but no way to close it if it's open.

Is there way of getting the handle to the open file?

Thanks,
Stanley

 
Waht is really causing the 1102 error? Do you have an error handler showing you the line number LINENO()?

Code:
lcOutFile = Strtofile(Filetostr('a.tmp') + lcKey1, (gcFileName), 0)
Do CloseFile
´

This is what we talk about, isn't it? In the code I copied from a post of you you used z.tmp instead of (gcFileName) and so the problem is the OS level not closing the output file of STRTOFILE() before you do the next processing of it, isn't it? But where does that really happen? Where does the error occur, if you remove DO CloseFile and don't wait with INKEY()? That's still unclear and my assumption still is it's about a constant file name you use, also FileList.full_path might have same names in sequential records. The error does happen somewhere afterwards, maybe in the next loop iteration.

By the way, lookup the help topic on STRTOFILE, it's return value is the number of bytes written, lcOutfile = SRTRTOFILE() makes no sense at all from the variable naming, lcOutfile will not contain the output file, that's written to a file named as the second parameter value. There also is no need to use name expression here, you're specifying the file name as parameter, no need for neither name expresion nor macrosubstitution in functions, nae expressions are only needed in commands. Commands being VFP language listed in the Reference->Language Reference->Commands section of the help, of course only in file related commands. Within the parameterization of a function call the seperate parameters are delimited and separated by the commas, the brackets you use here work as simple expression brackets as in math expressions like (1+2)*3.

You obviously can't put a breakpoint at the place and single step until you arrive at the error, because that introduces a wait state which makes the file close and no problem occurs. But what about errorhandling. ON ERROR will help you here to locate the problemativ file usage.

What you should do is avoiding to use any file name twice, you still make any processing go through a.tmp, why? That's your single point of failure, don't do that.

Bye, Olaf.
 
Hi Olaf,

>> What is really causing the 1102 error? Do you have an error handler showing you the line number LINENO()?

The error is occuring when writing gcFileName. Here is the latest version that does not blindly close files like I wanted. It involves a custom handler that issues a retry when that error is raised.

Code:
Procedure FileEncrypt
	lnSuccess = goCrypt.CkEncryptFile(Alltrim(gcFileName), 'enc.tmp')

	If (lnSuccess <> 1)
		=Messagebox(goCrypt.LastErrorText, 0, 'En-Cryption Error', 30000)
		llEncFailed = .T.
	Else
		llRetry = .T.
		lnTryCount = 0
		llReadOnly = .F.
		llResetReadOnly = .F.

		Do While llRetry = .T. .And. lnTryCount < 10
			llRetry = .F.
			lnTryCount = lnTryCount + 1

			=Adir(laFN, gcFileName)
			llReadOnly = 'R' $ laFN(1,5)

			If llReadOnly = .T.
				Store "Attrib -r " + '"' + (gcFileName) + '"' To cCommand
				Run /N2 &cCommand
				llRetry = .T.
				llResetReadOnly = .T.
			Else
				Delete File (gcFileName)
				Exit
			Endif
		Enddo

		lcOutFile = Strtofile(Filetostr('enc.tmp') + lcKey1, 'z.tmp', 0)
		*lcOutFile = Strtofile(Filetostr('enc.tmp') + lcKey1, (gcFileName), 0)
		*lcOutFile = Strtofile(Filetostr('a.tmp') + lcKey1, (gcFileName), 0)

		Store 'Copy File "z.tmp" To ' + '"' + (gcFileName) + '"' To zCommand
		&zCommand

		If llResetReadOnly = .T.
			Store "Attrib +r " + '"' + (gcFileName) + '"' To cCommand
			Run /N2 &cCommand
		Endif

		Delete File '*.tmp'
	Endif
Endproc

Here is the error handler...
Code:
Procedure ErrorAction
	Parameter llShowDetail, gcFileName, merror, Mess, mess1, mprog, mlineno
	If llShowDetail
		? gcFileName
		? 'Number: ' + Ltrim(Str(merror))
		? 'Message: ' + Mess
		? 'Line of code with error: ' + mess1
		? 'Line number of error: ' + Ltrim(Str(mlineno))
	Endif

	Inkey(.05)
	Retry
Endproc

And here is the the on error call...

Code:
	llShowDetail = .F.

	Do While !Eof('FileList')
		Y=Y+1
		r=r+1

		On Error Do ErrorAction With llShowDetail, gcFileName, ;
			Error(), Message(), Message(1), Program(), Lineno()

>> also FileList.full_path might have same names in sequential records.
No, all full+path values are unique...


>> lcOutfile = SRTRTOFILE() makes no sense at all from the variable naming, lcOutfile will not contain the output file
True, it only contains as you said, the number of bytes written... In tracking this down I did test lcOutfile for a value greater than 0, just to make sure that SRTRTOFILE() created a non zero length file. All files created had a >0 file size, so that wasn't the problem.


>> nae expressions... no need for neither name expresion nor macrosubstitution in functions, nae expressions are only needed in commands.
I know that your nae above is a typo, not sure what you meant?


>> so the problem is the OS level not closing the output file of STRTOFILE() before you do the next processing of it, isn't it?
Yes, a previous file operation on gcFileName doesn'g get finished in time, before the next file operation...

Just thinking out loud now, would a begin/end transaction force filesystem I/O? Or is it just a table and record framework?


Code:
lcOutFile = Strtofile(Filetostr('a.tmp') + lcKey1, (gcFileName), 0)
Do CloseFile

I've tried multiple iteration using multiple file names and all with the same error, as the problem occurs when writing it back to the original filename as defined in gcFileName which get populated by full_path. I don't have a choice here if I want it to go back to the original location and use its original filename.

I even tried your version, that failed... It failed, not because of the .tmp files, but the writing of the final file.
Code:
lcSourceFile = "org"+SYS(2015)+".tmp"

lcSourceFile = ''
lcOutFile = Strtofile(Filetostr('a.tmp') + lcKey1, (lcSourceFile), 0)
Store 'Copy File ' + (lcSourceFile) + ' To ' + '"' + (gcFileName) + '"' To zCommand
&zCommand

also tried...
copy file (lcSourceFile) to (gcFileName)
and
copy file lcSourceFile to gcFileName


This is what I'm using now...
lcOutFile = Strtofile(Filetostr('a.tmp') + lcKey1, 'z.tmp', 0)
Store 'Copy File "z.tmp" To ' + '"' + (gcFileName) + '"' To zCommand
&zCommand


>> avoiding to use any file name twice, you still make any processing go through a.tmp, why?
Let me explain... The process of encrypting a file goes as follows,
1. I copy FileList.full_path to gcFileName,
2. Once the file is internally checked to determine that it is not already encrypted, I pass gcFileName to the encryption function as input and after encrypting the file the function outputs the file the 'enc.tmp'
3. Now the strtofile(filetostr... takes 'enc.tmp' file as input and appends the value of lcKey1, then outputs a new file 'z.tmp' that contains the "original file as encrypted" with the "appended key as NOT encrypted". It is this key that step 2 uses to determine if the file is encrypted.
4. Delete gcFileName,
5. Finally copy 'z.tmp' to its original location and name as contained in gcFileName,
6. Do it all again...

Given the steps involved, I don't how to make it work without using z.tmp multiple times. Best I can see, it doesn't matter as that is not where the problem is.

Thanks,
Stanley
 
OK, here is a screenshot of output showing each time the error routine is fired along with the filename and other data. Notice that several items appear more than once, which represents how many times the error was triggered. If the file name is shown 2 times, then the error was triggered 2 times and it successfully processed on the 3rd try.

Also note that there are no missing filename as they are all contiguous, therefore if a number is not shown, that means that it processed the 1st time without any error. Notice 435-1.tif thru 437-1.tif are missing as they did not error, as did 448-1.tif thru 458-1.tif.

[link] [/url]


Olaf, you will ask to see line 220... that screenshot is included...

[link] [/url]

Stanley

 
Ok, another interested screenshot showing performance data encrypting and decrypting files to both a local harddrive and a local ssd. The entire folder 'd:\Leslie' (ssd) was copied to the 'e:\leslie' folder on a harddrive (folder structure is identical.

1. On iteration #1, I encrypt all the files on a hard drive,
2. Iteration #2, I decrypt those harddrive files,
3. Next I run this code that substitutes the drive letters on all the records so the focus will be on the solid state drive.

Code:
	Do While !Eof()
		Replace full_path With Strtran(Upper(full_path), 'D:\', 'E:\')
		Skip 1
	Enddo

4. Iteration #3 encrypts all the files on a SSD drive,
5. Iteration #4 decrypts those files.

I record the time required for each and every action for each record.

Stanley
 
 http://files.engineering.com/getfile.aspx?folder=8525b0a3-2c6f-4430-a827-64d420907cdc&file=Picture0001.png
I think COPY FILE fails on reading z.tmp rather than on writing (gcFilename), since you delete that earlier.
Or the reasoning is even simple, you don't have write permission on the original file. Readonly attribute is just one way to write protect a file, and a very weak protection. You may simply not have permissions.
You don't need to care about the readonly attribute, as it only is about updating/changing the file, not about overwriting it!

If that's not the case. As I recommended don't reuse any file name all file processing has to go through. Why must every processing go through z.tmp? You can compute a file name with SYS(2015) for every single step and finally overwrite original with encrypted file. That's a risky step anyway, as you don't keep the original. Most probably it's meant that way, but in case you want to store files encrypted rather make use of OS features as encrypted folders. It surely has a downside on being readably transparently via login to the account it's bound to.

So let me assume you want to encrypt some complete folder by processing each file and finally overwriting the original files with the encrypted version.
Then go this route:
1. lcEncrypted = ADDBS(GETENV("TEMP"))+"enc"+SYS(2015)+".tmp"
lnSuccess = goCrypt.CkEncryptFile(Alltrim(gcFileName), lcEncrypted)
2. In case of success COPY FILE (lcEncrypted) To (gcFileName) && overwrite original with encryption
3. DELETE FILE (lcEncrypted)

You don't need to delete the original, COPY FILE can overwrite it, unless you don't have write permissions. You indeed need to remove readonly atribute to overwrite the original with COPY FILE, also STRTOFILE() will return 0 (having written 0 bytes), even with overwrite=.t. or flag=0. But you may try WinAPI CopyFile with the option to overwrite, it should be possible in some way, if not you can get along with ATTRIB as usual, its just a possible shortcut to be able to do without it.

As you want to erase the encrypted temp file, you could also make use of MoveFile or MoveFileEx to do steps 2 and 3 in one step, you might check if MoveFile can overwrite, I'm quite sure it has such an option and can be set to different behaviours, eg failing with error, failing without error (skipping) or overwriting.

What could be problematic is goCrypt doesn't close the encrypted file before you try to copy it. For example as a comparison: If you automate Shell.Application CopyHere method, that also runs in parallel and VFP continues before the CopyHere operation finishes. So you may act on the still open encrypted file and that may be your only problem, really. So you may need to wait for goCrypt. It seems to be a third party component, maybe activeX, so such things can run parallel. Besides, even though VFP itself is strictly sequentially working, the OS filesystem and caching can cause all kinds of delays.

You may add in to recreate the readonly attribute of (gcFilename), but if you want original attributes you'd need to copy all of them. And it wonÄt recreate the file permissions you are the new owner of the new file as you created it. You don't act on the original file, you overwrite it, just restoring readonly is not at all restoring all the file permissions you had beforehand.

Anyway: The main point is, using SYS(2015), not enc.tmp, I just added the "enc" prefix to make visible what type of file the random file name is, the important part is not using enc instead of z, but using SYS(2015), using a different file name for all outputs you don't have to care about OS or file system delays in closing file handles.

Bye, Olaf.

PS: About your performance log: Seconds has a certain granularity of ~0.015 seconds, so you won't get good measures on short durations, you either get 0 or 0.015 for single operations taking shorter time.

Try this:
Code:
Create Cursor curDiffs (nTimediff B)
now = Seconds()
For n = 1 to 100000
Insert into curDiffs Values (Seconds()-now)
now = Seconds()
Endfor
Select nTimediff, Count(*) from curDiffs group by 1

It'll mainly give you 0.00, a few times the time will advance 0.015 or 0.016 seconds, with the default 2 decimals display you'll see 0.02 rounded. SET DECIMALS 3 (or more) and SET FIXED ON, then you'll see that.
 
Hi Olaf,

>> You don't need to care about the readonly attribute, as it only is about updating/changing the file, not about overwriting it!

I deal with the readonly because if I'm encrypting or decrypting it, I have to be able to modify it (by applying encryption) and writing it back to disk. Then, if it was originally readonly, I set the attribute back to readonly. On my first roll-out of this I was not dealing with the readonly, and half way thru the batch I started getting exceptions related to the readonly attribute, so I added code to deal with it, and that started all these other issues.


>> in case you want to store files encrypted rather make use of OS features as encrypted folders. It surely has a downside on being readably transparently via login to the account it's bound to.

That style of encryption doesn't protect us in any way. In the past, a few customers did not pay and allowed our competition access to all their images on their system so they could be migrated to the competitors system. We had no way to enforce payment of services. Now with this new encryption thingy in place, they get the keys when their account is in good standing. And, most importantly, I don't have to worry about the images falling into the wrong hands, (competition).


>> 1. lcEncrypted = ADDBS(GETENV("TEMP"))+"enc"+SYS(2015)+".tmp"
>> lnSuccess = goCrypt.CkEncryptFile(Alltrim(gcFileName), lcEncrypted)
>> 2. In case of success COPY FILE (lcEncrypted) To (gcFileName) && overwrite original with encryption
>> 3. DELETE FILE (lcEncrypted)

I will try this...


>> WinAPI CopyFile with the option to overwrite, it should be possible in some way, if not you can get
>> along with ATTRIB as usual, its just a possible shortcut to be able to do without it.

The code I've written is able to set the attributes without issue. Never a problem with that part. I would like to get rid of the run command, as it tends blink the screen randomly and while doing so, it messes with most open application windows including windows explorer. It's not real bad, but noticable at times. Just yesterday, i was able to lots of other work while the script was encrypting and decrypting, but at times I was having some difficulty with explorer. I'm using Win8.0x64.


>> What could be problematic is goCrypt doesn't close the encrypted file before you try to copy it.

I don't believe this is the issue, because if I run my original code (that doesn't have the attribute changing stuff) then it runs the entire record set only erroring on the readonly files. If goCrypt was holding it open, then I should have been getting them errors early on.


>> You may add in to recreate the readonly attribute of (gcFilename), but if you want original
>> attributes you'd need to copy all of them. And it wonÄt recreate the file permissions you
>> are the new owner of the new file as you created it. You don't act on the original file,
>> you overwrite it, just restoring readonly is not at all restoring all the file permissions
>> you had beforehand.

Very good point, I'll refactor my code with this in mind...


I'll also refactor the performance gathering data using your suggestions,

Thanks Olaf, this has been most helpfull,
Stanley

 
>this has been most helpful
Glad to be of help.

As you see I edited the part saying you can overwrite readonly files, indeed you can't simply. But it's not the only file attribute and if you'd want to get the new file with same attributes, permissions, owner and other related meta data, you'd better not overwrite it with COPY FILE, but open it with FOPEN, then overwrite it. This way you recycle the filesystem table of content entry with file attributes and other file info.

Code:
*Keep a file and write it new:
lnFH=FOPEN(gcFilename,12)
FCHSIZE(lnFH,0) && truncate the file to write it new keeping name and other meta info
FWRITE(lnFH,FILETOSTR(lcEncrypted))
FCLOSE(lnFH)

Indeed you still have to turn the readonly attribute off temporarily, before doing this way of overwriting. There surely also is a API call to set file attributes without getting the shell window. But you can do any RUN with Shellexecute, especially attrib, as it's really a separate Attrib.EXE. You may also automate wscript.shell run so you can set the window visibility.

Bye, Olaf.
 

OK, Thanks so much Olaf,

Now it time to implement these new changes...

Thanks again,
Stanley

 
>I don't have to worry about the images falling into the wrong hands, (competition).

Well, the moment you decrypt the file they are there. Even after you later encrypt them again, you never overwrite the same bytes on HDD, even less so on SSD due to wear levelling, but also normal magnetic platter based file systems write to unused disc blocks, the reason for fragmentation. You can't enforce usage of certain sectors and blocks, neither with COPY FILE nor FWRITE.

Indeed a bitlocker or other protection that is "transparent" seemingly unencrypted while a user is logged in with the account having access rights, really is transparent up to the point, that any copy, also manual copies out of the OS encrypted folder or partition onto USB thumbdrive is decrypting automatically, so that would only work with confidence to the customer. If you can trust a customer his locking of the computer is only protecting from hardware theft, any spyware/virus working in his account also has decrypted access. The advantage though, any user stealing the hdd will only find strong encrypted data written there.

It's a topic on its own, as you're always capable to get some portion of currently shown decrypted data or images piece by piece anyway.

Bye, Olaf.
 
>> the moment you decrypt the file they are there.

Yes and No... When viewing the files, the currently viewed file get decryped to a LOCAL file currentfile.tmp. Switch to a different record and view its image, the decryption overwrites currentfile.tmp, so there will only be one LOCAL decrypted file as its name never changes, just its contents.
Also after the initial encryption process, immediately afterwards, we use a sdelete to overwrite all unused space.

>> If you can trust a customer his locking of the computer is only protecting from hardware theft,
>> any spyware/virus working in his account also has decrypted access.

In my opinion, the only thing that OS generated encryption is good for is protecting the data from is unauthorized access, all based on the current logged in account and theft.

I have had customers allow other computer repair shops take the system to their repair facilities complete with user names and passwords. We could be held liable should any of that data becomes breached as they contain sensitive data like "hipaa" and "cobra".

I've also had customers allow our competition to take their systems to their place of business to extract all the data and migrate it into their offerings. This is what I'm protecting against. If their account is in good standing and they want to switch vendors, we would like a chance to fix any issues if any exist, and if they want to switch anyway, we give them the keys. Also each site will be using an entirely different set of keys, so they cannot be shared across installations. Also note the variable lcKey1 only contains the seed used in the calculation of the actual key so anyone looking at the file and discovers it at the file's end still doesn't have enough to decrypt. And I'm using AES256...

Stanley

 
As soon as you write to hdd a file is there, even if the file system doesn't allow direct access, you can get at deleted files, as long as their blocks are not reused, even just with software. So your best bet would be unencrypting in memory only and using Image.PictureVal for images. Anyway, even then people can scrape off the images with screen shots and cause a memory dump to hdd.

If you need to defend against the case of giving out computers for repairs or retiring hardware you should not at all write to the local drive, even if you wipe files with SDELETE, which really overwrites the original clusters, your routine doesn't show that, so you don't sdelete the original files. So when do you SDELETE and what? You can't point SDELETE to files you deleted normally beforehand, eg via FoxPro DELETE FILE. In regard of retirement it's of course much easier to make data unrecoverable for a whole drive than single files.

I know without googling, that HIPAA is about health care data and COBRA possibly, too. Law regulations will be totally incompetent technically, the same goes for data protection in general, laws are formulated in a very general way and the measurements you should do are outlined in a very broad way.

Bye, Olaf.
 
Status
Not open for further replies.

Part and Inventory Search

Sponsor

Back
Top