@@ -19,6 +19,7 @@ import { containerSubstitute } from './variableSubstitution';
1919import { delay } from './async' ;
2020import { Log , LogEvent , LogLevel , makeLog , nullLog } from '../spec-utils/log' ;
2121import { buildProcessTrees , findProcesses , Process , processTreeToString } from './proc' ;
22+ import { installDotfiles } from './dotfiles' ;
2223
2324export enum ResolverProgress {
2425 Begin ,
@@ -44,6 +45,7 @@ export interface ResolverParameters {
4445 env : NodeJS . ProcessEnv ;
4546 cwd : string ;
4647 isLocalContainer : boolean ;
48+ dotfilesConfiguration : DotfilesConfiguration ;
4749 progress : ( current : ResolverProgress ) => void ;
4850 output : Log ;
4951 allowSystemConfigChange : boolean ;
@@ -62,6 +64,7 @@ export interface ResolverParameters {
6264 skipFeatureAutoMapping : boolean ;
6365 skipPostAttach : boolean ;
6466 experimentalImageMetadata : boolean ;
67+ containerSessionDataFolder ?: string ;
6568 skipPersistingCustomizationsFromFeatures : boolean ;
6669}
6770
@@ -185,6 +188,12 @@ export interface ContainerProperties {
185188 launchRootShellServer ?: ( ) => Promise < ShellServer > ;
186189}
187190
191+ export interface DotfilesConfiguration {
192+ repository : string | undefined ;
193+ installCommand : string | undefined ;
194+ targetPath : string ;
195+ }
196+
188197export async function getContainerProperties ( options : {
189198 params : ResolverParameters ;
190199 createdAt : string | undefined ;
@@ -347,6 +356,10 @@ export async function runPostCreateCommands(params: ResolverParameters, containe
347356 return 'skipNonBlocking' ;
348357 }
349358
359+ if ( params . dotfilesConfiguration ) {
360+ await installDotfiles ( params , containerProperties , remoteEnv ) ;
361+ }
362+
350363 if ( stopForPersonalization ) {
351364 return 'stopForPersonalization' ;
352365 }
@@ -513,7 +526,7 @@ async function createFile(shellServer: ShellServer, location: string) {
513526 }
514527}
515528
516- function createFileCommand ( location : string ) {
529+ export function createFileCommand ( location : string ) {
517530 return `test ! -f '${ location } ' && set -o noclobber && mkdir -p '${ path . posix . dirname ( location ) } ' && { > '${ location } ' ; } 2> /dev/null` ;
518531}
519532
@@ -655,22 +668,72 @@ async function patchEtcProfile(params: ResolverParameters, containerProperties:
655668 }
656669}
657670
658- async function probeUserEnv ( params : { defaultUserEnvProbe : UserEnvProbe ; allowSystemConfigChange : boolean ; output : Log } , containerProperties : { shell : string ; remoteExec : ExecFunction ; installFolder ?: string ; env ?: NodeJS . ProcessEnv ; shellServer ?: ShellServer ; launchRootShellServer ?: ( ( ) => Promise < ShellServer > ) ; user ?: string } , config ?: CommonMergedDevContainerConfig ) {
659- const env = await runUserEnvProbe ( params , containerProperties , config , 'cat /proc/self/environ' , '\0' ) ;
671+ async function probeUserEnv ( params : { defaultUserEnvProbe : UserEnvProbe ; allowSystemConfigChange : boolean ; output : Log ; containerSessionDataFolder ?: string } , containerProperties : { shell : string ; remoteExec : ExecFunction ; installFolder ?: string ; env ?: NodeJS . ProcessEnv ; shellServer ?: ShellServer ; launchRootShellServer ?: ( ( ) => Promise < ShellServer > ) ; user ?: string } , config ?: CommonMergedDevContainerConfig ) {
672+ let userEnvProbe = getUserEnvProb ( config , params ) ;
673+ if ( ! userEnvProbe || userEnvProbe === 'none' ) {
674+ return { } ;
675+ }
676+
677+ let env = await readUserEnvFromCache ( userEnvProbe , params , containerProperties . shellServer ) ;
660678 if ( env ) {
661679 return env ;
662680 }
663- params . output . write ( 'userEnvProbe: falling back to printenv' ) ;
664- const env2 = await runUserEnvProbe ( params , containerProperties , config , 'printenv' , '\n' ) ;
665- return env2 || { } ;
681+
682+ params . output . write ( 'userEnvProbe: not found in cache' ) ;
683+ env = await runUserEnvProbe ( userEnvProbe , params , containerProperties , 'cat /proc/self/environ' , '\0' ) ;
684+ if ( ! env ) {
685+ params . output . write ( 'userEnvProbe: falling back to printenv' ) ;
686+ env = await runUserEnvProbe ( userEnvProbe , params , containerProperties , 'printenv' , '\n' ) ;
687+ }
688+
689+ if ( env ) {
690+ await updateUserEnvCache ( env , userEnvProbe , params , containerProperties . shellServer ) ;
691+ }
692+
693+ return env || { } ;
666694}
667695
668- async function runUserEnvProbe ( params : { defaultUserEnvProbe : UserEnvProbe ; allowSystemConfigChange : boolean ; output : Log } , containerProperties : { shell : string ; remoteExec : ExecFunction ; installFolder ?: string ; env ?: NodeJS . ProcessEnv ; shellServer ?: ShellServer ; launchRootShellServer ?: ( ( ) => Promise < ShellServer > ) ; user ?: string } , config : CommonMergedDevContainerConfig | undefined , cmd : string , sep : string ) {
669- let userEnvProbe = config ?. userEnvProbe ;
670- params . output . write ( `userEnvProbe: ${ userEnvProbe || params . defaultUserEnvProbe } ${ userEnvProbe ? '' : ' (default)' } ` ) ;
671- if ( ! userEnvProbe ) {
672- userEnvProbe = params . defaultUserEnvProbe ;
696+ async function readUserEnvFromCache ( userEnvProbe : UserEnvProbe , params : { output : Log ; containerSessionDataFolder ?: string } , shellServer ?: ShellServer ) {
697+ if ( ! shellServer || ! params . containerSessionDataFolder ) {
698+ return undefined ;
699+ }
700+
701+ const cacheFile = getUserEnvCacheFilePath ( userEnvProbe , params . containerSessionDataFolder ) ;
702+ try {
703+ if ( await isFile ( shellServer , cacheFile ) ) {
704+ const { stdout } = await shellServer . exec ( `cat '${ cacheFile } '` ) ;
705+ return JSON . parse ( stdout ) ;
706+ }
673707 }
708+ catch ( e ) {
709+ params . output . write ( `Failed to read/parse user env cache: ${ e } ` , LogLevel . Error ) ;
710+ }
711+
712+ return undefined ;
713+ }
714+
715+ async function updateUserEnvCache ( env : Record < string , string > , userEnvProbe : UserEnvProbe , params : { output : Log ; containerSessionDataFolder ?: string } , shellServer ?: ShellServer ) {
716+ if ( ! shellServer || ! params . containerSessionDataFolder ) {
717+ return ;
718+ }
719+
720+ const cacheFile = getUserEnvCacheFilePath ( userEnvProbe , params . containerSessionDataFolder ) ;
721+ try {
722+ await shellServer . exec ( `mkdir -p '${ path . posix . dirname ( cacheFile ) } ' && cat > '${ cacheFile } ' << 'envJSON'
723+ ${ JSON . stringify ( env , null , '\t' ) }
724+ envJSON
725+ ` ) ;
726+ }
727+ catch ( e ) {
728+ params . output . write ( `Failed to cache user env: ${ e } ` , LogLevel . Error ) ;
729+ }
730+ }
731+
732+ function getUserEnvCacheFilePath ( userEnvProbe : UserEnvProbe , cacheFolder : string ) : string {
733+ return path . posix . join ( cacheFolder , `env-${ userEnvProbe } .json` ) ;
734+ }
735+
736+ async function runUserEnvProbe ( userEnvProbe : UserEnvProbe , params : { allowSystemConfigChange : boolean ; output : Log } , containerProperties : { shell : string ; remoteExec : ExecFunction ; installFolder ?: string ; env ?: NodeJS . ProcessEnv ; shellServer ?: ShellServer ; launchRootShellServer ?: ( ( ) => Promise < ShellServer > ) ; user ?: string } , cmd : string , sep : string ) {
674737 if ( userEnvProbe === 'none' ) {
675738 return { } ;
676739 }
@@ -767,6 +830,15 @@ Merged: ${typeof env.PATH === 'string' ? `'${env.PATH}'` : 'None'}` : ''}`);
767830 }
768831}
769832
833+ function getUserEnvProb ( config : CommonMergedDevContainerConfig | undefined , params : { defaultUserEnvProbe : UserEnvProbe ; allowSystemConfigChange : boolean ; output : Log } ) {
834+ let userEnvProbe = config ?. userEnvProbe ;
835+ params . output . write ( `userEnvProbe: ${ userEnvProbe || params . defaultUserEnvProbe } ${ userEnvProbe ? '' : ' (default)' } ` ) ;
836+ if ( ! userEnvProbe ) {
837+ userEnvProbe = params . defaultUserEnvProbe ;
838+ }
839+ return userEnvProbe ;
840+ }
841+
770842function mergePaths ( shellPath : string , containerPath : string , rootUser : boolean ) {
771843 const result = shellPath . split ( ':' ) ;
772844 let insertAt = 0 ;
0 commit comments