@@ -2,12 +2,17 @@ import * as Chalk from 'chalk';
22import { exec } from 'child_process' ;
33import * as copyfiles from 'copyfiles' ;
44import * as fs from 'fs' ;
5+ import * as readline from 'readline' ;
6+ import { Key } from 'readline' ;
57import { asLines , isFunction , isNumber , isObject , isString , processMillis , toBoolean , toNumber } from '@tubular/util' ;
68import * as path from 'path' ;
79import { convertPinToGpio } from './server/src/rpi-pin-conversions' ;
810import { ErrorMode , getSudoUser , getUserHome , monitorProcess , monitorProcessLines , sleep , spawn } from './server/src/process-util' ;
911import { promisify } from 'util' ;
1012
13+ readline . emitKeypressEvents ( process . stdin ) ;
14+ process . stdin . setRawMode ( true ) ;
15+
1116const CHECK_MARK = '\u2714' ;
1217const FAIL_MARK = '\u2718' ;
1318const SPIN_CHARS = '|/-\\' ;
@@ -29,6 +34,7 @@ let doStdDeploy = false;
2934let doDedicated = false ;
3035let doLaunch = false ;
3136let doReboot = false ;
37+ let noStop = false ;
3238let prod = false ;
3339let viaBash = false ;
3440let interactive = false ;
@@ -60,7 +66,7 @@ chalk.paleYellow = chalk.hex('#FFFFAA');
6066let backspace = '\x08' ;
6167let sol = '\x1B[1G' ;
6268let trailingSpace = ' ' ; // Two spaces
63- let totalSteps = 3 ;
69+ let totalSteps = 2 ;
6470let currentStep = 0 ;
6571const settings : Record < string , string > = {
6672 AWC_ALLOW_ADMIN : 'false' ,
@@ -81,6 +87,7 @@ const settingsPath = '/etc/default/weatherService';
8187const rpiSetupStuff = path . join ( __dirname , 'raspberry_pi_setup' ) ;
8288const serviceSrc = rpiSetupStuff + '/weatherService' ;
8389const serviceDst = '/etc/init.d/.' ;
90+ const serviceDstFull = '/etc/init.d/weatherService' ;
8491const fontSrc = rpiSetupStuff + '/fonts/' ;
8592const fontDst = '/usr/local/share/fonts/' ;
8693let chromium = 'chromium' ;
@@ -181,6 +188,9 @@ process.argv.forEach(arg => {
181188 onlyOnRaspberryPi . push ( arg ) ;
182189 onlyDedicated . push ( arg ) ;
183190 break ;
191+ case '--nostop' :
192+ noStop = true ;
193+ break ;
184194 case '-p' :
185195 prod = true ;
186196 break ;
@@ -227,21 +237,24 @@ process.argv.forEach(arg => {
227237
228238 console . error ( 'Unrecognized option "' + chalk . redBright ( arg ) + '"' ) ;
229239 console . log ( helpMsg ) ;
230- process . exit ( 0 ) ;
240+ process . exit ( 1 ) ;
231241 }
232242 }
233243} ) ;
234244
245+ if ( noStop )
246+ doReboot = doLaunch = false ;
247+
235248if ( ! treatAsRaspberryPi && onlyOnRaspberryPi . length > 0 ) {
236249 onlyOnRaspberryPi . forEach ( opt =>
237250 console . error ( chalk . redBright ( opt ) + ' option is only valid on Raspberry Pi' ) ) ;
238- process . exit ( 0 ) ;
251+ process . exit ( 1 ) ;
239252}
240253
241254if ( ! doDedicated && onlyDedicated . length > 0 ) {
242255 onlyDedicated . forEach ( opt =>
243256 console . error ( chalk . redBright ( opt ) + ' option is only valid when used with the --ddev or -i options' ) ) ;
244- process . exit ( 0 ) ;
257+ process . exit ( 1 ) ;
245258}
246259
247260if ( treatAsRaspberryPi ) {
@@ -289,14 +302,56 @@ if (treatAsRaspberryPi) {
289302if ( ! isRaspberryPi && doAcu )
290303 console . warn ( chalk . yellow ( 'Warning: this setup will only generate fake wireless sensor data' ) ) ;
291304
292- async function readLine ( ) : Promise < string > {
305+ async function readUserInput ( ) : Promise < string > {
293306 return new Promise < string > ( resolve => {
294- const callback = ( data : any ) => {
295- process . stdin . off ( 'data' , callback ) ;
296- resolve ( data . toString ( ) . trim ( ) ) ;
307+ let buffer = '' ;
308+ let length = 0 ;
309+ const clearLine = ( ) => write ( '\x08 \x08' . repeat ( length ) ) ;
310+
311+ const callback = ( ch : string , key : Key ) => {
312+ if ( ch === '\x03' ) { // ctrl-C
313+ write ( '^C\n' ) ;
314+ process . exit ( 130 ) ;
315+ }
316+ else if ( ch === '\x15' ) { // ctrl-U
317+ clearLine ( ) ;
318+ length = 0 ;
319+ }
320+ else if ( key . name === 'enter' || key . name === 'return' ) {
321+ write ( '\n' ) ;
322+ process . stdin . off ( 'keypress' , callback ) ;
323+ resolve ( buffer . substr ( 0 , length ) . trim ( ) ) ;
324+ }
325+ else if ( key . name === 'backspace' || key . name === 'left' ) {
326+ if ( length > 0 ) {
327+ write ( '\x08 \x08' ) ;
328+ -- length ;
329+ }
330+ }
331+ else if ( key . name === 'delete' ) {
332+ if ( length > 0 ) {
333+ write ( '\x08 \x08' ) ;
334+ buffer = buffer . substr ( 0 , -- length ) + buffer . substr ( length + 1 ) ;
335+ }
336+ }
337+ else if ( key . name === 'up' ) {
338+ clearLine ( ) ;
339+ write ( '\n' ) ;
340+ process . stdin . off ( 'keypress' , callback ) ;
341+ resolve ( '\x18' ) ;
342+ }
343+ else if ( key . name === 'right' ) {
344+ if ( length < buffer . length ) {
345+ write ( buffer . charAt ( length ++ ) ) ;
346+ }
347+ }
348+ else if ( ch != null && ch >= ' ' && ! key . ctrl && ! key . meta ) {
349+ write ( ch ) ;
350+ buffer = buffer . substr ( 0 , length ) + ch + buffer . substr ( length ++ ) ;
351+ }
297352 } ;
298353
299- process . stdin . on ( 'data ' , callback ) ;
354+ process . stdin . on ( 'keypress ' , callback ) ;
300355 } ) ;
301356}
302357
@@ -611,7 +666,7 @@ function finalActionValidate(s: string): boolean {
611666const finalAction = ( doReboot ? 'R' : doLaunch ? 'L' : 'N' ) ;
612667const finalOptions = '(l/r/n/)' . replace ( finalAction . toLowerCase ( ) , finalAction ) ;
613668
614- const questions = [
669+ let questions = [
615670 { prompt : 'Perform initial update/upgrade?' , ask : true , yn : true , deflt : doUpdateUpgrade ? 'Y' : 'N' , validate : upgradeValidate } ,
616671 { prompt : 'Perform npm install? (N option for debug only)' , ask : true , yn : true , deflt : doNpmI ? 'Y' : 'N' , validate : npmIValidate } ,
617672 { name : 'AWC_PORT' , prompt : 'HTTP server port.' , ask : true , validate : portValidate } ,
@@ -654,6 +709,9 @@ if (NO_MORE_DARK_SKY) {
654709 questions . splice ( 7 , 1 ) ;
655710}
656711
712+ if ( noStop )
713+ questions = questions . slice ( 0 , - 1 ) ;
714+
657715async function promptForConfiguration ( ) : Promise < void > {
658716 console . log ( chalk . cyan ( sol + '- Configuration -' ) ) ;
659717
@@ -676,9 +734,13 @@ async function promptForConfiguration(): Promise<void> {
676734 write ( ': ' ) ;
677735 }
678736
679- const response = await readLine ( ) ;
737+ const response = await readUserInput ( ) ;
680738
681- if ( response ) {
739+ if ( response === '\x18' ) {
740+ i = Math . max ( i - 2 , - 1 ) ;
741+ continue ;
742+ }
743+ else if ( response ) {
682744 const validation = q . validate ? q . validate ( response ) : true ;
683745
684746 if ( isString ( validation ) )
@@ -832,7 +894,7 @@ async function doServiceDeployment(): Promise<void> {
832894 showStep ( ) ;
833895 write ( 'Create or redeploy weatherService' + trailingSpace ) ;
834896 await monitorProcess ( spawn ( 'cp' , [ serviceSrc , serviceDst ] , { shell : true } ) , spin , ErrorMode . ANY_ERROR ) ;
835- await monitorProcess ( spawn ( 'chmod' , [ '+x' , serviceDst ] , { shell : true } ) , spin , ErrorMode . ANY_ERROR ) ;
897+ await monitorProcess ( spawn ( 'chmod' , [ '+x' , serviceDstFull ] , { shell : true } ) , spin , ErrorMode . ANY_ERROR ) ;
836898
837899 const settingsText =
838900 `# If you edit AWC_PORT below, be sure to update\n# ${ userHome } /${ autostartDst } /autostart accordingly.\n` +
@@ -897,7 +959,12 @@ async function doServiceDeployment(): Promise<void> {
897959 { shell : true } ) , spin , ErrorMode . ANY_ERROR ) ;
898960 await monitorProcess ( spawn ( 'chmod' , uid , [ '+x' , autostartDir + '/autostart*' ] ,
899961 { shell : true } ) , spin , ErrorMode . ANY_ERROR ) ;
900- await monitorProcess ( spawn ( 'service' , [ 'weatherService' , 'start' ] ) , spin ) ;
962+
963+ if ( noStop )
964+ console . log ( backspace + trailingSpace + '\n\nReboot to complete set-up.' ) ;
965+ else
966+ await monitorProcess ( spawn ( 'service' , [ 'weatherService' , 'start' ] ) , spin ) ;
967+
901968 stepDone ( ) ;
902969}
903970
@@ -910,7 +977,7 @@ async function doServiceDeployment(): Promise<void> {
910977
911978 if ( isRaspberryPi && ( nodeVersion < 10 || nodeVersion > 14 ) ) {
912979 console . error ( chalk . redBright ( `Node.js version 10.x-14.x required. Version ${ nodeVersionStr } found.` ) ) ;
913- process . exit ( 0 ) ;
980+ process . exit ( 1 ) ;
914981 }
915982
916983 if ( treatAsRaspberryPi && ! isRaspberryPi ) {
@@ -919,7 +986,7 @@ async function doServiceDeployment(): Promise<void> {
919986
920987 if ( ! isDebian || ! isLxde ) {
921988 console . error ( chalk . redBright ( '--tarp option (Treat As Raspberry Pi) only available for Linux Debian with LXDE' ) ) ;
922- process . exit ( 0 ) ;
989+ process . exit ( 1 ) ;
923990 }
924991 }
925992
@@ -929,6 +996,7 @@ async function doServiceDeployment(): Promise<void> {
929996 if ( interactive )
930997 await promptForConfiguration ( ) ;
931998
999+ totalSteps += noStop ? 0 : 1 ;
9321000 totalSteps += ( doNpmI || ! fs . existsSync ( 'node_modules' ) || ! fs . existsSync ( 'package-lock.json' ) ) ? 1 : 0 ;
9331001 totalSteps += ( doNpmI || ! fs . existsSync ( 'server/node_modules' ) || ! fs . existsSync ( 'server/package-lock.json' ) ) ? 1 : 0 ;
9341002 totalSteps += doAcu ? 1 : 0 ;
@@ -945,10 +1013,34 @@ async function doServiceDeployment(): Promise<void> {
9451013 if ( doDedicated ) {
9461014 totalSteps += 9 + ( doUpdateUpgrade ? 1 : 0 ) ;
9471015 console . log ( chalk . cyan ( sol + '- Dedicated device setup -' ) ) ;
948- showStep ( ) ;
949- write ( 'Stopping weatherService if currently running' + trailingSpace ) ;
950- await monitorProcess ( spawn ( 'service' , [ 'weatherService' , 'stop' ] ) , spin , ErrorMode . NO_ERRORS ) ;
951- stepDone ( ) ;
1016+
1017+ if ( ! noStop ) {
1018+ showStep ( ) ;
1019+ write ( 'Stopping weatherService if currently running' + trailingSpace ) ;
1020+
1021+ try {
1022+ await monitorProcess ( spawn ( 'service' , [ 'weatherService' , 'stop' ] ) , spin , ErrorMode . ANY_ERROR ) ;
1023+ }
1024+ catch ( err ) {
1025+ const msg = err . message || err . toString ( ) ;
1026+
1027+ // Grief from polkit?
1028+ if ( / I n t e r a c t i v e a u t h e n t i c a t i o n r e q u i r e d / i. test ( msg ) ) {
1029+ console . log ( backspace + trailingSpace ) ;
1030+ console . error ( err ) ;
1031+ console . log ( '\npolkit is requiring interactive authentication to stop weatherService.' ) ;
1032+ console . log ( 'Please enter "sudo service stop weatherService" at the prompt below.' ) ;
1033+ console . log ( '\nWhen that is done, restart this installation with either: ' ) ;
1034+ console . log ( ' sudo ./build.sh --nostop -i (for interactive set-up)' ) ;
1035+ console . log ( ' sudo ./build.sh --nostop -ddev (for automated set-up)' ) ;
1036+ process . exit ( 1 ) ;
1037+ }
1038+ // TODO: else? Probably just weatherService not installed yet
1039+ // Might check /unrecognized service|not loaded/ to be more certain.
1040+ }
1041+
1042+ stepDone ( ) ;
1043+ }
9521044
9531045 if ( doUpdateUpgrade ) {
9541046 showStep ( ) ;
@@ -965,6 +1057,7 @@ async function doServiceDeployment(): Promise<void> {
9651057 for ( let i = 0 ; i < 2 ; ++ i ) {
9661058 try {
9671059 await install ( 'forever' , true , false , i > 0 ) ;
1060+ break ;
9681061 }
9691062 catch {
9701063 if ( i > 0 )
@@ -1041,8 +1134,10 @@ async function doServiceDeployment(): Promise<void> {
10411134 showStep ( ) ;
10421135 write ( 'Press any key to stop reboot:' + trailingSpace ) ;
10431136
1044- if ( ! ( await sleep ( 3000 , spin , true ) ) )
1137+ if ( ! ( await sleep ( 5000 , spin , true ) ) ) {
1138+ console . log ( ) ;
10451139 exec ( 'reboot' ) ;
1140+ }
10461141 else
10471142 console . log ( ) ;
10481143 }
0 commit comments