Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions lib/core/extensions/extension_driver_session.dart
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,16 @@ class ExtensionDriverSession {
}
}

/// Restarts the extension driver process for [row] by cleanly disconnecting
/// the active bridge and re-establishing the connection.
Future<PluginRpcBridge> restart(ConnectionRow row) async {
final id = row.id;
if (id != null) {
await disconnect(id);
}
return ensureConnected(row);
}

Future<void> disconnectAll() async {
final ids = _bridges.keys.toList();
for (final id in ids) {
Expand Down
85 changes: 85 additions & 0 deletions lib/features/extensions/extension_driver_recovery_banner.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import 'package:flutter/material.dart' as material;
import 'package:shadcn_flutter/shadcn_flutter.dart';

/// Banner displayed when an extension driver process crashes, deadlocks,
/// or disconnects unexpectedly, allowing the user to restart the driver process
/// with a single click and retry the pending action.
class ExtensionDriverRecoveryBanner extends material.StatelessWidget {
const ExtensionDriverRecoveryBanner({
super.key,
required this.onRestart,
this.isRestarting = false,
this.customMessage,
});

final material.VoidCallback onRestart;
final bool isRestarting;
final String? customMessage;

@override
material.Widget build(material.BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == material.Brightness.dark;

final bgColor = isDark
? theme.colorScheme.destructive.withValues(alpha: 0.12)
: theme.colorScheme.destructive.withValues(alpha: 0.08);

final borderColor = theme.colorScheme.destructive.withValues(alpha: 0.35);

return material.Container(
margin: const material.EdgeInsets.fromLTRB(12, 8, 12, 8),
padding: const material.EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: material.BoxDecoration(
color: bgColor,
borderRadius: material.BorderRadius.circular(8),
border: material.Border.all(color: borderColor),
),
child: material.Row(
children: [
material.Icon(
material.Icons.power_off_rounded,
size: 20,
color: theme.colorScheme.destructive,
),
const Gap(12),
material.Expanded(
child: material.Column(
crossAxisAlignment: material.CrossAxisAlignment.start,
mainAxisSize: material.MainAxisSize.min,
children: [
const Text(
'Driver Process Terminated or Unresponsive',
).semiBold().small(),
const Gap(2),
Text(
customMessage ??
'The background driver process exited or lost communication. '
'Restart the driver to restore the connection.',
).muted().xSmall(),
],
),
),
const Gap(12),
OutlineButton(
size: ButtonSize.small,
onPressed: isRestarting ? null : onRestart,
leading: isRestarting
? const material.SizedBox(
width: 14,
height: 14,
child: material.CircularProgressIndicator(
strokeWidth: 2,
),
)
: const material.Icon(
material.Icons.restart_alt_rounded,
size: 16,
),
child: Text(isRestarting ? 'Restarting...' : 'Restart Driver'),
),
],
),
);
}
}
80 changes: 80 additions & 0 deletions lib/features/extensions/extension_sql_workspace.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:querya_desktop/core/extensions/extension_driver_session.dart';
import 'package:querya_desktop/core/layout/vertical_split_pane.dart';
import 'package:querya_desktop/core/storage/app_settings.dart';
import 'package:querya_desktop/core/storage/local_db.dart';
import 'package:querya_desktop/features/extensions/extension_driver_recovery_banner.dart';
import 'package:querya_desktop/features/workspace/workspace.dart';
import 'package:querya_desktop/shared/widgets/widgets.dart';

Expand Down Expand Up @@ -44,6 +45,7 @@ class _ExtensionSqlWorkspaceState
final ValueNotifier<double> _topFraction = ValueNotifier(0.6);

bool _running = false;
bool _restartingDriver = false;
String? _error;
List<String> _columns = [];
List<List<String>> _rows = [];
Expand All @@ -55,6 +57,52 @@ class _ExtensionSqlWorkspaceState

static const _previewRowLimit = 200;

bool get _isDriverError {
final err = _error;
if (err == null) return false;
return err.contains('PluginCrashedException') ||
err.contains('PluginDeadlockException') ||
err.contains('PluginProtocolTimeoutException') ||
err.contains('TimeoutException') ||
err.contains('SocketException') ||
err.contains('Broken pipe') ||
err.contains('JsonRpcStdioClient') ||
err.contains('Connection') ||
err.contains('is not started');
}

Future<void> _restartDriver() async {
if (_restartingDriver) return;
setState(() {
_restartingDriver = true;
});
try {
await ExtensionDriverSession.instance.restart(widget.connectionRow);
if (!mounted) return;
showAppToast(
context: context,
message: 'Driver restarted successfully',
variant: AppToastVariant.success,
);
setState(() {
_restartingDriver = false;
_error = null;
});
await _execute();
} catch (e) {
if (!mounted) return;
setState(() {
_error = 'Driver restart failed: $e';
_restartingDriver = false;
});
showAppToast(
context: context,
message: 'Driver restart failed: $e',
variant: AppToastVariant.error,
);
}
}

@override
void initState() {
super.initState();
Expand Down Expand Up @@ -313,6 +361,8 @@ class _ExtensionSqlWorkspaceState
connectionName: widget.connectionRow.name,
onExecute: _running ? null : () => unawaited(_execute()),
running: _running,
isRestarting: _restartingDriver,
onRestartDriver: _running ? null : () => unawaited(_restartDriver()),
onOpenSqlFile: () => unawaited(_openSqlFile()),
onSaveSqlFile: () => unawaited(_saveSqlFile()),
onOpenHistory: widget.connectionRow.id != null && !_running
Expand Down Expand Up @@ -355,6 +405,12 @@ class _ExtensionSqlWorkspaceState
errorMessage: _error,
isLoading: _running,
statusLine: _statusLine,
errorAction: _isDriverError
? ExtensionDriverRecoveryBanner(
onRestart: () => unawaited(_restartDriver()),
isRestarting: _restartingDriver,
)
: null,
),
),
],
Expand All @@ -373,6 +429,8 @@ class _ExtensionSqlToolbar extends material.StatelessWidget {
required this.onOpenSqlFile,
required this.onSaveSqlFile,
this.onOpenHistory,
this.onRestartDriver,
this.isRestarting = false,
});

