wiki:PaintShopProFailsOnWine

PaintShopPro 8 Fails on Wine

Running Jasc PaintShopPro (PSP) version 8 or 8.1 (and possibly others) on Linux with versions of Wine up to and including 1.1.9 leads to a long-standing problem where PSP fails to perform many commands since an error occurs when it tries to create a temporary file (used by the undo system). The error is:

Unhandled exception: code = 200, what = unable to create the file.

Cause

The reason appears to be that PSP fails to create the parent directory of the temporary file. Unfortunately it isn't clear when PSP should create that directory (at start-up, when user file-locations preferences are changed, at file-creation time, etc.), or why this fails when running with Wine.

Specifically, on Windows XP PSP by default sets the user Temp Files/Undo? directory (HKEY_CURRENT_USER\Software\Jasc\Paint Shop Pro 8\FileLocations\TempFiles\0\Dir) to "C:\windows\temp\Temp Files". When PSP performs operations with results written to temporary files it tries to write those files into a sub-directory of the "Temp Files" directory.

The name of the sub-directory is "JSC%X_%s" where %X is a hex number that sometimes appears to be semi-static across multiple PCs and users (e.g. Seen values 10024, 1002E) but other times will appear to be random (changes on each program invocation) and %s is the trimmed user-name (e.g. tj).

So, PSP will try to create a temporary file such as "C:\windows\temp\Temp Files\JSC10024_tj\JSC87aa.tmp", which would map to "/home/tj/.wine/dosdevices/c:/windows/temp/Temp\ Files/JSC10024_tj/JSC87aa.tmp".

This fails because the sub-directory "JSC10024_tj" doesn't exist, and calls to the Windows function CreateFile() will not create directories in the path to the file if those directories don't exist.

Manual Solution

If the sub-directory is created manually (whilst PSP is running) and permissions are set to rwxrwxr-x (775) PSP creates the temporary file successfully and performs the desired function.

To perform this manual creation step wine needs to be run with file debugging enabled and capturing output to a log-file which can be searched for the temporary sub-directory name, e.g.

"WINEDEBUG=+file wine "Paint Shop Pro.exe" >winedbg.log 2>&1"

and then, when PSP reports it can't open the file when trying to perform an operation, do "grep JSC winedbg.log" which will show, in part, something similar to:

0031:warn:file:wine_nt_to_unix_file_name L"JSCA02DA_tj\\JSCed5b.tmp" not found in /home/tj/.wine/dosdevices/c:/windows/temp/Temp Files
0031:warn:file:CreateFileW Unable to create file L"C:\\windows\\temp\\Temp Files\\JSCA02DA_tj\\JSCed5b.tmp" (status c000003a)

Now, manually we can do:

mkdir -p ~/.wine/dosdevices/c:/windows/temp/Temp\ Files/JSCA02DA_tj

Automatic Solution

It would make more sense if we could catch the failure automatically and create the directory on-the-fly. By piping the wine debug output to a (m)awk script it is possible. It is executed like this:

WINEDEBUG=+file WINEPREFIX="/home/$USER/.wine" \
wine "C:\\Program Files\\Jasc Software Inc\\Paint Shop Pro 8\\Paint Shop Pro.exe"  2>&1 | \
awk -f ~/My\ PSP8\ Files/mkdir.mawk

A slightly modified version of this command-line (which wraps the execution in a sub-shell) can be set as a program shortcut in the GUI menus rather than manually executing it from a terminal or shell script:

sh -c 'env WINEDEBUG="+file" WINEPREFIX="/home/$USER/.wine" \
wine "C:\\Program Files\\Jasc Software Inc\\Paint Shop Pro 8\\Paint Shop Pro.exe" 2>&1 | \
awk -f ~/My\ PSP8\ Files/mkdir.mawk'

The fully qualified paths are used to ensure the solution will work from any working directory. Place the script "mkdir.mawk" in the PSP user directory ~/My PSP8 Files/. The script is attached to this article:

