23
23
24
24
package jdk .jpackage .test ;
25
25
26
- import java .awt .image .BufferedImage ;
27
26
import java .io .IOException ;
28
- import java .lang .reflect .InvocationTargetException ;
29
- import java .lang .reflect .Method ;
30
27
import java .nio .file .Files ;
31
28
import java .nio .file .Path ;
32
- import java .util .Optional ;
33
- import javax .imageio .ImageIO ;
34
- import static jdk .jpackage .internal .util .function .ExceptionBox .rethrowUnchecked ;
35
29
36
30
public final class LauncherIconVerifier {
37
31
public LauncherIconVerifier () {
@@ -68,7 +62,7 @@ public void applyTo(JPackageCommand cmd) throws IOException {
68
62
69
63
if (TKit .isWindows ()) {
70
64
TKit .assertPathExists (iconPath , false );
71
- WinIconVerifier . instance .verifyLauncherIcon (cmd , launcherName ,
65
+ WinExecutableIconVerifier .verifyLauncherIcon (cmd , launcherName ,
72
66
expectedIcon , expectedDefault );
73
67
} else if (expectedDefault ) {
74
68
TKit .assertPathExists (iconPath , true );
@@ -83,193 +77,6 @@ public void applyTo(JPackageCommand cmd) throws IOException {
83
77
}
84
78
}
85
79
86
- private static class WinIconVerifier {
87
-
88
- void verifyLauncherIcon (JPackageCommand cmd , String launcherName ,
89
- Path expectedIcon , boolean expectedDefault ) {
90
- TKit .withTempDirectory ("icons" , tmpDir -> {
91
- Path launcher = cmd .appLauncherPath (launcherName );
92
- Path iconWorkDir = tmpDir .resolve (launcher .getFileName ());
93
- Path iconContainer = iconWorkDir .resolve ("container.exe" );
94
- Files .createDirectories (iconContainer .getParent ());
95
- Files .copy (getDefaultAppLauncher (expectedIcon == null
96
- && !expectedDefault ), iconContainer );
97
- if (expectedIcon != null ) {
98
- Executor .tryRunMultipleTimes (() -> {
99
- setIcon (expectedIcon , iconContainer );
100
- }, 3 , 5 );
101
- }
102
-
103
- Path extractedExpectedIcon = extractIconFromExecutable (
104
- iconWorkDir , iconContainer , "expected" );
105
- Path extractedActualIcon = extractIconFromExecutable (iconWorkDir ,
106
- launcher , "actual" );
107
-
108
- TKit .trace (String .format (
109
- "Check icon file [%s] of %s launcher is a copy of source icon file [%s]" ,
110
- extractedActualIcon ,
111
- Optional .ofNullable (launcherName ).orElse ("main" ),
112
- extractedExpectedIcon ));
113
-
114
- if (Files .mismatch (extractedExpectedIcon , extractedActualIcon )
115
- != -1 ) {
116
- // On Windows11 .NET API extracting icons from executables
117
- // produce slightly different output for the same icon.
118
- // To workaround it, compare pixels of images and if the
119
- // number of off pixels is below a threshold, assume
120
- // equality.
121
- BufferedImage expectedImg = ImageIO .read (
122
- extractedExpectedIcon .toFile ());
123
- BufferedImage actualImg = ImageIO .read (
124
- extractedActualIcon .toFile ());
125
-
126
- int w = expectedImg .getWidth ();
127
- int h = expectedImg .getHeight ();
128
-
129
- TKit .assertEquals (w , actualImg .getWidth (),
130
- "Check expected and actual icons have the same width" );
131
- TKit .assertEquals (h , actualImg .getHeight (),
132
- "Check expected and actual icons have the same height" );
133
-
134
- int diffPixelCount = 0 ;
135
-
136
- for (int i = 0 ; i != w ; ++i ) {
137
- for (int j = 0 ; j != h ; ++j ) {
138
- int expectedRGB = expectedImg .getRGB (i , j );
139
- int actualRGB = actualImg .getRGB (i , j );
140
-
141
- if (expectedRGB != actualRGB ) {
142
- TKit .trace (String .format (
143
- "Images mismatch at [%d, %d] pixel" , i ,
144
- j ));
145
- diffPixelCount ++;
146
- }
147
- }
148
- }
149
-
150
- double threshold = 0.1 ;
151
- TKit .assertTrue (((double ) diffPixelCount ) / (w * h )
152
- < threshold ,
153
- String .format (
154
- "Check the number of mismatched pixels [%d] of [%d] is < [%f] threshold" ,
155
- diffPixelCount , (w * h ), threshold ));
156
- }
157
- });
158
- }
159
-
160
- private WinIconVerifier () {
161
- try {
162
- executableRebranderClass = Class .forName (
163
- "jdk.jpackage.internal.ExecutableRebrander" );
164
-
165
- lockResource = executableRebranderClass .getDeclaredMethod (
166
- "lockResource" , String .class );
167
- // Note: this reflection call requires
168
- // --add-opens jdk.jpackage/jdk.jpackage.internal=ALL-UNNAMED
169
- lockResource .setAccessible (true );
170
-
171
- unlockResource = executableRebranderClass .getDeclaredMethod (
172
- "unlockResource" , long .class );
173
- unlockResource .setAccessible (true );
174
-
175
- iconSwapWrapper = executableRebranderClass .getDeclaredMethod (
176
- "iconSwapWrapper" , long .class , String .class );
177
- iconSwapWrapper .setAccessible (true );
178
- } catch (ClassNotFoundException | NoSuchMethodException
179
- | SecurityException ex ) {
180
- throw rethrowUnchecked (ex );
181
- }
182
- }
183
-
184
- private Path extractIconFromExecutable (Path outputDir , Path executable ,
185
- String label ) {
186
- // Run .NET code to extract icon from the given executable.
187
- // ExtractAssociatedIcon() will succeed even if the target file
188
- // is locked (by an antivirus). It will output a default icon
189
- // in case of error. To prevent this "fail safe" behavior we try
190
- // lock the target file with Open() call. If the attempt
191
- // fails ExtractAssociatedIcon() is not called and the script exits
192
- // with the exit code that will be trapped
193
- // inside of Executor.executeAndRepeatUntilExitCode() method that
194
- // will keep running the script until it succeeds or the number of
195
- // allowed attempts is exceeded.
196
-
197
- Path extractedIcon = outputDir .resolve (label + ".bmp" );
198
- String script = String .join (";" ,
199
- String .format (
200
- "try { [System.io.File]::Open('%s', 'Open', 'Read', 'None') } catch { exit 100 }" ,
201
- executable .toAbsolutePath ().normalize ()),
202
- "[System.Reflection.Assembly]::LoadWithPartialName('System.Drawing')" ,
203
- String .format (
204
- "[System.Drawing.Icon]::ExtractAssociatedIcon('%s').ToBitmap().Save('%s', [System.Drawing.Imaging.ImageFormat]::Bmp)" ,
205
- executable .toAbsolutePath ().normalize (),
206
- extractedIcon .toAbsolutePath ().normalize ()));
207
-
208
- Executor .of ("powershell" , "-NoLogo" , "-NoProfile" , "-Command" ,
209
- script ).executeAndRepeatUntilExitCode (0 , 5 , 10 );
210
-
211
- return extractedIcon ;
212
- }
213
-
214
- private Path getDefaultAppLauncher (boolean noIcon ) {
215
- // Create app image with the sole purpose to get the default app launcher
216
- Path defaultAppOutputDir = TKit .workDir ().resolve (String .format (
217
- "out-%d" , ProcessHandle .current ().pid ()));
218
- JPackageCommand cmd = JPackageCommand .helloAppImage ().setFakeRuntime ().setArgumentValue (
219
- "--dest" , defaultAppOutputDir );
220
-
221
- String launcherName ;
222
- if (noIcon ) {
223
- launcherName = "no-icon" ;
224
- new AdditionalLauncher (launcherName ).setNoIcon ().applyTo (cmd );
225
- } else {
226
- launcherName = null ;
227
- }
228
-
229
- if (!Files .isExecutable (cmd .appLauncherPath (launcherName ))) {
230
- cmd .execute ();
231
- }
232
- return cmd .appLauncherPath (launcherName );
233
- }
234
-
235
- private void setIcon (Path iconPath , Path launcherPath ) {
236
- TKit .trace (String .format ("Set icon of [%s] launcher to [%s] file" ,
237
- launcherPath , iconPath ));
238
- try {
239
- launcherPath .toFile ().setWritable (true , true );
240
- try {
241
- long lock = 0 ;
242
- try {
243
- lock = (Long ) lockResource .invoke (null , new Object []{
244
- launcherPath .toAbsolutePath ().normalize ().toString ()});
245
- if (lock == 0 ) {
246
- throw new RuntimeException (String .format (
247
- "Failed to lock [%s] executable" ,
248
- launcherPath ));
249
- }
250
- iconSwapWrapper .invoke (null , new Object []{lock ,
251
- iconPath .toAbsolutePath ().normalize ().toString ()});
252
- } finally {
253
- if (lock != 0 ) {
254
- unlockResource .invoke (null , new Object []{lock });
255
- }
256
- }
257
- } catch (IllegalAccessException | InvocationTargetException ex ) {
258
- throw rethrowUnchecked (ex );
259
- }
260
- } finally {
261
- launcherPath .toFile ().setWritable (false , true );
262
- }
263
- }
264
-
265
- static final WinIconVerifier instance = new WinIconVerifier ();
266
-
267
- private final Class <?> executableRebranderClass ;
268
- private final Method lockResource ;
269
- private final Method unlockResource ;
270
- private final Method iconSwapWrapper ;
271
- }
272
-
273
80
private String launcherName ;
274
81
private Path expectedIcon ;
275
82
private boolean expectedDefault ;
0 commit comments