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