$0 ~ /JSC/ {
 if ($0 ~ /not found in/ && $0 ~ /wine_nt_to_unix_file_name/) {
  temp=substr($0,2+index($0,"L\""));
  temp_len=index(temp, ".tmp")+3;
  temp=substr(temp,1,temp_len);
  temp_parts_qty=split(temp,temp_parts,"\\");
  dirname=substr($0,13+index($0, "not found in ")) "/" temp_parts[1];
  print "Creating temporary directory:", dirname;

  system("mkdir \"" dirname "\"")
 }
}

In Use

With this work-around in place the script will create the directory successfully, usually without PSP reporting an error. However there are still a few occasions when PSP will report the error because the script isn't quick enough to create the directory. In these cases the PSP operation will have failed, so simply redoing the operation will be sufficient. Here's the script output when that occurs. The second and third lines indicate the directory was created the first time around, even though PSP reported a "file open" error:

Creating temporary directory: /home/tj/.wine/dosdevices/c:/windows/temp/Temp Files/JSC30034_tj
Creating temporary directory: /home/tj/.wine/dosdevices/c:/windows/temp/Temp Files/JSC30034_tj
mkdir: cannot create directory `/home/tj/.wine/dosdevices/c:/windows/temp/Temp Files/JSC30034_tj': File exists

Hacking

The functionality to *set* the temporary directory is in the PSP class::function CJCmdHistory::SetInitialTmpDir() (see below for extract) but, strangely, it isn't clear where or even *if* that method calls CreateDir/CreateDirEx().

However, if a sub-directory with a name matching JSC*.* exists in "Temp Dir" this method will cause the sub-directory to be removed.

File: JascCmdProc.dll
Offset: 0x10012410 (Ordinal 197) CJCmdHistory::SetInitialTmpDir(ICommandExecutionContext *)

loc_10012554:
mov     ecx, [ebp+var_14] ;contains trimmed and validated current user-name - the %s value
mov     eax, [eax+20h] ; at this point EAX seems to always be zero, so this reads offset 0x20...
                       ; but from where - data segment or 0x00000000? This is the %X value.
add     esi, 0F4h
mov     edx, [ecx]
push    edx
push    eax
push    offset aJscX_S  ; "\\JSC%X_%s"
push    esi
call    ?Format@CString@@QAAXPBDZZ ; CString::Format(char const *,...)
add     esp, 10h
push    ebx
push    0Bh
push    edi
call    ?TmpDirEventCB@CJCmdHistory@@SGXPAVICommandExecutionContext@@HPAX@Z ; CJCmdHistory::TmpDirEventCB(ICommandExecutionContext *,int,void *)

These extracts from the wine debug logs give some idea of the unusual nature of this issue:

Success

0009:trace:file:GetTempFileNameW returning L"C:\\windows\\temp\\BCG7837.tmp"
0009:trace:file:CreateFileW L"C:\\windows\\temp\\TempFiles\\JSC1002E_tj\\JSC87aa.tmp" GENERIC_WRITE  creation 1 attributes 0x80
0009:trace:file:RtlDosPathNameToNtPathName_U (L"C:\\windows\\temp\\TempFiles\\JSC1002E_tj\\JSC87aa.tmp",0x32da68,(nil),(nil))
0009:trace:file:RtlGetFullPathName_U (L"C:\\windows\\temp\\TempFiles\\JSC1002E_tj\\JSC87aa.tmp" 520 0x32d7ac (nil))
0009:warn:file:wine_nt_to_unix_file_name L"\\??\\C:\\windows\\temp\\TempFiles\\JSC1002E_tj\\JSC87aa.tmp" -> "/home/tj/.wine/dosdevices/c:/windows/temp/TempFiles/JSC1002E_tj/JSC87aa.tmp" required a case-insensitive search
0009:trace:file:wine_nt_to_unix_file_name L"\\??\\C:\\windows\\temp\\TempFiles\\JSC1002E_tj\\JSC87aa.tmp" -> "/home/tj/.wine/dosdevices/c:/windows/temp/TempFiles/JSC1002E_tj/JSC87aa.tmp"
0009:trace:file:GetTempFileNameW created L"C:\\windows\\temp\\TempFiles\\JSC1002E_tj\\JSC87aa.tmp"
0009:trace:file:GetTempFileNameW returning L"C:\\windows\\temp\\TempFiles\\JSC1002E_tj\\JSC87aa.tmp"
0009:trace:file:CreateFileW L"C:\\windows\\temp\\TempFiles\\JSC1002E_tj\\JSC87aa.tmp" GENERIC_WRITE FILE_SHARE_READ FILE_SHARE_WRITE  creation 2 attributes 0x80
0009:trace:file:RtlDosPathNameToNtPathName_U (L"C:\\windows\\temp\\TempFiles\\JSC1002E_tj\\JSC87aa.tmp",0x31db74,(nil),(nil))
0009:trace:file:RtlGetFullPathName_U (L"C:\\windows\\temp\\TempFiles\\JSC1002E_tj\\JSC87aa.tmp" 520 0x31d8b8 (nil))
0009:trace:file:wine_nt_to_unix_file_name L"\\??\\C:\\windows\\temp\\TempFiles\\JSC1002E_tj\\JSC87aa.tmp" -> "/home/tj/.wine/dosdevices/c:/windows/temp/TempFiles/JSC1002E_tj/JSC87aa.tmp"

Failure

0009:trace:file:GetTempFileNameW returning L"C:\\windows\\temp\\BCG3ca0.tmp"
0009:trace:file:CreateFileW L"C:\\windows\\temp\\TempFiles\\JSC1002E_tj\\JSC501b.tmp" GENERIC_WRITE  creation 1 attributes 0x80
0009:trace:file:RtlDosPathNameToNtPathName_U (L"C:\\windows\\temp\\TempFiles\\JSC1002E_tj\\JSC501b.tmp",0x32da68,(nil),(nil))
0009:trace:file:RtlGetFullPathName_U (L"C:\\windows\\temp\\TempFiles\\JSC1002E_tj\\JSC501b.tmp" 520 0x32d7ac (nil))
0009:warn:file:wine_nt_to_unix_file_name L"JSC1002E_tj\\JSC501b.tmp" not found in /home/tj/.wine/dosdevices/c:/windows/temp/TempFiles
0009:warn:file:CreateFileW Unable to create file L"C:\\windows\\temp\\TempFiles\\JSC1002E_tj\\JSC501b.tmp" (status c000003a)
0009:trace:file:GetTempFileNameW returning L"C:\\windows\\temp\\TempFiles\\JSC1002E_tj\\JSC501b.tmp"
0009:trace:file:CreateFileW L"C:\\windows\\temp\\TempFiles\\JSC1002E_tj\\JSC501b.tmp" GENERIC_WRITE FILE_SHARE_READ FILE_SHARE_WRITE  creation 2 attributes 0x80
0009:trace:file:RtlDosPathNameToNtPathName_U (L"C:\\windows\\temp\\TempFiles\\JSC1002E_tj\\JSC501b.tmp",0x31db74,(nil),(nil))
0009:trace:file:RtlGetFullPathName_U (L"C:\\windows\\temp\\TempFiles\\JSC1002E_tj\\JSC501b.tmp" 520 0x31d8b8 (nil))
0009:warn:file:wine_nt_to_unix_file_name L"JSC1002E_tj\\JSC501b.tmp" not found in /home/tj/.wine/dosdevices/c:/windows/temp/TempFiles
0009:warn:file:CreateFileW Unable to create file L"C:\\windows\\temp\\TempFiles\\JSC1002E_tj\\JSC501b.tmp" (status c000003a)

Clues

0009:trace:file:find_drive_rootA "/home/tj/.wine/drive_c/windows/temp/Temp Files/" -> drive C:, root="/home/tj/.wine/drive_c", name="/windows/temp/Temp Files/"
0009:trace:file:GetFileAttributesW L"C:\\windows\\temp\\Temp Files"
0009:trace:file:RtlDosPathNameToNtPathName_U (L"C:\\windows\\temp\\Temp Files",0x32cb88,(nil),(nil))
0009:trace:file:RtlGetFullPathName_U (L"C:\\windows\\temp\\Temp Files" 520 0x32c8fc (nil))
0009:trace:file:wine_nt_to_unix_file_name L"\\??\\C:\\windows\\temp\\Temp Files" -> "/home/tj/.wine/dosdevices/c:/windows/temp/Temp Files"
0009:trace:file:RtlGetFullPathName_U (L"C:\\windows\\temp\\Temp Files" 520 0x32d720 (nil))
0009:trace:file:CreateFileW L"\\\\.\\C:" GENERIC_READ FILE_SHARE_READ FILE_SHARE_WRITE  creation 3 attributes 0x0
0009:trace:file:RtlDosPathNameToNtPathName_U (L"\\\\.\\C:",0x32cd1c,(nil),(nil))
0009:trace:file:RtlGetFullPathName_U (L"\\\\.\\C:" 520 0x32ca60 (nil))
0009:trace:file:CreateFileW returning 0x248
0009:trace:file:FindFirstFileExW L"C:\\windows\\temp\\Temp Files" 0 0x32d6b0 0 (nil) 0
0009:trace:file:RtlDosPathNameToNtPathName_U (L"C:\\windows\\temp\\Temp Files",0x32d668,0x32d670,(nil))
0009:trace:file:RtlGetFullPathName_U (L"C:\\windows\\temp\\Temp Files" 520 0x32d3a8 0x32d670)
0009:trace:file:wine_nt_to_unix_file_name L"\\??\\C:\\windows\\temp\\" -> "/home/tj/.wine/dosdevices/c:/windows/temp/"
0009:trace:file:NtQueryDirectoryFile (0x248 (nil) (nil) (nil) 0x32d658 0x4361e58 0x00002000 0x00000003 0x00000000 L"Temp Files" 0x00000001
0009:trace:file:read_directory_stat trying optimisation for file L"Temp Files"
0009:trace:file:append_entry long L"Temp Files" short L"TEMP~RIN" mask <null>
0009:trace:file:read_directory_stat returning 0
0009:trace:file:NtQueryDirectoryFile => 0 (116)
0009:trace:file:FindNextFileW 0x4361e18 0x32d6b0
0009:trace:file:FindNextFileW returning L"Temp Files" (L"TEMP~RIN")
0009:trace:file:FindFirstFileExW L"C:\\windows\\temp\\Temp Files" 0 0x32d820 0 (nil) 0
0009:trace:file:RtlDosPathNameToNtPathName_U (L"C:\\windows\\temp\\Temp Files",0x32d7d8,0x32d7e0,(nil))
0009:trace:file:RtlGetFullPathName_U (L"C:\\windows\\temp\\Temp Files" 520 0x32d518 0x32d7e0)
0009:trace:file:wine_nt_to_unix_file_name L"\\??\\C:\\windows\\temp\\" -> "/home/tj/.wine/dosdevices/c:/windows/temp/"
0009:trace:file:NtQueryDirectoryFile (0x248 (nil) (nil) (nil) 0x32d7c8 0x4361e58 0x00002000 0x00000003 0x00000000 L"Temp Files" 0x00000001
0009:trace:file:read_directory_stat trying optimisation for file L"Temp Files"
0009:trace:file:append_entry long L"Temp Files" short L"TEMP~RIN" mask <null>
0009:trace:file:read_directory_stat returning 0
0009:trace:file:NtQueryDirectoryFile => 0 (116)
0009:trace:file:FindNextFileW 0x4361e18 0x32d820

0009:trace:file:FindNextFileW returning L"Temp Files" (L"TEMP~RIN")
0009:trace:file:RtlGetFullPathName_U (L"C:\\windows\\temp\\Temp Files\\WRITE.TST" 520 0x32d70c (nil))
0009:trace:file:CreateFileW L"\\\\.\\C:" GENERIC_READ FILE_SHARE_READ FILE_SHARE_WRITE  creation 3 attributes 0x0
0009:trace:file:RtlDosPathNameToNtPathName_U (L"\\\\.\\C:",0x32cd08,(nil),(nil))
0009:trace:file:RtlGetFullPathName_U (L"\\\\.\\C:" 520 0x32ca4c (nil))
0009:trace:file:CreateFileW returning 0x248
0009:trace:file:FindFirstFileExW L"C:\\windows\\temp\\Temp Files\\WRITE.TST" 0 0x32d69c 0 (nil) 0
0009:trace:file:RtlDosPathNameToNtPathName_U (L"C:\\windows\\temp\\Temp Files\\WRITE.TST",0x32d654,0x32d65c,(nil))
0009:trace:file:RtlGetFullPathName_U (L"C:\\windows\\temp\\Temp Files\\WRITE.TST" 520 0x32d394 0x32d65c)
0009:trace:file:wine_nt_to_unix_file_name L"\\??\\C:\\windows\\temp\\Temp Files\\" -> "/home/tj/.wine/dosdevices/c:/windows/temp/Temp Files/"
0009:trace:file:NtQueryDirectoryFile (0x248 (nil) (nil) (nil) 0x32d644 0x4361e58 0x00002000 0x00000003 0x00000000 L"WRITE.TST" 0x00000001
0009:trace:file:read_directory_stat trying optimisation for file L"WRITE.TST"
0009:trace:file:read_directory_stat returning -1
0009:trace:file:append_entry long L"." short L"" mask L"WRITE.TST"
0009:trace:file:match_filename (L".", L"WRITE.TST")
0009:trace:file:append_entry long L".." short L"" mask L"WRITE.TST"
0009:trace:file:match_filename (L"..", L"WRITE.TST")
0009:trace:file:append_entry long L"JSC10024_tj" short L"JSC1~IHG" mask L"WRITE.TST"
0009:trace:file:match_filename (L"JSC10024_tj", L"WRITE.TST")
0009:trace:file:match_filename (L"JSC1~IHG", L"WRITE.TST")
0009:trace:file:NtQueryDirectoryFile => c000000f (0)
0009:trace:file:CreateFileW L"C:\\windows\\temp\\Temp Files\\WRITE.TST" GENERIC_WRITE  creation 2 attributes 0x80
0009:trace:file:RtlDosPathNameToNtPathName_U (L"C:\\windows\\temp\\Temp Files\\WRITE.TST",0x32da3c,(nil),(nil))
0009:trace:file:RtlGetFullPathName_U (L"C:\\windows\\temp\\Temp Files\\WRITE.TST" 520 0x32d780 (nil))
0009:warn:file:wine_nt_to_unix_file_name L"\\??\\C:\\windows\\temp\\Temp Files\\WRITE.TST" -> "/home/tj/.wine/dosdevices/c:/windows/temp/Temp Files/WRITE.TST" required a case-insensitive search
0009:trace:file:wine_nt_to_unix_file_name L"\\??\\C:\\windows\\temp\\Temp Files\\WRITE.TST" -> "/home/tj/.wine/dosdevices/c:/windows/temp/Temp Files/WRITE.TST"
0009:trace:file:CreateFileW returning 0x248
0009:trace:file:DeleteFileW L"C:\\windows\\temp\\Temp Files\\WRITE.TST"
0009:trace:file:RtlDosPathNameToNtPathName_U (L"C:\\windows\\temp\\Temp Files\\WRITE.TST",0x32dbc8,(nil),(nil))
0009:trace:file:RtlGetFullPathName_U (L"C:\\windows\\temp\\Temp Files\\WRITE.TST" 520 0x32d95c (nil))
0009:trace:file:wine_nt_to_unix_file_name L"\\??\\C:\\windows\\temp\\Temp Files\\WRITE.TST" -> "/home/tj/.wine/dosdevices/c:/windows/temp/Temp Files/WRITE.TST"

Source-Code Hacking

What is most interesting in the logs is the few references to JSC10024_tj since it isn't clear from the wine source-code how that string got into the  NtQueryDirectoryFile() function. The call-stack is:

 NtQueryDirectoryFile()

 read_directory_stat()
 read_directory_getdents()

 getdents64() (see also  getdents64)
 append_entry()

 match_filename()

References

 Paint Shop Pro 8 Tip
 Wine AppsDB entry

Attachments