@@ -8,7 +8,6 @@ import 'package:dio/io.dart';
88import 'package:fl_lib/fl_lib.dart' ;
99import 'package:freezed_annotation/freezed_annotation.dart' ;
1010import 'package:riverpod_annotation/riverpod_annotation.dart' ;
11- import 'package:server_box/core/extension/context/locale.dart' ;
1211import 'package:server_box/data/model/app/error.dart' ;
1312import 'package:server_box/data/model/server/pve.dart' ;
1413import 'package:server_box/data/model/server/server_private_info.dart' ;
@@ -19,6 +18,13 @@ part 'pve.g.dart';
1918
2019typedef PveCtrlFunc = Future <bool > Function (String node, String id);
2120
21+ enum PveLoadingStep {
22+ none,
23+ forwarding,
24+ loggingIn,
25+ fetchingData,
26+ }
27+
2228@freezed
2329abstract class PveState with _$PveState {
2430 const factory PveState ({
@@ -27,43 +33,51 @@ abstract class PveState with _$PveState {
2733 @Default (null ) String ? release,
2834 @Default (false ) bool isBusy,
2935 @Default (false ) bool isConnected,
36+ @Default (PveLoadingStep .none) PveLoadingStep loadingStep,
3037 }) = _PveState ;
3138}
3239
3340@riverpod
3441class PveNotifier extends _$PveNotifier {
35- late final Spi spi;
36- late String addr;
37- late final SSHClient _client;
38- late final ServerSocket _serverSocket;
42+ String ? addr;
43+ ServerSocket ? _serverSocket;
3944 final List <SSHForwardChannel > _forwards = [];
4045 int _localPort = 0 ;
41- late final Dio session ;
42- late final bool _ignoreCert;
46+ Dio ? _session ;
47+ bool _ignoreCert = false ;
4348
44- @override
45- PveState build (Spi spiParam) {
46- spi = spiParam;
47- final serverState = ref.watch (serverProvider (spi.id));
49+ Dio get session => _session! ;
50+ String get addrValue => addr! ;
51+
52+ SSHClient get _client {
53+ final serverState = ref.read (serverProvider (spiParam.id));
4854 final client = serverState.client;
4955 if (client == null ) {
50- return const PveState (error : PveErr (type: PveErrType .net, message: 'Server client is null' ) );
56+ throw PveErr (type: PveErrType .net, message: 'Server client is null' );
5157 }
52- _client = client;
53- final addr = spi.custom? .pveAddr;
54- if (addr == null ) {
58+ return client;
59+ }
60+
61+ @override
62+ PveState build (Spi spiParam) {
63+ ref.onDispose (() => dispose ());
64+ final serverState = ref.watch (serverProvider (spiParam.id));
65+ if (serverState.client == null ) {
66+ return const PveState (loadingStep: PveLoadingStep .forwarding);
67+ }
68+ final pveAddr = spiParam.custom? .pveAddr;
69+ if (pveAddr == null ) {
5570 return PveState (error: PveErr (type: PveErrType .net, message: 'PVE address is null' ));
5671 }
57- this . addr = addr ;
58- _ignoreCert = spi .custom? .pveIgnoreCert ?? false ;
72+ addr = pveAddr ;
73+ _ignoreCert = spiParam .custom? .pveIgnoreCert ?? false ;
5974 _initSession ();
60- // Async initialization
6175 Future .microtask (() => _init ());
62- return const PveState ();
76+ return const PveState (loadingStep : PveLoadingStep .forwarding );
6377 }
6478
6579 void _initSession () {
66- session = Dio ()
80+ _session = Dio ()
6781 ..httpClientAdapter = IOHttpClientAdapter (
6882 createHttpClient: () {
6983 final client = HttpClient ();
@@ -79,35 +93,53 @@ class PveNotifier extends _$PveNotifier {
7993
8094 bool get onlyOneNode => state.data? .nodes.length == 1 ;
8195
96+ Future <void > reconnect () async {
97+ state = state.copyWith (error: null , isConnected: false , loadingStep: PveLoadingStep .forwarding);
98+ await _init ();
99+ }
100+
82101 Future <void > _init () async {
83102 try {
103+ if (! ref.mounted) return ;
104+ state = state.copyWith (loadingStep: PveLoadingStep .forwarding);
84105 await _forward ();
106+ if (! ref.mounted) return ;
107+ state = state.copyWith (loadingStep: PveLoadingStep .loggingIn);
85108 await _login ();
109+ if (! ref.mounted) return ;
110+ state = state.copyWith (loadingStep: PveLoadingStep .fetchingData);
86111 await _getRelease ();
112+ if (! ref.mounted) return ;
87113 state = state.copyWith (isConnected: true );
88- } on PveErr {
89- state = state.copyWith (error: PveErr (type: PveErrType .loginFailed, message: l10n.pveLoginFailed));
114+ await list ();
115+ if (! ref.mounted) return ;
116+ state = state.copyWith (loadingStep: PveLoadingStep .none);
117+ } on PveErr catch (e) {
118+ if (! ref.mounted) return ;
119+ state = state.copyWith (error: e, loadingStep: PveLoadingStep .none);
90120 } catch (e, s) {
121+ if (! ref.mounted) return ;
91122 Loggers .app.warning ('PVE init failed' , e, s);
92- state = state.copyWith (error: PveErr (type: PveErrType .unknown, message: e.toString ()));
123+ state = state.copyWith (error: PveErr (type: PveErrType .unknown, message: e.toString ()), loadingStep : PveLoadingStep .none );
93124 }
94125 }
95126
96127 Future <void > _forward () async {
97- final url = Uri .parse (addr );
128+ final url = Uri .parse (addrValue );
98129 if (_localPort == 0 ) {
99130 _serverSocket = await ServerSocket .bind ('localhost' , 0 );
100- _localPort = _serverSocket.port;
101- _serverSocket.listen ((socket) async {
102- final forward = await _client.forwardLocal (url.host, url.port);
103- _forwards.add (forward);
104- forward.stream.cast <List <int >>().pipe (socket);
105- socket.cast <List <int >>().pipe (forward.sink);
131+ _localPort = _serverSocket! .port;
132+ _serverSocket! .listen ((socket) async {
133+ try {
134+ final forward = await _client.forwardLocal (url.host, url.port);
135+ _forwards.add (forward);
136+ forward.stream.cast <List <int >>().pipe (socket);
137+ socket.cast <List <int >>().pipe (forward.sink);
138+ } catch (e, s) {
139+ Loggers .app.warning ('PVE forward failed' , e, s);
140+ socket.destroy ();
141+ }
106142 });
107- final newUrl = Uri .parse (
108- addr,
109- ).replace (host: 'localhost' , port: _localPort).toString ();
110- dprint ('Forwarding $newUrl to $addr ' );
111143 }
112144 }
113145
@@ -116,15 +148,6 @@ class PveNotifier extends _$PveNotifier {
116148 String ? proxyHost,
117149 int ? proxyPort,
118150 ) async {
119- /* final serverSocket = await ServerSocket.bind(InternetAddress.anyIPv4, 0);
120- final _localPort = serverSocket.port;
121- serverSocket.listen((socket) async {
122- final forward = await _client.forwardLocal(url.host, url.port);
123- forwards.add(forward);
124- forward.stream.cast<List<int>>().pipe(socket);
125- socket.cast<List<int>>().pipe(forward.sink);
126- });*/
127-
128151 if (url.isScheme ('https' )) {
129152 return SecureSocket .startConnect (
130153 'localhost' ,
@@ -137,33 +160,43 @@ class PveNotifier extends _$PveNotifier {
137160 }
138161
139162 Future <void > _login () async {
163+ final useKeyAuth = spiParam.keyId != null ;
164+ final password = useKeyAuth ? spiParam.custom? .pvePwd : spiParam.pwd;
165+ if (password == null ) {
166+ throw PveErr (type: PveErrType .loginFailed, message: 'PVE password is required. Please set it in server settings.' );
167+ }
140168 final resp = await session.post (
141- '$addr /api2/extjs/access/ticket' ,
169+ '$addrValue /api2/extjs/access/ticket' ,
142170 data: {
143- 'username' : spi .user,
144- 'password' : spi.pwd ,
171+ 'username' : spiParam .user,
172+ 'password' : password ,
145173 'realm' : 'pam' ,
146174 'new-format' : '1' ,
147175 },
148176 options: Options (
149177 headers: {HttpHeaders .contentTypeHeader: Headers .jsonContentType},
150178 ),
151179 );
152- try {
153- final ticket = resp.data['data' ]['ticket' ];
154- session.options.headers['CSRFPreventionToken' ] =
155- resp.data['data' ]['CSRFPreventionToken' ];
156- session.options.headers['Cookie' ] = 'PVEAuthCookie=$ticket ' ;
157- } catch (e) {
158- throw PveErr (type: PveErrType .loginFailed, message: e.toString ());
180+
181+ final data = resp.data['data' ];
182+ if (data['NeedTFA' ] == 1 ) {
183+ throw PveErr (type: PveErrType .needTfa, message: 'Two-factor authentication is not supported yet. Please disable OTP on the PVE server and try again.' );
159184 }
185+
186+ _setAuthHeaders (data);
187+ }
188+
189+ void _setAuthHeaders (Map <String , dynamic > data) {
190+ final ticket = data['ticket' ];
191+ session.options.headers['CSRFPreventionToken' ] = data['CSRFPreventionToken' ];
192+ session.options.headers['Cookie' ] = 'PVEAuthCookie=$ticket ' ;
160193 }
161194
162195 /// Returns true if the PVE version is 8.0 or later
163196 Future <void > _getRelease () async {
164- final resp = await session.get ('$addr /api2/extjs/version' );
197+ final resp = await session.get ('$addrValue /api2/extjs/version' );
165198 final version = resp.data['data' ]['release' ] as String ? ;
166- if (version != null ) {
199+ if (version != null && ref.mounted ) {
167200 state = state.copyWith (release: version);
168201 }
169202 }
@@ -172,25 +205,29 @@ class PveNotifier extends _$PveNotifier {
172205 if (! state.isConnected || state.isBusy) return ;
173206 state = state.copyWith (isBusy: true );
174207 try {
175- final resp = await session.get ('$addr /api2/json/cluster/resources' );
208+ final resp = await session.get ('$addrValue /api2/json/cluster/resources' );
176209 final res = resp.data['data' ] as List ;
177210 final result = await Computer .shared.start (PveRes .parse, (
178211 res,
179212 state.data,
180213 ));
214+ if (! ref.mounted) return ;
181215 state = state.copyWith (data: result, error: null );
182216 } catch (e) {
217+ if (! ref.mounted) return ;
183218 Loggers .app.warning ('PVE list failed' , e);
184219 state = state.copyWith (error: PveErr (type: PveErrType .unknown, message: e.toString ()));
185220 } finally {
186- state = state.copyWith (isBusy: false );
221+ if (ref.mounted) {
222+ state = state.copyWith (isBusy: false );
223+ }
187224 }
188225 }
189226
190227 Future <bool > reboot (String node, String id) async {
191228 if (! state.isConnected) return false ;
192229 final resp = await session.post (
193- '$addr /api2/json/nodes/$node /$id /status/reboot' ,
230+ '$addrValue /api2/json/nodes/$node /$id /status/reboot' ,
194231 );
195232 final success = _isCtrlSuc (resp);
196233 if (success) await list (); // Refresh data
@@ -200,7 +237,7 @@ class PveNotifier extends _$PveNotifier {
200237 Future <bool > start (String node, String id) async {
201238 if (! state.isConnected) return false ;
202239 final resp = await session.post (
203- '$addr /api2/json/nodes/$node /$id /status/start' ,
240+ '$addrValue /api2/json/nodes/$node /$id /status/start' ,
204241 );
205242 final success = _isCtrlSuc (resp);
206243 if (success) await list (); // Refresh data
@@ -210,7 +247,7 @@ class PveNotifier extends _$PveNotifier {
210247 Future <bool > stop (String node, String id) async {
211248 if (! state.isConnected) return false ;
212249 final resp = await session.post (
213- '$addr /api2/json/nodes/$node /$id /status/stop' ,
250+ '$addrValue /api2/json/nodes/$node /$id /status/stop' ,
214251 );
215252 final success = _isCtrlSuc (resp);
216253 if (success) await list (); // Refresh data
@@ -220,7 +257,7 @@ class PveNotifier extends _$PveNotifier {
220257 Future <bool > shutdown (String node, String id) async {
221258 if (! state.isConnected) return false ;
222259 final resp = await session.post (
223- '$addr /api2/json/nodes/$node /$id /status/shutdown' ,
260+ '$addrValue /api2/json/nodes/$node /$id /status/shutdown' ,
224261 );
225262 final success = _isCtrlSuc (resp);
226263 if (success) await list (); // Refresh data
@@ -233,7 +270,7 @@ class PveNotifier extends _$PveNotifier {
233270
234271 Future <void > dispose () async {
235272 try {
236- await _serverSocket.close ();
273+ await _serverSocket? .close ();
237274 } catch (e, s) {
238275 Loggers .app.warning ('Failed to close server socket' , e, s);
239276 }
0 commit comments