2020-05-10 10:15:29 +02:00
import { field , Level , logger } from "@coder/logger"
2020-05-10 07:19:32 +02:00
import * as fs from "fs-extra"
import yaml from "js-yaml"
2020-05-14 12:08:37 +02:00
import * as os from "os"
2020-02-06 20:11:38 +01:00
import * as path from "path"
2020-02-06 19:29:19 +01:00
import { Args as VsArgs } from "../../lib/vscode/src/vs/server/ipc"
2020-02-06 20:11:38 +01:00
import { AuthType } from "./http"
2020-05-10 10:15:29 +02:00
import { generatePassword , humanPath , paths } from "./util"
2020-02-06 19:29:19 +01:00
2020-02-07 01:26:07 +01:00
export class Optional < T > {
public constructor ( public readonly value? : T ) { }
}
2020-02-19 18:06:32 +01:00
export enum LogLevel {
Trace = "trace" ,
Debug = "debug" ,
Info = "info" ,
Warn = "warn" ,
Error = "error" ,
}
2020-02-07 01:26:07 +01:00
export class OptionalString extends Optional < string > { }
2020-02-06 19:29:19 +01:00
export interface Args extends VsArgs {
2020-05-10 07:19:32 +02:00
readonly config? : string
2020-02-07 01:26:07 +01:00
readonly auth? : AuthType
2020-05-10 08:35:15 +02:00
readonly password? : string
2020-02-07 01:26:07 +01:00
readonly cert? : OptionalString
readonly "cert-key" ? : string
2020-02-18 19:24:12 +01:00
readonly "disable-telemetry" ? : boolean
2020-02-07 01:26:07 +01:00
readonly help? : boolean
readonly host? : string
readonly json? : boolean
2020-02-19 18:06:32 +01:00
log? : LogLevel
2020-02-07 01:26:07 +01:00
readonly open? : boolean
readonly port? : number
2020-04-27 15:22:52 +02:00
readonly "bind-addr" ? : string
2020-02-07 01:26:07 +01:00
readonly socket? : string
readonly version? : boolean
2020-03-05 17:39:42 +01:00
readonly force? : boolean
2020-02-18 19:24:12 +01:00
readonly "list-extensions" ? : boolean
readonly "install-extension" ? : string [ ]
2020-03-13 19:17:59 +01:00
readonly "show-versions" ? : boolean
2020-02-18 19:24:12 +01:00
readonly "uninstall-extension" ? : string [ ]
2020-03-23 18:08:50 +01:00
readonly "proxy-domain" ? : string [ ]
2020-02-21 00:36:38 +01:00
readonly locale? : string
2020-02-07 01:26:07 +01:00
readonly _ : string [ ]
2020-08-27 20:06:21 +02:00
readonly "reuse-window" ? : boolean
readonly "new-window" ? : boolean
2020-09-09 01:39:17 +02:00
readonly "expose" ? : OptionalString
2020-02-07 01:26:07 +01:00
}
interface Option < T > {
type : T
/ * *
* Short flag for the option .
* /
short? : string
/ * *
* Whether the option is a path and should be resolved .
* /
path? : boolean
/ * *
* Description of the option . Leave blank to hide the option .
* /
description? : string
}
type OptionType < T > = T extends boolean
? "boolean"
: T extends OptionalString
? typeof OptionalString
2020-02-19 18:06:32 +01:00
: T extends LogLevel
? typeof LogLevel
2020-02-07 01:26:07 +01:00
: T extends AuthType
? typeof AuthType
: T extends number
? "number"
: T extends string
? "string"
: T extends string [ ]
? "string[]"
: "unknown"
type Options < T > = {
[ P in keyof T ] : Option < OptionType < T [ P ] > >
2020-02-06 19:29:19 +01:00
}
2020-02-07 01:26:07 +01:00
const options : Options < Required < Args > > = {
auth : { type : AuthType , description : "The type of authentication to use." } ,
2020-05-10 10:15:29 +02:00
password : {
type : "string" ,
description : "The password for password authentication (can only be passed in via $PASSWORD or the config file)." ,
} ,
2020-02-07 01:26:07 +01:00
cert : {
type : OptionalString ,
path : true ,
description : "Path to certificate. Generated if no path is provided." ,
} ,
"cert-key" : { type : "string" , path : true , description : "Path to certificate key when using non-generated cert." } ,
2020-02-18 19:24:12 +01:00
"disable-telemetry" : { type : "boolean" , description : "Disable telemetry." } ,
2020-02-07 01:26:07 +01:00
help : { type : "boolean" , short : "h" , description : "Show this output." } ,
json : { type : "boolean" } ,
2020-02-19 18:06:32 +01:00
open : { type : "boolean" , description : "Open in browser on startup. Does not work remotely." } ,
2020-04-27 15:22:52 +02:00
2020-05-10 10:15:29 +02:00
"bind-addr" : {
type : "string" ,
description : "Address to bind to in host:port. You can also use $PORT to override the port." ,
} ,
2020-04-27 15:22:52 +02:00
2020-05-10 08:35:15 +02:00
config : {
type : "string" ,
2020-05-10 10:15:29 +02:00
description : "Path to yaml config file. Every flag maps directly to a key in the config file." ,
2020-05-10 08:35:15 +02:00
} ,
2020-05-10 07:19:32 +02:00
2020-04-27 15:22:52 +02:00
// These two have been deprecated by bindAddr.
host : { type : "string" , description : "" } ,
port : { type : "number" , description : "" } ,
2020-04-29 00:29:25 +02:00
socket : { type : "string" , path : true , description : "Path to a socket (bind-addr will be ignored)." } ,
2020-02-07 01:26:07 +01:00
version : { type : "boolean" , short : "v" , description : "Display version information." } ,
_ : { type : "string[]" } ,
"user-data-dir" : { type : "string" , path : true , description : "Path to the user data directory." } ,
"extensions-dir" : { type : "string" , path : true , description : "Path to the extensions directory." } ,
"builtin-extensions-dir" : { type : "string" , path : true } ,
"extra-extensions-dir" : { type : "string[]" , path : true } ,
"extra-builtin-extensions-dir" : { type : "string[]" , path : true } ,
2020-03-13 19:21:46 +01:00
"list-extensions" : { type : "boolean" , description : "List installed VS Code extensions." } ,
force : { type : "boolean" , description : "Avoid prompts when installing VS Code extensions." } ,
2020-08-14 01:08:35 +02:00
"install-extension" : {
type : "string[]" ,
description :
"Install or update a VS Code extension by id or vsix. The identifier of an extension is `${publisher}.${name}`. To install a specific version provide `@${version}`. For example: 'vscode.csharp@1.2.3'." ,
} ,
2020-08-26 21:18:40 +02:00
"enable-proposed-api" : {
type : "string[]" ,
description :
"Enable proposed API features for extensions. Can receive one or more extension IDs to enable individually." ,
} ,
2020-03-13 19:21:46 +01:00
"uninstall-extension" : { type : "string[]" , description : "Uninstall a VS Code extension by id." } ,
"show-versions" : { type : "boolean" , description : "Show VS Code extension versions." } ,
2020-03-23 18:08:50 +01:00
"proxy-domain" : { type : "string[]" , description : "Domain used for proxying ports." } ,
2020-02-07 01:26:07 +01:00
2020-08-27 20:06:21 +02:00
"new-window" : {
type : "boolean" ,
short : "n" ,
description : "Force to open a new window. (use with open-in)" ,
} ,
"reuse-window" : {
type : "boolean" ,
short : "r" ,
description : "Force to open a file or folder in an already opened window. (use with open-in)" ,
} ,
2020-02-20 19:38:46 +01:00
locale : { type : "string" } ,
2020-02-19 18:06:32 +01:00
log : { type : LogLevel } ,
2020-02-07 01:26:07 +01:00
verbose : { type : "boolean" , short : "vvv" , description : "Enable verbose logging." } ,
2020-09-09 01:39:17 +02:00
"expose" : { type : OptionalString , description : "Expose via Coder Cloud with the passed name. You'll get a URL" +
"like https://myname.coder-cloud.com at which you can easily access your code-server instance. Authorization is done via GitHub." } ,
2020-02-07 01:26:07 +01:00
}
export const optionDescriptions = ( ) : string [ ] = > {
const entries = Object . entries ( options ) . filter ( ( [ , v ] ) = > ! ! v . description )
const widths = entries . reduce (
( prev , [ k , v ] ) = > ( {
long : k.length > prev . long ? k.length : prev.long ,
short : v.short && v . short . length > prev . short ? v.short.length : prev.short ,
} ) ,
2020-02-15 01:46:00 +01:00
{ short : 0 , long : 0 } ,
2020-02-07 01:26:07 +01:00
)
return entries . map (
( [ k , v ] ) = >
` ${ " " . repeat ( widths . short - ( v . short ? v.short.length : 0 ) ) } ${ v . short ? ` - ${ v . short } ` : " " } -- ${ k } ${ " " . repeat (
2020-02-15 01:46:00 +01:00
widths . long - k . length ,
) } $ { v . description } $ { typeof v . type === "object" ? ` [ ${ Object . values ( v . type ) . join ( ", " ) } ] ` : "" } ` ,
2020-02-07 01:26:07 +01:00
)
}
2020-05-19 06:39:57 +02:00
export const parse = (
2020-05-10 10:15:29 +02:00
argv : string [ ] ,
opts ? : {
configFile : string
} ,
2020-05-19 06:39:57 +02:00
) : Args = > {
2020-05-10 10:15:29 +02:00
const error = ( msg : string ) : Error = > {
if ( opts ? . configFile ) {
msg = ` error reading ${ opts . configFile } : ${ msg } `
}
return new Error ( msg )
}
2020-02-07 01:26:07 +01:00
const args : Args = { _ : [ ] }
let ended = false
for ( let i = 0 ; i < argv . length ; ++ i ) {
const arg = argv [ i ]
// -- signals the end of option parsing.
2020-08-04 22:08:45 +02:00
if ( ! ended && arg === "--" ) {
2020-02-07 01:26:07 +01:00
ended = true
continue
}
2020-02-06 20:11:38 +01:00
2020-02-07 01:26:07 +01:00
// Options start with a dash and require a value if non-boolean.
if ( ! ended && arg . startsWith ( "-" ) ) {
let key : keyof Args | undefined
2020-02-19 17:54:23 +01:00
let value : string | undefined
2020-02-07 01:26:07 +01:00
if ( arg . startsWith ( "--" ) ) {
2020-02-19 17:54:23 +01:00
const split = arg . replace ( /^--/ , "" ) . split ( "=" , 2 )
key = split [ 0 ] as keyof Args
value = split [ 1 ]
2020-02-07 01:26:07 +01:00
} else {
const short = arg . replace ( /^-/ , "" )
const pair = Object . entries ( options ) . find ( ( [ , v ] ) = > v . short === short )
if ( pair ) {
key = pair [ 0 ] as keyof Args
}
}
if ( ! key || ! options [ key ] ) {
2020-05-10 10:15:29 +02:00
throw error ( ` Unknown option ${ arg } ` )
}
if ( key === "password" && ! opts ? . configFile ) {
throw new Error ( "--password can only be set in the config file or passed in via $PASSWORD" )
2020-02-07 01:26:07 +01:00
}
const option = options [ key ]
if ( option . type === "boolean" ) {
; ( args [ key ] as boolean ) = true
continue
}
2020-02-19 17:54:23 +01:00
// Might already have a value if it was the --long=value format.
if ( typeof value === "undefined" ) {
// A value is only valid if it doesn't look like an option.
value = argv [ i + 1 ] && ! argv [ i + 1 ] . startsWith ( "-" ) ? argv [ ++ i ] : undefined
}
2020-02-07 01:26:07 +01:00
if ( ! value && option . type === OptionalString ) {
; ( args [ key ] as OptionalString ) = new OptionalString ( value )
continue
} else if ( ! value ) {
2020-05-10 10:15:29 +02:00
throw error ( ` -- ${ key } requires a value ` )
}
2020-08-04 22:08:45 +02:00
if ( option . type === OptionalString && value === "false" ) {
2020-05-10 10:15:29 +02:00
continue
2020-02-07 01:26:07 +01:00
}
if ( option . path ) {
value = path . resolve ( value )
}
switch ( option . type ) {
case "string" :
; ( args [ key ] as string ) = value
break
case "string[]" :
if ( ! args [ key ] ) {
; ( args [ key ] as string [ ] ) = [ ]
}
; ( args [ key ] as string [ ] ) . push ( value )
break
case "number" :
; ( args [ key ] as number ) = parseInt ( value , 10 )
if ( isNaN ( args [ key ] as number ) ) {
2020-05-10 10:15:29 +02:00
throw error ( ` -- ${ key } must be a number ` )
2020-02-07 01:26:07 +01:00
}
break
case OptionalString :
; ( args [ key ] as OptionalString ) = new OptionalString ( value )
break
default : {
2020-04-28 23:39:01 +02:00
if ( ! Object . values ( option . type ) . includes ( value ) ) {
2020-05-10 10:15:29 +02:00
throw error ( ` -- ${ key } valid values: [ ${ Object . values ( option . type ) . join ( ", " ) } ] ` )
2020-02-07 01:26:07 +01:00
}
; ( args [ key ] as string ) = value
break
}
}
continue
}
// Everything else goes into _.
args . _ . push ( arg )
2020-02-06 20:11:38 +01:00
}
2020-02-07 01:26:07 +01:00
logger . debug ( "parsed command line" , field ( "args" , args ) )
2020-04-28 23:39:01 +02:00
// --verbose takes priority over --log and --log takes priority over the
// environment variable.
if ( args . verbose ) {
args . log = LogLevel . Trace
} else if (
! args . log &&
process . env . LOG_LEVEL &&
Object . values ( LogLevel ) . includes ( process . env . LOG_LEVEL as LogLevel )
) {
2020-02-19 18:06:32 +01:00
args . log = process . env . LOG_LEVEL as LogLevel
2020-02-06 19:29:19 +01:00
}
2020-02-07 01:26:07 +01:00
2020-04-28 23:39:01 +02:00
// Sync --log, --verbose, the environment variable, and logger level.
if ( args . log ) {
process . env . LOG_LEVEL = args . log
}
2020-02-07 01:26:07 +01:00
switch ( args . log ) {
2020-02-19 18:06:32 +01:00
case LogLevel . Trace :
2020-02-07 01:26:07 +01:00
logger . level = Level . Trace
2020-04-28 23:39:01 +02:00
args . verbose = true
2020-02-07 01:26:07 +01:00
break
2020-02-19 18:06:32 +01:00
case LogLevel . Debug :
2020-02-07 01:26:07 +01:00
logger . level = Level . Debug
2020-05-19 06:39:57 +02:00
args . verbose = false
2020-02-07 01:26:07 +01:00
break
2020-02-19 18:06:32 +01:00
case LogLevel . Info :
2020-02-07 01:26:07 +01:00
logger . level = Level . Info
2020-05-19 06:39:57 +02:00
args . verbose = false
2020-02-07 01:26:07 +01:00
break
2020-02-19 18:06:32 +01:00
case LogLevel . Warn :
2020-02-07 01:26:07 +01:00
logger . level = Level . Warning
2020-05-19 06:39:57 +02:00
args . verbose = false
2020-02-07 01:26:07 +01:00
break
2020-02-19 18:06:32 +01:00
case LogLevel . Error :
2020-02-07 01:26:07 +01:00
logger . level = Level . Error
2020-05-19 06:39:57 +02:00
args . verbose = false
2020-02-07 01:26:07 +01:00
break
}
2020-05-19 06:39:57 +02:00
return args
}
export async function setDefaults ( args : Args ) : Promise < Args > {
args = { . . . args }
2020-02-07 01:26:07 +01:00
if ( ! args [ "user-data-dir" ] ) {
2020-05-14 12:08:37 +02:00
await copyOldMacOSDataDir ( )
2020-05-10 07:35:42 +02:00
args [ "user-data-dir" ] = paths . data
2020-02-07 01:26:07 +01:00
}
if ( ! args [ "extensions-dir" ] ) {
args [ "extensions-dir" ] = path . join ( args [ "user-data-dir" ] , "extensions" )
}
return args
2020-02-06 19:29:19 +01:00
}
2020-05-10 07:19:32 +02:00
2020-05-10 10:15:29 +02:00
async function defaultConfigFile ( ) : Promise < string > {
return ` bind-addr: 127.0.0.1:8080
2020-05-10 07:35:42 +02:00
auth : password
2020-05-10 10:15:29 +02:00
password : $ { await generatePassword ( ) }
cert : false
`
}
/ * *
* Reads the code - server yaml config file and returns it as Args .
*
* @param configPath Read the config from configPath instead of $CODE_SERVER_CONFIG or the default .
* /
export async function readConfigFile ( configPath? : string ) : Promise < Args > {
if ( ! configPath ) {
configPath = process . env . CODE_SERVER_CONFIG
if ( ! configPath ) {
configPath = path . join ( paths . config , "config.yaml" )
}
}
2020-05-10 07:19:32 +02:00
if ( ! ( await fs . pathExists ( configPath ) ) ) {
2020-05-10 10:15:29 +02:00
await fs . outputFile ( configPath , await defaultConfigFile ( ) )
logger . info ( ` Wrote default config file to ${ humanPath ( configPath ) } ` )
2020-05-10 07:19:32 +02:00
}
2020-08-27 22:04:37 +02:00
if ( ! process . env . CODE_SERVER_PARENT_PID && ! process . env . VSCODE_IPC_HOOK_CLI ) {
2020-06-03 20:02:55 +02:00
logger . info ( ` Using config file ${ humanPath ( configPath ) } ` )
}
2020-05-10 07:35:42 +02:00
2020-05-10 07:19:32 +02:00
const configFile = await fs . readFile ( configPath )
const config = yaml . safeLoad ( configFile . toString ( ) , {
2020-05-10 10:15:29 +02:00
filename : configPath ,
2020-05-10 07:19:32 +02:00
} )
2020-08-26 20:21:37 +02:00
if ( ! config || typeof config === "string" ) {
throw new Error ( ` invalid config: ${ config } ` )
}
2020-05-10 07:19:32 +02:00
// We convert the config file into a set of flags.
// This is a temporary measure until we add a proper CLI library.
2020-05-10 07:35:42 +02:00
const configFileArgv = Object . entries ( config ) . map ( ( [ optName , opt ] ) = > {
2020-05-10 10:15:29 +02:00
if ( opt === true ) {
2020-05-10 07:35:42 +02:00
return ` -- ${ optName } `
}
return ` -- ${ optName } = ${ opt } `
} )
2020-05-19 06:39:57 +02:00
const args = parse ( configFileArgv , {
2020-05-10 10:15:29 +02:00
configFile : configPath ,
} )
return {
. . . args ,
config : configPath ,
}
}
2020-05-10 07:19:32 +02:00
2020-05-10 10:15:29 +02:00
function parseBindAddr ( bindAddr : string ) : [ string , number ] {
const u = new URL ( ` http:// ${ bindAddr } ` )
2020-09-30 18:56:49 +02:00
// With the http scheme 80 will be dropped so assume it's 80 if missing. This
// means --bind-addr <addr> without a port will default to 80 as well and not
// the code-server default.
return [ u . hostname , u . port ? parseInt ( u . port , 10 ) : 80 ]
2020-05-10 07:19:32 +02:00
}
2020-05-10 10:15:29 +02:00
interface Addr {
host : string
port : number
}
function bindAddrFromArgs ( addr : Addr , args : Args ) : Addr {
addr = { . . . addr }
if ( args [ "bind-addr" ] ) {
; [ addr . host , addr . port ] = parseBindAddr ( args [ "bind-addr" ] )
2020-05-10 07:19:32 +02:00
}
2020-05-10 10:15:29 +02:00
if ( args . host ) {
addr . host = args . host
2020-05-10 07:19:32 +02:00
}
2020-05-15 03:57:10 +02:00
if ( process . env . PORT ) {
addr . port = parseInt ( process . env . PORT , 10 )
}
2020-05-10 10:15:29 +02:00
if ( args . port !== undefined ) {
addr . port = args . port
}
return addr
}
export function bindAddrFromAllSources ( cliArgs : Args , configArgs : Args ) : [ string , number ] {
let addr : Addr = {
host : "localhost" ,
port : 8080 ,
}
addr = bindAddrFromArgs ( addr , configArgs )
addr = bindAddrFromArgs ( addr , cliArgs )
return [ addr . host , addr . port ]
2020-05-10 07:19:32 +02:00
}
2020-05-14 12:08:37 +02:00
async function copyOldMacOSDataDir ( ) : Promise < void > {
if ( os . platform ( ) !== "darwin" ) {
return
}
if ( await fs . pathExists ( paths . data ) ) {
return
}
// If the old data directory exists, we copy it in.
const oldDataDir = path . join ( os . homedir ( ) , "Library/Application Support" , "code-server" )
if ( await fs . pathExists ( oldDataDir ) ) {
await fs . copy ( oldDataDir , paths . data )
}
}