diff --git a/internal/arduino/builder/internal/preprocessor/gcc.go b/internal/arduino/builder/internal/preprocessor/gcc.go index cbf156dfae6..f8d6100d10d 100644 --- a/internal/arduino/builder/internal/preprocessor/gcc.go +++ b/internal/arduino/builder/internal/preprocessor/gcc.go @@ -16,6 +16,7 @@ package preprocessor import ( + "bytes" "context" "errors" "fmt" @@ -77,10 +78,42 @@ func GCC( if err != nil { return Result{}, err } - stdout, stderr, err := proc.RunAndCaptureOutput(ctx) - // Append gcc arguments to stdout - stdout = append([]byte(fmt.Sprintln(strings.Join(args, " "))), stdout...) + stdout := bytes.NewBuffer(nil) + stderr := bytes.NewBuffer(nil) - return Result{args: proc.GetArgs(), stdout: stdout, stderr: stderr}, err + ctx, cancel := context.WithCancel(ctx) + defer cancel() + count := 0 + stderrLimited := writerFunc(func(p []byte) (int, error) { + // Limit the size of the stderr buffer to 100KB + n, err := stderr.Write(p) + count += n + if count > 100*1024 { + fmt.Fprintln(stderr, i18n.Tr("Compiler error output has been truncated.")) + cancel() + } + return n, err + }) + + proc.RedirectStdoutTo(stdout) + proc.RedirectStderrTo(stderrLimited) + + // Append gcc arguments to stdout before running the command + fmt.Fprintln(stdout, strings.Join(args, " ")) + + if err := proc.Start(); err != nil { + return Result{}, err + } + + // Wait for the process to finish + err = proc.WaitWithinContext(ctx) + + return Result{args: proc.GetArgs(), stdout: stdout.Bytes(), stderr: stderr.Bytes()}, err +} + +type writerFunc func(p []byte) (n int, err error) + +func (f writerFunc) Write(p []byte) (n int, err error) { + return f(p) } diff --git a/internal/integrationtest/arduino-cli.go b/internal/integrationtest/arduino-cli.go index f035e792f55..4b2b4850450 100644 --- a/internal/integrationtest/arduino-cli.go +++ b/internal/integrationtest/arduino-cli.go @@ -22,6 +22,7 @@ import ( "errors" "fmt" "io" + "maps" "os" "runtime" "strings" @@ -190,12 +191,16 @@ func (cli *ArduinoCLI) Run(args ...string) ([]byte, []byte, error) { return cli.RunWithCustomEnv(cli.cliEnvVars, args...) } +// RunWithContext executes the given arduino-cli command with the given context and returns the output. +// If the context is canceled, the command is killed. +func (cli *ArduinoCLI) RunWithContext(ctx context.Context, args ...string) ([]byte, []byte, error) { + return cli.RunWithCustomEnvContext(ctx, cli.cliEnvVars, args...) +} + // GetDefaultEnv returns a copy of the default execution env used with the Run method. func (cli *ArduinoCLI) GetDefaultEnv() map[string]string { res := map[string]string{} - for k, v := range cli.cliEnvVars { - res[k] = v - } + maps.Copy(res, cli.cliEnvVars) return res } @@ -324,8 +329,13 @@ func (cli *ArduinoCLI) InstallMockedAvrdude(t *testing.T) { // RunWithCustomEnv executes the given arduino-cli command with the given custom env and returns the output. func (cli *ArduinoCLI) RunWithCustomEnv(env map[string]string, args ...string) ([]byte, []byte, error) { + return cli.RunWithCustomEnvContext(context.Background(), env, args...) +} + +// RunWithCustomEnv executes the given arduino-cli command with the given custom env and returns the output. +func (cli *ArduinoCLI) RunWithCustomEnvContext(ctx context.Context, env map[string]string, args ...string) ([]byte, []byte, error) { var stdoutBuf, stderrBuf bytes.Buffer - err := cli.run(&stdoutBuf, &stderrBuf, nil, env, args...) + err := cli.run(ctx, &stdoutBuf, &stderrBuf, nil, env, args...) errBuf := stderrBuf.Bytes() cli.t.NotContains(string(errBuf), "panic: runtime error:", "arduino-cli panicked") @@ -336,7 +346,7 @@ func (cli *ArduinoCLI) RunWithCustomEnv(env map[string]string, args ...string) ( // RunWithCustomInput executes the given arduino-cli command pushing the given input stream and returns the output. func (cli *ArduinoCLI) RunWithCustomInput(in io.Reader, args ...string) ([]byte, []byte, error) { var stdoutBuf, stderrBuf bytes.Buffer - err := cli.run(&stdoutBuf, &stderrBuf, in, cli.cliEnvVars, args...) + err := cli.run(context.Background(), &stdoutBuf, &stderrBuf, in, cli.cliEnvVars, args...) errBuf := stderrBuf.Bytes() cli.t.NotContains(string(errBuf), "panic: runtime error:", "arduino-cli panicked") @@ -344,7 +354,7 @@ func (cli *ArduinoCLI) RunWithCustomInput(in io.Reader, args ...string) ([]byte, return stdoutBuf.Bytes(), errBuf, err } -func (cli *ArduinoCLI) run(stdoutBuff, stderrBuff io.Writer, stdinBuff io.Reader, env map[string]string, args ...string) error { +func (cli *ArduinoCLI) run(ctx context.Context, stdoutBuff, stderrBuff io.Writer, stdinBuff io.Reader, env map[string]string, args ...string) error { if cli.cliConfigPath != nil { args = append([]string{"--config-file", cli.cliConfigPath.String()}, args...) } @@ -402,8 +412,8 @@ func (cli *ArduinoCLI) run(stdoutBuff, stderrBuff io.Writer, stdinBuff io.Reader } }() } + cliErr := cliProc.WaitWithinContext(ctx) wg.Wait() - cliErr := cliProc.Wait() fmt.Fprintln(terminalOut, color.HiBlackString("<<< Run completed (err = %v)", cliErr)) return cliErr diff --git a/internal/integrationtest/compile_5/recusive_include_test.go b/internal/integrationtest/compile_5/recusive_include_test.go new file mode 100644 index 00000000000..59a3c421c7f --- /dev/null +++ b/internal/integrationtest/compile_5/recusive_include_test.go @@ -0,0 +1,44 @@ +// This file is part of arduino-cli. +// +// Copyright 2025 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package compile_test + +import ( + "context" + "testing" + "time" + + "github.com/arduino/arduino-cli/internal/integrationtest" + "github.com/arduino/go-paths-helper" + "github.com/stretchr/testify/require" +) + +func TestCompileWithInfiniteMultipleIncludeRecursion(t *testing.T) { + env, cli := integrationtest.CreateArduinoCLIWithEnvironment(t) + t.Cleanup(env.CleanUp) + + // Install Arduino AVR Boards + _, _, err := cli.Run("core", "install", "arduino:avr@1.8.6") + require.NoError(t, err) + + sketch, err := paths.New("testdata", "SketchWithRecursiveIncludes").Abs() + require.NoError(t, err) + + // Time-limited test to prevent OOM + ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) + t.Cleanup(cancel) + _, _, _ = cli.RunWithContext(ctx, "compile", "-b", "arduino:avr:uno", sketch.String()) + require.NotErrorIs(t, ctx.Err(), context.DeadlineExceeded, "compilation should not hang") +} diff --git a/internal/integrationtest/compile_5/testdata/SketchWithRecursiveIncludes/SketchWithRecursiveIncludes.ino b/internal/integrationtest/compile_5/testdata/SketchWithRecursiveIncludes/SketchWithRecursiveIncludes.ino new file mode 100644 index 00000000000..2243de1baf9 --- /dev/null +++ b/internal/integrationtest/compile_5/testdata/SketchWithRecursiveIncludes/SketchWithRecursiveIncludes.ino @@ -0,0 +1 @@ +#include "a.h" diff --git a/internal/integrationtest/compile_5/testdata/SketchWithRecursiveIncludes/a.h b/internal/integrationtest/compile_5/testdata/SketchWithRecursiveIncludes/a.h new file mode 100644 index 00000000000..7c626e5beb0 --- /dev/null +++ b/internal/integrationtest/compile_5/testdata/SketchWithRecursiveIncludes/a.h @@ -0,0 +1,2 @@ +#include "a.h" +#include "a.h"