123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492 |
- #include "shellcommand.h"
- #include "compat.h"
- #include "defaults.h"
- #include "fs.h"
- #include "sigset.h"
- #include "spinlock.h"
- #include <library/cpp/testing/unittest/env.h>
- #include <library/cpp/testing/unittest/registar.h>
- #include <util/random/random.h>
- #include <util/stream/file.h>
- #include <util/stream/str.h>
- #include <util/stream/mem.h>
- #include <util/string/strip.h>
- #include <util/folder/tempdir.h>
- #if defined(_win_)
- #define NL "\r\n"
- const char catCommand[] = "sort"; // not really cat but ok
- const size_t textSize = 1;
- #else
- #define NL "\n"
- const char catCommand[] = "/bin/cat";
- const size_t textSize = 20000;
- #endif
- class TGuardedStringStream: public IInputStream, public IOutputStream {
- public:
- TGuardedStringStream() {
- Stream_.Reserve(100);
- }
- TString Str() const {
- with_lock (Lock_) {
- return Stream_.Str();
- }
- return TString(); // line for compiler
- }
- protected:
- size_t DoRead(void* buf, size_t len) override {
- with_lock (Lock_) {
- return Stream_.Read(buf, len);
- }
- return 0; // line for compiler
- }
- void DoWrite(const void* buf, size_t len) override {
- with_lock (Lock_) {
- return Stream_.Write(buf, len);
- }
- }
- private:
- TAdaptiveLock Lock_;
- TStringStream Stream_;
- };
- Y_UNIT_TEST_SUITE(TShellQuoteTest) {
- Y_UNIT_TEST(TestQuoteArg) {
- TString cmd;
- ShellQuoteArg(cmd, "/pr f/krev/prev.exe");
- ShellQuoteArgSp(cmd, "-DVal=\"W Quotes\"");
- ShellQuoteArgSp(cmd, "-DVal=W Space");
- ShellQuoteArgSp(cmd, "-DVal=Blah");
- UNIT_ASSERT_STRINGS_EQUAL(cmd, "\"/pr f/krev/prev.exe\" \"-DVal=\\\"W Quotes\\\"\" \"-DVal=W Space\" \"-DVal=Blah\"");
- }
- } // Y_UNIT_TEST_SUITE(TShellQuoteTest)
- Y_UNIT_TEST_SUITE(TShellCommandTest) {
- Y_UNIT_TEST(TestNoQuotes) {
- TShellCommandOptions options;
- options.SetQuoteArguments(false);
- TShellCommand cmd("echo hello");
- cmd.Run();
- UNIT_ASSERT_VALUES_EQUAL(cmd.GetError(), "");
- UNIT_ASSERT_VALUES_EQUAL(cmd.GetOutput(), "hello" NL);
- UNIT_ASSERT(TShellCommand::SHELL_FINISHED == cmd.GetStatus());
- UNIT_ASSERT(cmd.GetExitCode().Defined() && 0 == cmd.GetExitCode());
- UNIT_ASSERT_VALUES_EQUAL(cmd.GetQuotedCommand(), "echo hello");
- }
- Y_UNIT_TEST(TestOnlyNecessaryQuotes) {
- TShellCommandOptions options;
- options.SetQuoteArguments(true);
- TShellCommand cmd("echo");
- cmd << "hey"
- << "hello&world";
- cmd.Run();
- UNIT_ASSERT_VALUES_EQUAL(cmd.GetError(), "");
- UNIT_ASSERT_VALUES_EQUAL(cmd.GetOutput(), "hey hello&world" NL);
- UNIT_ASSERT(TShellCommand::SHELL_FINISHED == cmd.GetStatus());
- UNIT_ASSERT(cmd.GetExitCode().Defined() && 0 == cmd.GetExitCode());
- }
- Y_UNIT_TEST(TestRun) {
- TShellCommand cmd("echo");
- cmd << "hello";
- cmd.Run();
- UNIT_ASSERT_VALUES_EQUAL(cmd.GetError(), "");
- #if defined(_win_)
- UNIT_ASSERT_VALUES_EQUAL(cmd.GetOutput(), "\"hello\"\r\n");
- #else
- UNIT_ASSERT_VALUES_EQUAL(cmd.GetOutput(), "hello\n");
- #endif
- UNIT_ASSERT(TShellCommand::SHELL_FINISHED == cmd.GetStatus());
- UNIT_ASSERT(cmd.GetExitCode().Defined() && 0 == cmd.GetExitCode());
- }
- // running with no shell is not implemented for win
- // there should be no problem with it as long as SearchPath is on
- Y_UNIT_TEST(TestNoShell) {
- #if defined(_win_)
- const char dir[] = "dir";
- #else
- const char dir[] = "ls";
- #endif
- TShellCommandOptions options;
- options.SetQuoteArguments(false);
- {
- options.SetUseShell(false);
- TShellCommand cmd(dir, options);
- cmd << "|"
- << "sort";
- cmd.Run();
- UNIT_ASSERT(TShellCommand::SHELL_ERROR == cmd.GetStatus());
- UNIT_ASSERT(cmd.GetExitCode().Defined() && 0 != cmd.GetExitCode());
- }
- {
- options.SetUseShell(true);
- TShellCommand cmd(dir, options);
- cmd << "|"
- << "sort";
- cmd.Run();
- UNIT_ASSERT(TShellCommand::SHELL_FINISHED == cmd.GetStatus());
- UNIT_ASSERT_VALUES_EQUAL(cmd.GetError().size(), 0u);
- UNIT_ASSERT(cmd.GetExitCode().Defined() && 0 == cmd.GetExitCode());
- }
- }
- Y_UNIT_TEST(TestAsyncRun) {
- TShellCommandOptions options;
- options.SetAsync(true);
- #if defined(_win_)
- // fails with weird error "Input redirection is not supported"
- // TShellCommand cmd("sleep", options);
- // cmd << "3";
- TShellCommand cmd("ping 1.1.1.1 -n 1 -w 2000", options);
- #else
- TShellCommand cmd("sleep", options);
- cmd << "2";
- #endif
- UNIT_ASSERT(TShellCommand::SHELL_NONE == cmd.GetStatus());
- cmd.Run();
- sleep(1);
- UNIT_ASSERT(TShellCommand::SHELL_RUNNING == cmd.GetStatus());
- cmd.Wait();
- UNIT_ASSERT(TShellCommand::SHELL_RUNNING != cmd.GetStatus());
- UNIT_ASSERT_VALUES_EQUAL(cmd.GetError(), "");
- #if !defined(_win_)
- UNIT_ASSERT(TShellCommand::SHELL_FINISHED == cmd.GetStatus());
- UNIT_ASSERT_VALUES_EQUAL(cmd.GetOutput().size(), 0u);
- UNIT_ASSERT(cmd.GetExitCode().Defined() && 0 == cmd.GetExitCode());
- #endif
- }
- Y_UNIT_TEST(TestQuotes) {
- TShellCommandOptions options;
- TString input = TString("a\"a a");
- TString output;
- TStringOutput outputStream(output);
- options.SetOutputStream(&outputStream);
- TShellCommand cmd("echo", options);
- cmd << input;
- cmd.Run().Wait();
- output = StripString(output);
- #if defined(_win_)
- UNIT_ASSERT_VALUES_EQUAL("\"a\\\"a a\"", output);
- #else
- UNIT_ASSERT_VALUES_EQUAL(input, output);
- #endif
- UNIT_ASSERT_VALUES_EQUAL(cmd.GetError().size(), 0u);
- }
- Y_UNIT_TEST(TestRunNonexistent) {
- TShellCommand cmd("iwerognweiofnewio"); // some nonexistent command name
- cmd.Run().Wait();
- UNIT_ASSERT(TShellCommand::SHELL_ERROR == cmd.GetStatus());
- UNIT_ASSERT_VALUES_UNEQUAL(cmd.GetError().size(), 0u);
- UNIT_ASSERT(cmd.GetExitCode().Defined() && 0 != cmd.GetExitCode());
- }
- Y_UNIT_TEST(TestExitCode) {
- TShellCommand cmd("grep qwerty qwerty"); // some nonexistent file name
- cmd.Run().Wait();
- UNIT_ASSERT(TShellCommand::SHELL_ERROR == cmd.GetStatus());
- UNIT_ASSERT_VALUES_UNEQUAL(cmd.GetError().size(), 0u);
- UNIT_ASSERT(cmd.GetExitCode().Defined() && 2 == cmd.GetExitCode());
- }
- // 'type con' and 'copy con con' want real console, not stdin, use sort
- Y_UNIT_TEST(TestInput) {
- TShellCommandOptions options;
- TString input = (TString("a") * 2000).append(NL) * textSize;
- TStringInput inputStream(input);
- options.SetInputStream(&inputStream);
- TShellCommand cmd(catCommand, options);
- cmd.Run().Wait();
- UNIT_ASSERT_VALUES_EQUAL(input, cmd.GetOutput());
- UNIT_ASSERT_VALUES_EQUAL(cmd.GetError().size(), 0u);
- }
- Y_UNIT_TEST(TestOutput) {
- TShellCommandOptions options;
- TString input = (TString("a") * 2000).append(NL) * textSize;
- TStringInput inputStream(input);
- options.SetInputStream(&inputStream);
- TString output;
- TStringOutput outputStream(output);
- options.SetOutputStream(&outputStream);
- TShellCommand cmd(catCommand, options);
- cmd.Run().Wait();
- UNIT_ASSERT_VALUES_EQUAL(input, output);
- UNIT_ASSERT_VALUES_EQUAL(cmd.GetError().size(), 0u);
- }
- Y_UNIT_TEST(TestIO) {
- // descriptive test: use all options
- TShellCommandOptions options;
- options.SetAsync(true);
- options.SetQuoteArguments(false);
- options.SetLatency(10);
- options.SetClearSignalMask(true);
- options.SetCloseAllFdsOnExec(true);
- options.SetCloseInput(false);
- TGuardedStringStream write;
- options.SetInputStream(&write);
- TGuardedStringStream read;
- options.SetOutputStream(&read);
- options.SetUseShell(true);
- TShellCommand cmd("cat", options);
- cmd.Run();
- write << "alpha" << NL;
- while (read.Str() != "alpha" NL) {
- Sleep(TDuration::MilliSeconds(10));
- }
- write << "omega" << NL;
- while (read.Str() != "alpha" NL "omega" NL) {
- Sleep(TDuration::MilliSeconds(10));
- }
- write << "zeta" << NL;
- cmd.CloseInput();
- cmd.Wait();
- UNIT_ASSERT_VALUES_EQUAL(cmd.GetError(), "");
- UNIT_ASSERT(TShellCommand::SHELL_FINISHED == cmd.GetStatus());
- UNIT_ASSERT_VALUES_EQUAL(read.Str(), "alpha" NL "omega" NL "zeta" NL);
- UNIT_ASSERT(cmd.GetExitCode().Defined() && 0 == cmd.GetExitCode());
- }
- Y_UNIT_TEST(TestStreamClose) {
- struct TStream: public IOutputStream {
- size_t NumCloses = 0;
- void DoWrite(const void* buf, size_t len) override {
- Y_UNUSED(buf);
- Y_UNUSED(len);
- }
- void DoFinish() override {
- ++NumCloses;
- }
- } stream;
- auto options1 = TShellCommandOptions().SetCloseStreams(false).SetOutputStream(&stream).SetErrorStream(&stream);
- TShellCommand("echo hello", options1).Run().Wait();
- UNIT_ASSERT_VALUES_EQUAL(stream.NumCloses, 0);
- auto options = TShellCommandOptions().SetCloseStreams(true).SetOutputStream(&stream).SetErrorStream(&stream);
- TShellCommand("echo hello", options).Run().Wait();
- UNIT_ASSERT_VALUES_EQUAL(stream.NumCloses, 2);
- }
- Y_UNIT_TEST(TestInterruptSimple) {
- TShellCommandOptions options;
- options.SetAsync(true);
- options.SetCloseInput(false);
- TGuardedStringStream write;
- options.SetInputStream(&write); // set input stream that will be waited by cat
- TShellCommand cmd(catCommand, options);
- cmd.Run();
- sleep(1);
- UNIT_ASSERT(TShellCommand::SHELL_RUNNING == cmd.GetStatus());
- cmd.Terminate();
- cmd.Wait();
- UNIT_ASSERT(TShellCommand::SHELL_RUNNING != cmd.GetStatus());
- }
- #if !defined(_win_)
- // this ut is unix-only, port to win using %TEMP%
- Y_UNIT_TEST(TestInterrupt) {
- TString tmpfile = TString("shellcommand_ut.interrupt.") + ToString(RandomNumber<ui32>());
- TShellCommandOptions options;
- options.SetAsync(true);
- options.SetQuoteArguments(false);
- {
- TShellCommand cmd("/bin/sleep", options);
- cmd << " 1300 & wait; /usr/bin/touch " << tmpfile;
- cmd.Run();
- sleep(1);
- UNIT_ASSERT(TShellCommand::SHELL_RUNNING == cmd.GetStatus());
- // Async mode requires Terminate() + Wait() to send kill to child proc!
- cmd.Terminate();
- cmd.Wait();
- UNIT_ASSERT(TShellCommand::SHELL_ERROR == cmd.GetStatus());
- UNIT_ASSERT(cmd.GetExitCode().Defined() && -15 == cmd.GetExitCode());
- }
- sleep(1);
- UNIT_ASSERT(!NFs::Exists(tmpfile));
- }
- // this ut is unix-only (win has no signal mask)
- Y_UNIT_TEST(TestSignalMask) {
- // block SIGTERM
- int rc;
- sigset_t newmask, oldmask;
- SigEmptySet(&newmask);
- SigAddSet(&newmask, SIGTERM);
- rc = SigProcMask(SIG_SETMASK, &newmask, &oldmask);
- UNIT_ASSERT(rc == 0);
- TString tmpfile = TString("shellcommand_ut.interrupt.") + ToString(RandomNumber<ui32>());
- TShellCommandOptions options;
- options.SetAsync(true);
- options.SetQuoteArguments(false);
- // child proc should not receive SIGTERM anymore
- {
- TShellCommand cmd("/bin/sleep", options);
- // touch file only if sleep not interrupted by SIGTERM
- cmd << " 10 & wait; [ $? == 0 ] || /usr/bin/touch " << tmpfile;
- cmd.Run();
- sleep(1);
- UNIT_ASSERT(TShellCommand::SHELL_RUNNING == cmd.GetStatus());
- cmd.Terminate();
- cmd.Wait();
- UNIT_ASSERT(TShellCommand::SHELL_ERROR == cmd.GetStatus() || TShellCommand::SHELL_FINISHED == cmd.GetStatus());
- }
- sleep(1);
- UNIT_ASSERT(!NFs::Exists(tmpfile));
- // child proc should receive SIGTERM
- options.SetClearSignalMask(true);
- {
- TShellCommand cmd("/bin/sleep", options);
- // touch file regardless -- it will be interrupted
- cmd << " 10 & wait; /usr/bin/touch " << tmpfile;
- cmd.Run();
- sleep(1);
- UNIT_ASSERT(TShellCommand::SHELL_RUNNING == cmd.GetStatus());
- cmd.Terminate();
- cmd.Wait();
- UNIT_ASSERT(TShellCommand::SHELL_ERROR == cmd.GetStatus());
- }
- sleep(1);
- UNIT_ASSERT(!NFs::Exists(tmpfile));
- // restore signal mask
- rc = SigProcMask(SIG_SETMASK, &oldmask, nullptr);
- UNIT_ASSERT(rc == 0);
- }
- #else
- // This ut is windows-only
- Y_UNIT_TEST(TestStdinProperlyConstructed) {
- TShellCommandOptions options;
- options.SetErrorStream(&Cerr);
- TShellCommand cmd(BinaryPath("util/system/ut/stdin_osfhandle/stdin_osfhandle"), options);
- cmd.Run().Wait();
- UNIT_ASSERT(TShellCommand::SHELL_FINISHED == cmd.GetStatus());
- UNIT_ASSERT(cmd.GetExitCode().Defined() && 0 == cmd.GetExitCode());
- }
- #endif
- Y_UNIT_TEST(TestInternalError) {
- TString input = (TString("a") * 2000).append("\n");
- TStringInput inputStream(input);
- TMemoryOutput outputStream(nullptr, 0);
- TShellCommandOptions options;
- options.SetInputStream(&inputStream);
- options.SetOutputStream(&outputStream);
- TShellCommand cmd(catCommand, options);
- cmd.Run().Wait();
- UNIT_ASSERT(TShellCommand::SHELL_INTERNAL_ERROR == cmd.GetStatus());
- UNIT_ASSERT_VALUES_UNEQUAL(cmd.GetInternalError().size(), 0u);
- }
- Y_UNIT_TEST(TestHugeOutput) {
- TShellCommandOptions options;
- TGuardedStringStream stream;
- options.SetOutputStream(&stream);
- options.SetUseShell(true);
- TString input = TString(7000, 'a');
- TString command = TStringBuilder{} << "echo " << input;
- TShellCommand cmd(command, options);
- cmd.Run().Wait();
- UNIT_ASSERT_VALUES_EQUAL(stream.Str(), input + NL);
- }
- Y_UNIT_TEST(TestHugeError) {
- TShellCommandOptions options;
- TGuardedStringStream stream;
- options.SetErrorStream(&stream);
- options.SetUseShell(true);
- TString input = TString(7000, 'a');
- TString command = TStringBuilder{} << "echo " << input << ">&2";
- TShellCommand cmd(command, options);
- cmd.Run().Wait();
- UNIT_ASSERT_VALUES_EQUAL(stream.Str(), input + NL);
- }
- Y_UNIT_TEST(TestPipeInput) {
- TShellCommandOptions options;
- options.SetAsync(true);
- options.PipeInput();
- TShellCommand cmd(catCommand, options);
- cmd.Run();
- {
- TFile file(cmd.GetInputHandle().Release());
- TUnbufferedFileOutput fo(file);
- fo << "hello" << Endl;
- }
- cmd.Wait();
- UNIT_ASSERT_VALUES_EQUAL(cmd.GetOutput(), "hello" NL);
- UNIT_ASSERT_VALUES_EQUAL(cmd.GetError().size(), 0u);
- }
- Y_UNIT_TEST(TestPipeOutput) {
- TShellCommandOptions options;
- options.SetAsync(true);
- options.PipeOutput();
- constexpr TStringBuf firstMessage = "first message";
- constexpr TStringBuf secondMessage = "second message";
- const TString command = TStringBuilder() << "echo '" << firstMessage << "' && sleep 10 && echo '" << secondMessage << "'";
- TShellCommand cmd(command, options);
- cmd.Run();
- TUnbufferedFileInput cmdOutput(TFile(cmd.GetOutputHandle().Release()));
- TString firstLineOutput, secondLineOutput;
- {
- Sleep(TDuration::Seconds(5));
- firstLineOutput = cmdOutput.ReadLine();
- cmd.Wait();
- secondLineOutput = cmdOutput.ReadLine();
- }
- UNIT_ASSERT_VALUES_EQUAL(firstLineOutput, firstMessage);
- UNIT_ASSERT_VALUES_EQUAL(secondLineOutput, secondLineOutput);
- }
- Y_UNIT_TEST(TestOptionsConsistency) {
- TShellCommandOptions options;
- options.SetInheritOutput(false).SetInheritError(false);
- options.SetOutputStream(nullptr).SetErrorStream(nullptr);
- UNIT_ASSERT(options.OutputMode == TShellCommandOptions::HANDLE_STREAM);
- UNIT_ASSERT(options.ErrorMode == TShellCommandOptions::HANDLE_STREAM);
- }
- Y_UNIT_TEST(TestForkCallback) {
- TString tmpFile = TString("shellcommand_ut.test_for_callback.txt");
- TFsPath cwd(::NFs::CurrentWorkingDirectory());
- const TString tmpFilePath = cwd.Child(tmpFile);
- const TString text = "test output";
- auto afterForkCallback = [&tmpFilePath, &text]() -> void {
- TFixedBufferFileOutput out(tmpFilePath);
- out << text;
- };
- TShellCommandOptions options;
- options.SetFuncAfterFork(afterForkCallback);
- const TString command = "ls";
- TShellCommand cmd(command, options);
- cmd.Run();
- UNIT_ASSERT(NFs::Exists(tmpFilePath));
- TUnbufferedFileInput fileOutput(tmpFilePath);
- TString firstLine = fileOutput.ReadLine();
- UNIT_ASSERT_VALUES_EQUAL(firstLine, text);
- }
- } // Y_UNIT_TEST_SUITE(TShellCommandTest)
|