spice-client 0.2.0

A pure Rust SPICE client library with native and WebAssembly support
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
# SPICE Client WASM Architecture

## Executive Summary

The SPICE client has successfully implemented a clean WASM architecture:

✅ **Transport Abstraction** - Unified networking via Transport trait  
✅ **Cross-platform Dependencies** - Using `instant`, `getrandom`, etc.  
✅ **Helper Functions** - Platform utilities in `utils.rs`  
✅ **Minimal Conditionals** - Best files have only 4 platform checks  
✅ **Good Separation** - Platform code is well isolated  

The architecture is production-ready for both native and WebAssembly targets.

## Overview

This document outlines the current WebAssembly (WASM) architecture and remaining improvements:
1. Current implementation status and what's working well
2. Opportunities for further improvement
3. Testing infrastructure recommendations

## Current State Analysis (Updated)

### WASM Implementation Status ✅

The codebase has made significant progress in WASM compatibility:

#### Well-Architected Files (Minimal Conditionals)
- `src/channels/connection.rs` - **Only 4 conditionals** using Transport trait
-`src/utils.rs` - Provides cross-platform helpers (sleep, spawn_task)
-`src/transport.rs` - Clean transport abstraction
-`src/protocol.rs` - Fully cross-platform
-`src/video/frame.rs` - Uses `instant::Instant`

#### Files with Higher Conditional Count
- `src/channels/mod.rs` - 14 conditionals (old ChannelConnection implementation)
- `src/client.rs` - Platform-specific implementations
- `src/channels/main.rs` - Core protocol variations
- `src/channels/display.rs` - Different rendering paths

### Key Architectural Improvements Already Made

1. **Transport Abstraction**: Clean `Transport` trait isolates platform differences
2. **Helper Functions**: `utils.rs` provides cross-platform utilities
3. **Cross-platform Dependencies**: Using `instant`, `getrandom`, etc.
4. **Good Separation**: Platform-specific code is mostly isolated

### Remaining Opportunities

1. **Remove Duplication**: Two ChannelConnection implementations exist
2. **Feature Flags**: Could use feature flags instead of target_arch
3. **Further Abstraction**: Some files still have many conditionals

## Current Architecture: Transport Abstraction ✅

The codebase already implements a clean abstraction pattern:

### 1. Networking: Transport Trait Pattern (Already Implemented)

**Current Implementation**: `src/transport.rs` provides a unified interface

```rust
// Transport trait abstracts TCP vs WebSocket
#[async_trait]
pub trait Transport: Send + Sync {
    async fn read(&mut self, buf: &mut [u8]) -> io::Result<usize>;
    async fn write(&mut self, buf: &[u8]) -> io::Result<()>;
    async fn write_all(&mut self, buf: &[u8]) -> io::Result<()>;
    async fn flush(&mut self) -> io::Result<()>;
    fn is_connected(&self) -> bool;
    async fn close(&mut self) -> io::Result<()>;
}

// Platform-specific implementations
#[cfg(not(target_arch = "wasm32"))]
pub mod tcp;  // TCP implementation

#[cfg(target_arch = "wasm32")]
pub mod websocket;  // WebSocket implementation
```

**Usage in `channels/connection.rs`**:
```rust
pub struct ChannelConnection {
    transport: Box<dyn Transport>,  // Works with any transport
    // ... other fields
}
```

This pattern successfully reduces platform conditionals to just 4 in the connection module!

### 2. Async Runtime: Use compatible runtime

**Current**: `tokio` (native) vs `wasm-bindgen-futures` (WASM)

**Solution**: 
- Use `async-std` with WASM support
- Or use `tokio` with `tokio_wasm_not_send` feature
- Or lightweight executors like `smol` that work everywhere

### 3. Time/Timers: Cross-platform timing ✅

**Already Implemented**: The codebase uses the `instant` crate

```rust
// In Cargo.toml
instant = { version = "0.1", features = ["wasm-bindgen"] }

// In code (e.g., src/video/frame.rs)
use instant::{Instant, Duration};
```

**Helper functions in `src/utils.rs`**:
```rust
pub async fn sleep(duration: Duration) {
    #[cfg(not(target_arch = "wasm32"))]
    tokio::time::sleep(duration).await;
    #[cfg(target_arch = "wasm32")]
    gloo_timers::future::sleep(duration).await;
}
```

### 4. Rendering: Keep existing structure

- Native: Keep current implementation
- WASM: Keep `canvas` rendering in `channels/display_wasm.rs`
- Just reduce the conditionals in shared code

### 5. File Organization (Keep existing structure)

```
src/
├── client.rs          # Minimize #[cfg] by using cross-platform crates
├── channels/
│   ├── mod.rs        # Reduce conditionals
│   ├── display.rs    # Shared display logic
│   └── display_wasm.rs # WASM-specific rendering (keep as-is)
└── protocol.rs       # Already platform-agnostic
```