final String connectionName;
Expand All @@ -381,6 +439,8 @@ class _ExtensionSqlToolbar extends material.StatelessWidget {
final VoidCallback onOpenSqlFile;
final VoidCallback onSaveSqlFile;
final VoidCallback? onOpenHistory;
final VoidCallback? onRestartDriver;
final bool isRestarting;

@override
material.Widget build(material.BuildContext context) {
Expand All @@ -394,6 +454,26 @@ class _ExtensionSqlToolbar extends material.StatelessWidget {
child: Text('Query · $connectionName').semiBold().small(),
),
const Spacer(),
if (onRestartDriver != null) ...[
IconButton.ghost(
onPressed: running || isRestarting ? null : onRestartDriver,
icon: isRestarting
? material.SizedBox(
width: 14,
height: 14,
child: material.CircularProgressIndicator(
strokeWidth: 2,
color: accent,
),
)
: material.Icon(
material.Icons.restart_alt_rounded,
size: 18,
color: accent,
),
),
const Gap(4),
],
IconButton.ghost(
onPressed: running ? null : onOpenSqlFile,
icon: material.Icon(
Expand Down
24 changes: 24 additions & 0 deletions lib/features/extensions/extension_table_toolbar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ class ExtensionTableToolbar extends material.StatelessWidget {
this.onCopyFormat,
this.onSaveFormat,
this.onNavigateHome,
this.onRestartDriver,
this.isRestarting = false,
});

final String title;
Expand All @@ -42,6 +44,8 @@ class ExtensionTableToolbar extends material.StatelessWidget {
final material.ValueChanged<DataExportFormat>? onCopyFormat;
final material.ValueChanged<DataExportFormat>? onSaveFormat;
final VoidCallback? onNavigateHome;
final VoidCallback? onRestartDriver;
final bool isRestarting;

@override
material.Widget build(material.BuildContext context) {
Expand Down Expand Up @@ -193,6 +197,26 @@ class ExtensionTableToolbar extends material.StatelessWidget {
child: const Text('Next'),
),
const Gap(8),
if (onRestartDriver != null) ...[
OutlineButton(
size: ButtonSize.small,
onPressed: loading || isRestarting ? null : onRestartDriver,
leading: isRestarting
? const material.SizedBox(
width: 14,
height: 14,
child: material.CircularProgressIndicator(
strokeWidth: 2,
),
)
: const material.Icon(
material.Icons.restart_alt_rounded,
size: 15,
),
child: Text(isRestarting ? 'Restarting...' : 'Restart Driver'),
),
const Gap(4),
],
OutlineButton(
size: ButtonSize.small,
onPressed: loading ? null : onRefresh,
Expand Down
57 changes: 57 additions & 0 deletions lib/features/extensions/extension_table_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:querya_desktop/core/database/table_mutation_engine.dart';
import 'package:querya_desktop/core/extensions/extension_driver_session.dart';
import 'package:querya_desktop/core/extensions/models/extension_driver_capabilities.dart';
import 'package:querya_desktop/core/storage/local_db.dart';
import 'package:querya_desktop/features/extensions/extension_driver_recovery_banner.dart';
import 'package:querya_desktop/features/extensions/extension_table_toolbar.dart';
import 'package:querya_desktop/features/workspace/workspace.dart';
import 'package:querya_desktop/shared/services/data_export_service.dart';
Expand Down Expand Up @@ -52,13 +53,61 @@ class _ExtensionTableViewState extends material.State<ExtensionTableView> {
DataGridStagingBuffer? _stagingBuffer;
bool _isSaving = false;

bool _restartingDriver = false;

String get _qualifiedName => '`${widget.database}`.`${widget.tableName}`';

String get _whereClause {
final text = _filterController.text.trim();
return text.isEmpty ? '' : ' WHERE $text';
}

bool get _isDriverError {
final err = _error;
if (err == null) return false;
return err.contains('PluginCrashedException') ||
err.contains('PluginDeadlockException') ||
err.contains('PluginProtocolTimeoutException') ||
err.contains('TimeoutException') ||
err.contains('SocketException') ||
err.contains('Broken pipe') ||
err.contains('JsonRpcStdioClient') ||
err.contains('Connection') ||
err.contains('is not started');
}

Future<void> _restartDriver() async {
if (_restartingDriver) return;
setState(() {
_restartingDriver = true;
});
try {
await ExtensionDriverSession.instance.restart(widget.connectionRow);
if (!mounted) return;
showAppToast(
context: context,
message: 'Driver restarted successfully',
variant: AppToastVariant.success,
);
setState(() {
_restartingDriver = false;
_error = null;
});
await _loadPage(refreshCount: true);
} catch (e) {
if (!mounted) return;
setState(() {
_error = 'Driver restart failed: $e';
_restartingDriver = false;
});
showAppToast(
context: context,
message: 'Driver restart failed: $e',
variant: AppToastVariant.error,
);
}
}

@override
void initState() {
super.initState();
Expand Down Expand Up @@ -414,6 +463,8 @@ class _ExtensionTableViewState extends material.State<ExtensionTableView> {
onGoPrevious: _previousPage,
onGoNext: _nextPage,
onRefresh: () => _loadPage(refreshCount: true),
onRestartDriver: () => unawaited(_restartDriver()),
isRestarting: _restartingDriver,
onCopyFormat: (format) {
unawaited(() async {
await DataExportService.copyToClipboard(
Expand Down Expand Up @@ -495,6 +546,12 @@ class _ExtensionTableViewState extends material.State<ExtensionTableView> {
stagingBuffer: _stagingBuffer,
onApplyChanges: _stagingBuffer != null ? _onApplyChanges : null,
isSaving: _isSaving,
errorAction: _isDriverError
? ExtensionDriverRecoveryBanner(
onRestart: () => unawaited(_restartDriver()),
isRestarting: _restartingDriver,
)
: null,
),
),
],
Expand Down
Loading
Loading