11using Injectio . Attributes ;
2+ using NLog ;
23using StabilityMatrix . Core . Helper ;
34using StabilityMatrix . Core . Helper . Cache ;
45using StabilityMatrix . Core . Helper . HardwareInfo ;
@@ -29,6 +30,11 @@ IPipWheelService pipWheelService
2930 pipWheelService
3031 )
3132{
33+ private static readonly Logger Logger = LogManager . GetCurrentClassLogger ( ) ;
34+ private const string LegacyUpgradeAlert = "You are updating from an old version" ;
35+ private const string ContinuePrompt = "Press Enter to Continue" ;
36+ public override PyVersion ? MinimumPythonVersion => Python . PyInstallationManager . Python_3_13_12 ;
37+
3238 public override string Name => "forge-classic" ;
3339 public override string Author => "Haoming02" ;
3440 public override string RepositoryName => "sd-webui-forge-classic" ;
@@ -44,7 +50,7 @@ IPipWheelService pipWheelService
4450 public override PackageDifficulty InstallerSortOrder => PackageDifficulty . ReallyRecommended ;
4551 public override IEnumerable < TorchIndex > AvailableTorchIndices => [ TorchIndex . Cuda ] ;
4652 public override bool IsCompatible => HardwareHelper . HasNvidiaGpu ( ) ;
47- public override PyVersion RecommendedPythonVersion => Python . PyInstallationManager . Python_3_11_13 ;
53+ public override PyVersion RecommendedPythonVersion => Python . PyInstallationManager . Python_3_13_12 ;
4854 public override PackageType PackageType => PackageType . Legacy ;
4955
5056 public override Dictionary < SharedOutputType , IReadOnlyList < string > > SharedOutputFolders =>
@@ -184,10 +190,33 @@ public override async Task InstallPackage(
184190 CancellationToken cancellationToken = default
185191 )
186192 {
193+ var requestedPythonVersion =
194+ options . PythonOptions . PythonVersion
195+ ?? (
196+ PyVersion . TryParse ( installedPackage . PythonVersion , out var parsedVersion )
197+ ? parsedVersion
198+ : RecommendedPythonVersion
199+ ) ;
200+
201+ var shouldUpgradePython = options . IsUpdate && requestedPythonVersion < MinimumPythonVersion ;
202+ var targetPythonVersion = shouldUpgradePython ? MinimumPythonVersion ! . Value : requestedPythonVersion ;
203+
204+ if ( shouldUpgradePython )
205+ {
206+ onConsoleOutput ? . Invoke (
207+ ProcessOutput . FromStdOutLine (
208+ $ "Upgrading venv Python from { requestedPythonVersion } to { targetPythonVersion } "
209+ )
210+ ) ;
211+
212+ ResetVenvForPythonUpgrade ( installLocation , onConsoleOutput ) ;
213+ }
214+
187215 progress ? . Report ( new ProgressReport ( - 1f , "Setting up venv" , isIndeterminate : true ) ) ;
188216 await using var venvRunner = await SetupVenvPure (
189217 installLocation ,
190- pythonVersion : options . PythonOptions . PythonVersion
218+ forceRecreate : shouldUpgradePython ,
219+ pythonVersion : targetPythonVersion
191220 )
192221 . ConfigureAwait ( false ) ;
193222
@@ -210,18 +239,174 @@ public override async Task InstallPackage(
210239
211240 // Run their install script with our venv Python
212241 venvRunner . WorkingDirectory = new DirectoryPath ( installLocation ) ;
213- venvRunner . RunDetached ( [ .. launchArgs ] , onConsoleOutput ) ;
214242
215- await venvRunner . Process . WaitForExitAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
243+ var sawLegacyUpdatePrompt = false ;
244+
245+ var exitCode = await RunInstallScriptWithPromptHandling (
246+ venvRunner ,
247+ launchArgs ,
248+ onConsoleOutput ,
249+ cancellationToken ,
250+ onLegacyPromptDetected : ( ) => sawLegacyUpdatePrompt = true
251+ )
252+ . ConfigureAwait ( false ) ;
253+
254+ // If legacy prompt was detected, back up old config files regardless of exit code.
255+ if ( options . IsUpdate && sawLegacyUpdatePrompt )
256+ {
257+ BackupLegacyConfigFiles ( installLocation , onConsoleOutput ) ;
258+
259+ // If it also failed, retry once after the backup.
260+ if ( exitCode != 0 )
261+ {
262+ onConsoleOutput ? . Invoke (
263+ ProcessOutput . FromStdOutLine (
264+ "[ForgeClassic] Retrying install after backing up legacy config files..."
265+ )
266+ ) ;
267+
268+ exitCode = await RunInstallScriptWithPromptHandling (
269+ venvRunner ,
270+ launchArgs ,
271+ onConsoleOutput ,
272+ cancellationToken
273+ )
274+ . ConfigureAwait ( false ) ;
275+ }
276+ }
277+
278+ if ( exitCode != 0 )
279+ {
280+ throw new InvalidOperationException ( $ "Install script failed with exit code { exitCode } ") ;
281+ }
282+
283+ if (
284+ ! string . Equals (
285+ installedPackage . PythonVersion ,
286+ targetPythonVersion . StringValue ,
287+ StringComparison . Ordinal
288+ )
289+ )
290+ {
291+ installedPackage . PythonVersion = targetPythonVersion . StringValue ;
292+ }
293+
294+ progress ? . Report ( new ProgressReport ( 1f , "Install complete" , isIndeterminate : false ) ) ;
295+ }
296+
297+ private async Task < int > RunInstallScriptWithPromptHandling (
298+ IPyVenvRunner venvRunner ,
299+ IReadOnlyCollection < string > launchArgs ,
300+ Action < ProcessOutput > ? onConsoleOutput ,
301+ CancellationToken cancellationToken ,
302+ Action ? onLegacyPromptDetected = null
303+ )
304+ {
305+ var enterSent = false ;
216306
217- if ( venvRunner . Process . ExitCode != 0 )
307+ void HandleInstallOutput ( ProcessOutput output )
218308 {
309+ onConsoleOutput ? . Invoke ( output ) ;
310+
311+ var isLegacyPrompt =
312+ output . Text . Contains ( LegacyUpgradeAlert , StringComparison . OrdinalIgnoreCase )
313+ || output . Text . Contains ( ContinuePrompt , StringComparison . OrdinalIgnoreCase ) ;
314+
315+ if ( ! isLegacyPrompt )
316+ return ;
317+
318+ onLegacyPromptDetected ? . Invoke ( ) ;
319+
320+ if ( enterSent || venvRunner . Process is null || venvRunner . Process . HasExited )
321+ return ;
322+
323+ try
324+ {
325+ venvRunner . Process . StandardInput . WriteLine ( ) ;
326+ enterSent = true ;
327+
328+ onConsoleOutput ? . Invoke (
329+ ProcessOutput . FromStdOutLine (
330+ "[ForgeClassic] Detected legacy update prompt. Sent Enter automatically."
331+ )
332+ ) ;
333+ }
334+ catch ( Exception e )
335+ {
336+ Logger . Warn ( e , "Failed to auto-submit Enter for Forge Classic update prompt" ) ;
337+ }
338+ }
339+
340+ venvRunner . RunDetached ( [ .. launchArgs ] , HandleInstallOutput ) ;
341+ var process =
342+ venvRunner . Process
343+ ?? throw new InvalidOperationException ( "Failed to start Forge Classic install process" ) ;
344+ await process . WaitForExitAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
345+ return process . ExitCode ;
346+ }
347+
348+ private void ResetVenvForPythonUpgrade ( string installLocation , Action < ProcessOutput > ? onConsoleOutput )
349+ {
350+ var venvPath = Path . Combine ( installLocation , "venv" ) ;
351+ if ( ! Directory . Exists ( venvPath ) )
352+ return ;
353+
354+ try
355+ {
356+ Directory . Delete ( venvPath , recursive : true ) ;
357+ onConsoleOutput ? . Invoke (
358+ ProcessOutput . FromStdOutLine ( "[ForgeClassic] Removed existing venv before Python upgrade." )
359+ ) ;
360+ }
361+ catch ( Exception e )
362+ {
363+ Logger . Warn ( e , "Failed to remove existing venv during Forge Classic Python upgrade" ) ;
219364 throw new InvalidOperationException (
220- $ "Install script failed with exit code { venvRunner . Process . ExitCode } "
365+ "Failed to remove existing venv for Python upgrade. Ensure Forge is not running and retry." ,
366+ e
221367 ) ;
222368 }
369+ }
223370
224- progress ? . Report ( new ProgressReport ( 1f , "Install complete" , isIndeterminate : false ) ) ;
371+ private void BackupLegacyConfigFiles ( string installLocation , Action < ProcessOutput > ? onConsoleOutput )
372+ {
373+ BackupLegacyConfigFile ( installLocation , "config.json" , onConsoleOutput ) ;
374+ BackupLegacyConfigFile ( installLocation , "ui-config.json" , onConsoleOutput ) ;
375+ }
376+
377+ private void BackupLegacyConfigFile (
378+ string installLocation ,
379+ string fileName ,
380+ Action < ProcessOutput > ? onConsoleOutput
381+ )
382+ {
383+ var sourcePath = Path . Combine ( installLocation , fileName ) ;
384+ if ( ! File . Exists ( sourcePath ) )
385+ return ;
386+
387+ var backupPath = GetBackupPath ( sourcePath ) ;
388+ File . Move ( sourcePath , backupPath ) ;
389+
390+ var message = $ "[ForgeClassic] Backed up { fileName } to { Path . GetFileName ( backupPath ) } ";
391+ Logger . Info ( message ) ;
392+ onConsoleOutput ? . Invoke ( ProcessOutput . FromStdOutLine ( message ) ) ;
393+ }
394+
395+ private static string GetBackupPath ( string sourcePath )
396+ {
397+ var nextPath = sourcePath + ".bak" ;
398+ if ( ! File . Exists ( nextPath ) )
399+ return nextPath ;
400+
401+ var index = 1 ;
402+ while ( true )
403+ {
404+ nextPath = sourcePath + $ ".bak.{ index } ";
405+ if ( ! File . Exists ( nextPath ) )
406+ return nextPath ;
407+
408+ index ++ ;
409+ }
225410 }
226411
227412 private async Task InstallTritonAndSageAttention ( InstalledPackage ? installedPackage )
0 commit comments