@@ -20,6 +20,7 @@ import (
20
20
"encoding/json"
21
21
"flag"
22
22
"fmt"
23
+ "io"
23
24
"io/ioutil"
24
25
"math/rand"
25
26
"os"
@@ -35,12 +36,15 @@ import (
35
36
"github.com/pkg/errors"
36
37
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
37
38
"github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil"
39
+ "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
38
40
"github.com/pulumi/pulumi/sdk/v3/go/common/util/executable"
39
41
"github.com/pulumi/pulumi/sdk/v3/go/common/util/logging"
40
42
"github.com/pulumi/pulumi/sdk/v3/go/common/util/rpcutil"
41
43
"github.com/pulumi/pulumi/sdk/v3/go/common/version"
42
44
pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go"
43
45
"google.golang.org/grpc"
46
+ "google.golang.org/grpc/credentials/insecure"
47
+ "google.golang.org/protobuf/types/known/structpb"
44
48
)
45
49
46
50
// A exit-code we recognize when the nodejs process exits. If we see this error, there's no
@@ -150,6 +154,22 @@ func newLanguageHost(exec, engineAddress, tracing string, binary string) pulumir
150
154
}
151
155
}
152
156
157
+ func (host * dotnetLanguageHost ) connectToEngine () (pulumirpc.EngineClient , io.Closer , error ) {
158
+ // Make a connection to the real engine that we will log messages to.
159
+ conn , err := grpc .Dial (
160
+ host .engineAddress ,
161
+ grpc .WithTransportCredentials (insecure .NewCredentials ()),
162
+ rpcutil .GrpcChannelOptions (),
163
+ )
164
+ if err != nil {
165
+ return nil , nil , fmt .Errorf ("language host could not make connection to engine: %w" , err )
166
+ }
167
+
168
+ // Make a client around that connection.
169
+ engineClient := pulumirpc .NewEngineClient (conn )
170
+ return engineClient , conn , nil
171
+ }
172
+
153
173
// GetRequiredPlugins computes the complete set of anticipated plugins required by a program.
154
174
func (host * dotnetLanguageHost ) GetRequiredPlugins (
155
175
ctx context.Context ,
@@ -162,19 +182,11 @@ func (host *dotnetLanguageHost) GetRequiredPlugins(
162
182
return & pulumirpc.GetRequiredPluginsResponse {}, nil
163
183
}
164
184
165
- // Make a connection to the real engine that we will log messages to.
166
- conn , err := grpc .Dial (
167
- host .engineAddress ,
168
- grpc .WithInsecure (),
169
- rpcutil .GrpcChannelOptions (),
170
- )
185
+ engineClient , closer , err := host .connectToEngine ()
171
186
if err != nil {
172
- return nil , errors . Wrapf ( err , "language host could not make connection to engine" )
187
+ return nil , err
173
188
}
174
-
175
- // Make a client around that connection. We can then make our own server that will act as a
176
- // monitor for the sdk and forward to the real monitor.
177
- engineClient := pulumirpc .NewEngineClient (conn )
189
+ defer contract .IgnoreClose (closer )
178
190
179
191
// First do a `dotnet build`. This will ensure that all the nuget dependencies of the project
180
192
// are restored and locally available for us.
@@ -547,8 +559,47 @@ func (w *logWriter) LogToUser(val string) (int, error) {
547
559
return len (val ), nil
548
560
}
549
561
562
+ func (host * dotnetLanguageHost ) buildDll () (string , error ) {
563
+ // If we are running from source, we need to build the project.
564
+ // Run the `dotnet build` command. Importantly, report the output of this to the user
565
+ // (ephemerally) as it is happening so they're aware of what's going on and can see the progress
566
+ // of things.
567
+ args := []string {"build" , "-nologo" , "-o" , "bin/pulumi-debugging" }
568
+
569
+ cmd := exec .Command (host .exec , args ... )
570
+ err := cmd .Run ()
571
+ if err != nil {
572
+ return "" , errors .Wrapf (err , "failed to build project: %v" , err )
573
+ }
574
+
575
+ var binaryPath string
576
+ err = filepath .WalkDir ("." , func (path string , d os.DirEntry , err error ) error {
577
+ if err != nil {
578
+ return err
579
+ }
580
+
581
+ if name , ok := strings .CutSuffix (d .Name (), ".csproj" ); ok {
582
+ binaryPath = filepath .Join ("bin" , "pulumi-debugging" , name + ".dll" )
583
+ return filepath .SkipAll
584
+ }
585
+ return nil
586
+ })
587
+
588
+ return binaryPath , err
589
+ }
590
+
550
591
// Run is the RPC endpoint for LanguageRuntimeServer::Run
551
592
func (host * dotnetLanguageHost ) Run (ctx context.Context , req * pulumirpc.RunRequest ) (* pulumirpc.RunResponse , error ) {
593
+ binaryPath := host .binary
594
+ if req .GetAttachDebugger () && host .binary == "" {
595
+ binaryPath , err := host .buildDll ()
596
+ if err != nil {
597
+ return nil , err
598
+ }
599
+ if binaryPath == "" {
600
+ return nil , errors .New ("failed to find .csproj file, and could not start debugging" )
601
+ }
602
+ }
552
603
config , err := host .constructConfig (req )
553
604
if err != nil {
554
605
err = errors .Wrap (err , "failed to serialize configuration" )
@@ -564,12 +615,12 @@ func (host *dotnetLanguageHost) Run(ctx context.Context, req *pulumirpc.RunReque
564
615
args := []string {}
565
616
566
617
switch {
567
- case host . binary != "" && strings .HasSuffix (host . binary , ".dll" ):
618
+ case binaryPath != "" && strings .HasSuffix (binaryPath , ".dll" ):
568
619
// Portable pre-compiled dll: run `dotnet <name>.dll`
569
- args = append (args , host . binary )
570
- case host . binary != "" :
620
+ args = append (args , binaryPath )
621
+ case binaryPath != "" :
571
622
// Self-contained executable: run it directly.
572
- executable = host . binary
623
+ executable = binaryPath
573
624
default :
574
625
// Run from source.
575
626
args = append (args , "run" )
@@ -591,13 +642,36 @@ func (host *dotnetLanguageHost) Run(ctx context.Context, req *pulumirpc.RunReque
591
642
logging .V (5 ).Infoln ("Language host launching process: " , host .exec , commandStr )
592
643
}
593
644
645
+ cmd := exec .Command (executable , args ... ) // nolint: gas // intentionally running dynamic program name.
646
+
594
647
// Now simply spawn a process to execute the requested program, wiring up stdout/stderr directly.
595
648
var errResult string
596
- cmd := exec .Command (executable , args ... ) // nolint: gas // intentionally running dynamic program name.
597
649
cmd .Stdout = os .Stdout
598
650
cmd .Stderr = os .Stderr
599
651
cmd .Env = host .constructEnv (req , config , configSecretKeys )
600
- if err := cmd .Run (); err != nil {
652
+ if err := cmd .Start (); err != nil {
653
+ return nil , err
654
+ }
655
+
656
+ if req .GetAttachDebugger () {
657
+ engineClient , closer , err := host .connectToEngine ()
658
+ if err != nil {
659
+ return nil , err
660
+ }
661
+ defer contract .IgnoreClose (closer )
662
+
663
+ ctx , cancel := context .WithCancel (ctx )
664
+ defer cancel ()
665
+ go func () {
666
+ err = startDebugging (ctx , engineClient , cmd )
667
+ if err != nil {
668
+ // kill the program if we can't start debugging.
669
+ logging .Errorf ("Unable to start debugging: %v" , err )
670
+ contract .IgnoreError (cmd .Process .Kill ())
671
+ }
672
+ }()
673
+ }
674
+ if err := cmd .Wait (); err != nil {
601
675
if exiterr , ok := err .(* exec.ExitError ); ok {
602
676
// If the program ran, but exited with a non-zero error code. This will happen often, since user
603
677
// errors will trigger this. So, the error message should look as nice as possible.
@@ -625,6 +699,31 @@ func (host *dotnetLanguageHost) Run(ctx context.Context, req *pulumirpc.RunReque
625
699
return & pulumirpc.RunResponse {Error : errResult }, nil
626
700
}
627
701
702
+ func startDebugging (ctx context.Context , engineClient pulumirpc.EngineClient , cmd * exec.Cmd ) error {
703
+ // wait for the debugger to be ready
704
+ ctx , _ = context .WithTimeoutCause (ctx , 1 * time .Minute , errors .New ("debugger startup timed out" ))
705
+ // wait for the debugger to be ready
706
+
707
+ debugConfig , err := structpb .NewStruct (map [string ]interface {}{
708
+ "name" : "Pulumi: Program (Dotnet)" ,
709
+ "type" : "coreclr" ,
710
+ "request" : "attach" ,
711
+ "processId" : cmd .Process .Pid ,
712
+ })
713
+ if err != nil {
714
+ return err
715
+ }
716
+ _ , err = engineClient .StartDebugging (ctx , & pulumirpc.StartDebuggingRequest {
717
+ Config : debugConfig ,
718
+ Message : fmt .Sprintf ("on process id %d" , cmd .Process .Pid ),
719
+ })
720
+ if err != nil {
721
+ return fmt .Errorf ("unable to start debugging: %w" , err )
722
+ }
723
+
724
+ return nil
725
+ }
726
+
628
727
func (host * dotnetLanguageHost ) constructEnv (req * pulumirpc.RunRequest , config , configSecretKeys string ) []string {
629
728
env := os .Environ ()
630
729
@@ -646,6 +745,7 @@ func (host *dotnetLanguageHost) constructEnv(req *pulumirpc.RunRequest, config,
646
745
maybeAppendEnv ("tracing" , host .tracing )
647
746
maybeAppendEnv ("config" , config )
648
747
maybeAppendEnv ("config_secret_keys" , configSecretKeys )
748
+ maybeAppendEnv ("attach_debugger" , fmt .Sprint (req .GetAttachDebugger ()))
649
749
650
750
return env
651
751
}
0 commit comments