## Implementation Plan (Simplified)

### Phase 1: Audit and Replace Platform-Specific Crates

1. **Identify all `#[cfg]` usage**:
   ```bash
   rg "#\[cfg\(.*wasm" --type rust
   ```

2. **Replace with cross-platform alternatives**:
   
   | Current | Replacement | Notes |
   |---------|-------------|-------|
   | `tokio::net::TcpStream` | `gloo-net::websocket::WebSocket` | Use WebSocket for both |
   | `std::time::Instant` | `instant::Instant` | Drop-in replacement |
   | `tokio::time::sleep` | `async-std::task::sleep` | Or `gloo-timers` |
   | Platform-specific spawn | `wasm_bindgen_futures::spawn_local` | Unified spawning |

### Phase 2: Gradual Migration (File by File)

Start with the easiest files first:

1. **`src/protocol.rs`** - Already platform-agnostic ✓
2. **`src/video.rs`** - Replace timer conditionals with `instant`
3. **`src/client_shared.rs`** - Unify spawn/sleep functions
4. **`src/client.rs`** - Biggest change: unified networking

Example migration:
```rust
// Before: src/client.rs
#[cfg(not(target_arch = "wasm32"))]
async fn connect_tcp(host: &str, port: u16) -> Result<TcpStream> {
    TcpStream::connect((host, port)).await
}

#[cfg(target_arch = "wasm32")]
async fn connect_websocket(url: &str) -> Result<WebSocket> {
    // WebSocket connection
}

// After: src/client.rs
async fn connect(url: &str) -> Result<impl AsyncRead + AsyncWrite> {
    // Use a crate that provides unified interface
    let socket = unified_websocket::connect(url).await?;
    Ok(socket)
}
```

### Phase 3: Keep Platform-Specific Code Isolated

Some code MUST remain platform-specific:

1. **Rendering**: 
   - Keep `display_wasm.rs` for Canvas rendering
   - Keep native rendering separate
   - Use a simple feature flag in `channels/mod.rs`

2. **Binary targets**:
   - `spice-test-client` remains native-only
   - WASM entry points stay in `lib.rs` with `#[wasm_bindgen]`

### Phase 4: Update Dependencies

```toml
[dependencies]
# Cross-platform deps (no more target-specific sections needed!)
instant = "0.1"
gloo-net = "0.4"
gloo-timers = "0.3"
futures = "0.3"
async-trait = "0.1"

# These remain platform-specific
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { version = "1", features = ["full"] }

[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = "0.2"
web-sys = "0.3"
```

## WASM Test Infrastructure

We implement a two-tier testing approach:

### Tier 1: Core Protocol Tests (Lightweight)
- **No browser required** - Uses Node.js with WebAssembly directly
- **Fast execution** - Tests only protocol and networking layers
- **Frequently run** - Part of regular CI/CD pipeline
- Tests:
  - WebSocket connections
  - Protocol handshakes
  - Message serialization/deserialization
  - Channel establishment
  - Connection state management

### Tier 2: Full Integration Tests (Browser-based)
- **Uses Playwright** - Full browser environment
- **Tests rendering** - Canvas, display updates, UI interactions
- **Less frequent** - Run for releases or major changes
- Tests:
  - Display rendering
  - Mouse/keyboard input through DOM
  - Canvas operations
  - Full client lifecycle

### 1. WebSocket Proxy Enhancement

Update the existing `websocket-proxy.py` to support multiple simultaneous connections:

```python
# docker/websocket-proxy-multi.py
class SpiceWebSocketProxy:
    def __init__(self):
        self.connections = {}
        
    async def handle_connection(self, websocket, path):
        # Support multiple SPICE channels
        channel_type = self.parse_channel_type(path)
        target_port = self.get_channel_port(channel_type)
        # ... proxy implementation
```

### 2. WASM Test Runner

Create a WASM test runner that can execute in a headless browser:

```javascript
// tests/wasm/test-runner.js
const { chromium } = require('playwright');

async function runWasmTests() {
    const browser = await chromium.launch({ headless: true });
    const page = await browser.newPage();
    
    // Load WASM test module
    await page.goto('http://localhost:8080/test.html');
    
    // Execute tests
    const results = await page.evaluate(async () => {
        const wasm = await import('/spice_client.js');
        return await wasm.run_all_tests();
    });
    
    console.log('Test results:', results);
    await browser.close();
}
```

### 3. Docker Compose for WASM E2E Tests

