11import path from 'node:path'
22
3+ import { globby } from 'globby'
4+ import fse from 'fs-extra'
5+ import { $ } from 'execa'
6+
37// @ts -expect-error I can't get tsconfig setup in a way that allows this without breaking other things.
48// However we execute with tsx and it's able to resolve the import without issues.
59import packageJson from '../package.json' assert { type : 'json' }
@@ -12,7 +16,7 @@ import Server from './modules/server/index.js'
1216import User from './modules/user.js'
1317import AppStore from './modules/apps/app-store.js'
1418import Apps from './modules/apps/apps.js'
15- import { detectDevice , setCpuGovernor } from './modules/system.js'
19+ import { detectDevice , setCpuGovernor , reboot } from './modules/system.js'
1620
1721import { commitOsPartition } from './modules/system.js'
1822
@@ -84,6 +88,88 @@ export default class Umbreld {
8488 }
8589 }
8690
91+ // By default Linux uses the UAS driver for most devices. This causes major
92+ // stability problems on the Raspberry Pi 4, not due to issues with UAS, but due
93+ // to devices running in UAS mode using much more power. The Pi can't reliably
94+ // provide enough power to the USB port and the entire system experiences
95+ // extreme instability. By blacklisting all devices from the UAS driver on first
96+ // and then rebooting we fall back to the mass-storage driver, which results in
97+ // decreased performance, but lower power usage, and much better system stability.
98+ // TODO: Move this to a system module
99+ async blacklistUASDriver ( ) {
100+ try {
101+ const justDidRebootFile = '/umbrel-just-did-reboot'
102+ // Only run on Raspberry Pi 4
103+ const { deviceId} = await detectDevice ( )
104+ if ( deviceId !== 'pi-4' ) return
105+ this . logger . log ( 'Checking for UAS devices to blacklist' )
106+ const blacklist = [ ]
107+ // Get all USB device uevent files
108+ const usbDeviceUeventFiles = await globby ( '/sys/bus/usb/devices/*/uevent' )
109+ for ( const ueventFile of usbDeviceUeventFiles ) {
110+ const uevent = await fse . readFile ( ueventFile , 'utf8' )
111+ if ( ! uevent . includes ( 'DRIVER=uas' ) ) continue
112+ const [ vendorId , productId ] = uevent
113+ . split ( '\n' )
114+ . find ( ( line ) => line ?. startsWith ( 'PRODUCT=' ) )
115+ . replace ( 'PRODUCT=' , '' )
116+ . split ( '/' )
117+ const deviceId = `${ vendorId } :${ productId } `
118+ this . logger . log ( `UAS device found ${ deviceId } ` )
119+ blacklist . push ( deviceId )
120+ }
121+
122+ // Don't reboot if we don't have any UAS devices
123+ if ( blacklist . length === 0 ) {
124+ this . logger . log ( 'No UAS devices found!' )
125+ await fse . remove ( justDidRebootFile )
126+ return
127+ }
128+
129+ // Check we're not in a boot loop
130+ if ( await fse . pathExists ( justDidRebootFile ) ) {
131+ this . logger . log ( 'We just rebooted, we could be in a bootloop, skipping reboot' )
132+ return
133+ }
134+
135+ // Read current cmdline
136+ this . logger . log ( `Applying quirks to cmdline.txt` )
137+ let cmdline = await fse . readFile ( '/boot/cmdline.txt' , 'utf8' )
138+
139+ // Don't apply quirks if they're already applied
140+ const quirksAlreadyApplied = blacklist . every ( ( deviceId ) => cmdline . includes ( `${ deviceId } :u` ) )
141+ if ( quirksAlreadyApplied ) {
142+ this . logger . log ( 'UAS quirks already applied, skipping' )
143+ return
144+ }
145+
146+ // Remove any current quirks
147+ cmdline = cmdline
148+ . trim ( )
149+ . split ( ' ' )
150+ . filter ( ( flag ) => ! flag . startsWith ( 'usb-storage.quirks=' ) )
151+ . join ( ' ' )
152+ // Add new quirks
153+ const quirks = blacklist . map ( ( deviceId ) => `${ deviceId } :u` ) . join ( ',' )
154+ cmdline = `${ cmdline } usb-storage.quirks=${ quirks } `
155+
156+ // Remount /boot as writable
157+ await $ `mount -o remount,rw /boot`
158+ // Write new cmdline
159+ await fse . writeFile ( '/boot/cmdline.txt' , cmdline )
160+
161+ // Reboot the system
162+ this . logger . log ( `Rebooting` )
163+ // We need to make sure we commit before rebooting otherwise
164+ // OTA updates will get instantly rolled back.
165+ await commitOsPartition ( this )
166+ await fse . writeFile ( justDidRebootFile , cmdline )
167+ await reboot ( )
168+ } catch ( error ) {
169+ this . logger . error ( `Failed to blacklist UAS driver: ${ ( error as Error ) . message } ` )
170+ }
171+ }
172+
87173 async start ( ) {
88174 this . logger . log ( `☂️ Starting Umbrel v${ this . version } ` )
89175 this . logger . log ( )
@@ -98,6 +184,9 @@ export default class Umbreld {
98184 // Set ondemand cpu governer for Raspberry Pi
99185 this . setupPiCpuGoverner ( )
100186
187+ // Blacklist UAS driver for Raspberry Pi 4
188+ await this . blacklistUASDriver ( )
189+
101190 // Run migration module before anything else
102191 // TODO: think through if we want to allow the server module to run before migration.
103192 // It might be useful if we add more complicated migrations so we can signal progress.
0 commit comments