```yaml
# docker/docker-compose.wasm-e2e.yml
version: '3.8'

services:
  # SPICE server (same as native tests)
  qemu-spice:
    image: spice-test-server
    ports:
      - "5900:5900"
    
  # WebSocket proxy for all SPICE channels
  websocket-proxy:
    build:
      context: .
      dockerfile: Dockerfile.ws-proxy
    environment:
      - SPICE_HOST=qemu-spice
      - MAIN_CHANNEL_PORT=5900
      - DISPLAY_CHANNEL_PORT=5901
      - INPUTS_CHANNEL_PORT=5902
      - CURSOR_CHANNEL_PORT=5903
    ports:
      - "8080:8080"  # Main WebSocket
      - "8081:8081"  # Display WebSocket
      - "8082:8082"  # Inputs WebSocket
      - "8083:8083"  # Cursor WebSocket
    depends_on:
      - qemu-spice
    
  # WASM test runner
  wasm-test-runner:
    build:
      context: ..
      dockerfile: docker/Dockerfile.wasm-test
    volumes:
      - ../pkg:/app/pkg
      - ../tests/wasm:/app/tests
    environment:
      - WS_MAIN_URL=ws://websocket-proxy:8080
      - WS_DISPLAY_URL=ws://websocket-proxy:8081
      - WS_INPUTS_URL=ws://websocket-proxy:8082
      - WS_CURSOR_URL=ws://websocket-proxy:8083
    depends_on:
      - websocket-proxy
    command: npm test
```

### 4. WASM E2E Test Implementation

```rust
// tests/wasm/e2e_test.rs
#[wasm_bindgen_test]
async fn test_wasm_spice_connection() {
    // Initialize console logging
    console_log::init_with_level(log::Level::Debug).unwrap();
    
    // Connect to SPICE server through WebSocket proxy
    let client = spice_client::wasm::connect(
        "ws://localhost:8080",
        "test-canvas"
    ).await.unwrap();
    
    // Run connection test
    client.wait_for_init().await.unwrap();
    
    // Send test inputs
    client.send_key_event(KeyCode::A, true).await.unwrap();
    client.send_mouse_move(100, 100).await.unwrap();
    
    // Verify display updates
    let frame = client.capture_frame().await.unwrap();
    assert!(!frame.is_empty());
    
    client.disconnect().await.unwrap();
}
```

## Benefits of Simplified Approach

1. **Minimal Changes**: Keep existing code structure, just swap dependencies
2. **Proven Solutions**: Use battle-tested cross-platform crates
3. **Faster Migration**: No need to redesign the entire architecture
4. **Lower Risk**: Changes are localized and can be done incrementally
5. **Better Ecosystem**: Leverage existing Rust/WASM ecosystem

## Migration Strategy

1. **Start Small**: Begin with simple utilities (timers, instant)
2. **Test Continuously**: Use the WASM core tests after each change
3. **One File at a Time**: Complete migration file-by-file
4. **Keep What Works**: Don't change platform-specific code that's already working well

## Dependencies Update

Update Cargo.toml to better separate platform dependencies:

```toml
[dependencies]
# Core dependencies (used by all platforms)
binrw = "0.13"
thiserror = "1.0"
log = "0.4"

# Native dependencies
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { version = "1.35", features = ["full"] }
native-tls = "0.2"

# WASM dependencies  
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
web-sys = { version = "0.3", features = [
    "WebSocket", "MessageEvent", "ErrorEvent",
    "CanvasRenderingContext2d", "ImageData"
]}
js-sys = "0.3"

[dev-dependencies]
wasm-bindgen-test = "0.3"
playwright = "1.40"  # For WASM browser testing
```

## Recommendations for Further Improvement

### 1. Remove Duplicate ChannelConnection
The codebase has two ChannelConnection implementations:
- `src/channels/mod.rs` - Old implementation with 14+ conditionals
- `src/channels/connection.rs` - New implementation with Transport trait (only 4 conditionals)

**Action**: Remove the old implementation from mod.rs and update all channels to use connection.rs

### 2. Consider Feature Flags
Instead of `#[cfg(target_arch = "wasm32")]`, consider using feature flags:
```toml
[features]
default = ["native"]
native = ["tokio/full"]
wasm = ["wasm-bindgen", "web-sys"]
```

### 3. Reduce Conditionals in Remaining Files
Focus on files with higher conditional counts:
- `src/client.rs` - Consider using the Transport pattern here too
- `src/channels/main.rs` - Abstract platform differences
- `src/channels/display.rs` - Separate rendering logic more clearly

### 4. Performance Testing
- Benchmark Transport trait overhead
- Compare native TCP vs WebSocket performance
- Profile WASM bundle size

## Conclusion

The SPICE client demonstrates excellent WASM architecture with:
- Clean abstractions (Transport trait)
- Minimal platform conditionals where it matters
- Good use of cross-platform crates
- Well-organized code structure

The codebase is a good example of how to build a dual-target (native + WASM) Rust application.