/* * Copyright 2012-present Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package com.facebook.buck.cli; import static com.google.common.util.concurrent.MoreExecutors.listeningDecorator; import static com.google.common.util.concurrent.MoreExecutors.newDirectExecutorService; import com.facebook.buck.android.AndroidBuckConfig; import com.facebook.buck.android.AndroidDirectoryResolver; import com.facebook.buck.android.AndroidPlatformTarget; import com.facebook.buck.android.DefaultAndroidDirectoryResolver; import com.facebook.buck.artifact_cache.ArtifactCacheBuckConfig; import com.facebook.buck.artifact_cache.ArtifactCaches; import com.facebook.buck.artifact_cache.HttpArtifactCacheEvent; import com.facebook.buck.config.Config; import com.facebook.buck.config.Configs; import com.facebook.buck.counters.CounterRegistry; import com.facebook.buck.counters.CounterRegistryImpl; import com.facebook.buck.event.BuckEventBus; import com.facebook.buck.event.BuckEventListener; import com.facebook.buck.event.BuckInitializationDurationEvent; import com.facebook.buck.event.CommandEvent; import com.facebook.buck.event.ConsoleEvent; import com.facebook.buck.event.DaemonEvent; import com.facebook.buck.event.DefaultBuckEventBus; import com.facebook.buck.event.listener.AbstractConsoleEventBusListener; import com.facebook.buck.event.listener.BroadcastEventListener; import com.facebook.buck.event.listener.CacheRateStatsListener; import com.facebook.buck.event.listener.ChromeTraceBuckConfig; import com.facebook.buck.event.listener.ChromeTraceBuildListener; import com.facebook.buck.event.listener.FileSerializationEventBusListener; import com.facebook.buck.event.listener.JavaUtilsLoggingBuildListener; import com.facebook.buck.event.listener.LoadBalancerEventsListener; import com.facebook.buck.event.listener.LoggingBuildListener; import com.facebook.buck.event.listener.MachineReadableLoggerListener; import com.facebook.buck.event.listener.ProgressEstimator; import com.facebook.buck.event.listener.PublicAnnouncementManager; import com.facebook.buck.event.listener.RuleKeyDiagnosticsListener; import com.facebook.buck.event.listener.RuleKeyLoggerListener; import com.facebook.buck.event.listener.SimpleConsoleEventBusListener; import com.facebook.buck.event.listener.SuperConsoleConfig; import com.facebook.buck.event.listener.SuperConsoleEventBusListener; import com.facebook.buck.httpserver.WebServer; import com.facebook.buck.io.AsynchronousDirectoryContentsCleaner; import com.facebook.buck.io.BuckPaths; import com.facebook.buck.io.MoreFiles; import com.facebook.buck.io.PathOrGlobMatcher; import com.facebook.buck.io.ProjectFilesystem; import com.facebook.buck.io.Watchman; import com.facebook.buck.io.WatchmanDiagnosticEventListener; import com.facebook.buck.jvm.java.JavaBuckConfig; import com.facebook.buck.log.CommandThreadFactory; import com.facebook.buck.log.ConsoleHandlerState; import com.facebook.buck.log.GlobalStateManager; import com.facebook.buck.log.InvocationInfo; import com.facebook.buck.log.LogConfig; import com.facebook.buck.log.Logger; import com.facebook.buck.model.BuckVersion; import com.facebook.buck.model.BuildId; import com.facebook.buck.model.Pair; import com.facebook.buck.parser.Parser; import com.facebook.buck.parser.ParserConfig; import com.facebook.buck.rules.ActionGraphCache; import com.facebook.buck.rules.BuildInfoStoreManager; import com.facebook.buck.rules.Cell; import com.facebook.buck.rules.CellProvider; import com.facebook.buck.rules.DefaultCellPathResolver; import com.facebook.buck.rules.KnownBuildRuleTypesFactory; import com.facebook.buck.rules.RelativeCellName; import com.facebook.buck.rules.RuleKey; import com.facebook.buck.rules.RuleKeyDiagnosticsMode; import com.facebook.buck.rules.coercer.ConstructorArgMarshaller; import com.facebook.buck.rules.coercer.DefaultTypeCoercerFactory; import com.facebook.buck.rules.coercer.TypeCoercerFactory; import com.facebook.buck.rules.keys.RuleKeyCacheRecycler; import com.facebook.buck.shell.WorkerProcessPool; import com.facebook.buck.step.ExecutorPool; import com.facebook.buck.test.TestConfig; import com.facebook.buck.test.TestResultSummaryVerbosity; import com.facebook.buck.timing.Clock; import com.facebook.buck.timing.DefaultClock; import com.facebook.buck.timing.NanosAdjustedClock; import com.facebook.buck.util.Ansi; import com.facebook.buck.util.AnsiEnvironmentChecking; import com.facebook.buck.util.AsyncCloseable; import com.facebook.buck.util.BgProcessKiller; import com.facebook.buck.util.BuckArgsMethods; import com.facebook.buck.util.BuckIsDyingException; import com.facebook.buck.util.Console; import com.facebook.buck.util.DefaultProcessExecutor; import com.facebook.buck.util.HumanReadableException; import com.facebook.buck.util.InterruptionFailedException; import com.facebook.buck.util.Libc; import com.facebook.buck.util.PkillProcessManager; import com.facebook.buck.util.PrintStreamProcessExecutorFactory; import com.facebook.buck.util.ProcessExecutor; import com.facebook.buck.util.ProcessManager; import com.facebook.buck.util.Verbosity; import com.facebook.buck.util.WatchmanWatcher; import com.facebook.buck.util.WatchmanWatcherException; import com.facebook.buck.util.cache.DefaultFileHashCache; import com.facebook.buck.util.cache.ProjectFileHashCache; import com.facebook.buck.util.cache.StackedFileHashCache; import com.facebook.buck.util.concurrent.MostExecutors; import com.facebook.buck.util.environment.Architecture; import com.facebook.buck.util.environment.BuildEnvironmentDescription; import com.facebook.buck.util.environment.CommandMode; import com.facebook.buck.util.environment.DefaultExecutionEnvironment; import com.facebook.buck.util.environment.ExecutionEnvironment; import com.facebook.buck.util.environment.Platform; import com.facebook.buck.util.network.RemoteLogBuckConfig; import com.facebook.buck.util.perf.PerfStatsTracking; import com.facebook.buck.util.perf.ProcessTracker; import com.facebook.buck.util.shutdown.NonReentrantSystemExit; import com.facebook.buck.util.versioncontrol.DelegatingVersionControlCmdLineInterface; import com.facebook.buck.util.versioncontrol.VersionControlBuckConfig; import com.facebook.buck.util.versioncontrol.VersionControlStatsGenerator; import com.facebook.buck.versions.VersionedTargetGraphCache; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.base.Supplier; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.reflect.ClassPath; import com.google.common.util.concurrent.ListeningExecutorService; import com.martiansoftware.nailgun.NGContext; import com.martiansoftware.nailgun.NGListeningAddress; import com.martiansoftware.nailgun.NGServer; import com.sun.jna.LastErrorException; import com.sun.jna.Native; import com.sun.jna.Pointer; import com.sun.jna.ptr.IntByReference; import java.io.Closeable; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; import java.lang.management.ManagementFactory; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.nio.channels.ClosedByInterruptException; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.nio.channels.OverlappingFileLockException; import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.LinkOption; import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.text.SimpleDateFormat; import java.time.Duration; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.OptionalInt; import java.util.TimeZone; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import javax.annotation.Nullable; import org.kohsuke.args4j.CmdLineException; public final class Main { /** Trying again won't help. */ public static final int FAIL_EXIT_CODE = 1; /** Trying again later might work. */ public static final int BUSY_EXIT_CODE = 2; /** The command was interrupted */ public static final int INTERRUPTED_EXIT_CODE = 130; private static final Optional<String> BUCKD_LAUNCH_TIME_NANOS = Optional.ofNullable(System.getProperty("buck.buckd_launch_time_nanos")); private static final String BUCK_BUILD_ID_ENV_VAR = "BUCK_BUILD_ID"; private static final String BUCKD_COLOR_DEFAULT_ENV_VAR = "BUCKD_COLOR_DEFAULT"; private static final Duration DAEMON_SLAYER_TIMEOUT = Duration.ofDays(1); private static final Duration SUPER_CONSOLE_REFRESH_RATE = Duration.ofMillis(100); private static final Duration HANG_DETECTOR_TIMEOUT = Duration.ofMinutes(5); /** Path to a directory of static content that should be served by the {@link WebServer}. */ private static final int DISK_IO_STATS_TIMEOUT_SECONDS = 10; private static final int EXECUTOR_SERVICES_TIMEOUT_SECONDS = 60; private static final int COUNTER_AGGREGATOR_SERVICE_TIMEOUT_SECONDS = 20; private final InputStream stdIn; private final PrintStream stdOut; private final PrintStream stdErr; private final Architecture architecture; private static final Semaphore commandSemaphore = new Semaphore(1); private static volatile Optional<NGContext> commandSemaphoreNgClient = Optional.empty(); private static final DaemonLifecycleManager daemonLifecycleManager = new DaemonLifecycleManager(); // Ensure we only have one instance of this, so multiple trash cleaning // operations are serialized on one queue. private static final AsynchronousDirectoryContentsCleaner TRASH_CLEANER = new AsynchronousDirectoryContentsCleaner(); private final Platform platform; // Ignore changes to generated Xcode project files and editors' backup files // so we don't dump buckd caches on every command. private static final ImmutableSet<PathOrGlobMatcher> DEFAULT_IGNORE_GLOBS = ImmutableSet.of( new PathOrGlobMatcher("**/*.pbxproj"), new PathOrGlobMatcher("**/*.xcscheme"), new PathOrGlobMatcher("**/*.xcworkspacedata"), // Various editors' temporary files new PathOrGlobMatcher("**/*~"), // Emacs new PathOrGlobMatcher("**/#*#"), new PathOrGlobMatcher("**/.#*"), // Vim new PathOrGlobMatcher("**/*.swo"), new PathOrGlobMatcher("**/*.swp"), new PathOrGlobMatcher("**/*.swpx"), new PathOrGlobMatcher("**/*.un~"), new PathOrGlobMatcher("**/.netrhwist"), // Eclipse new PathOrGlobMatcher(".idea"), new PathOrGlobMatcher(".iml"), new PathOrGlobMatcher("**/*.pydevproject"), new PathOrGlobMatcher(".project"), new PathOrGlobMatcher(".metadata"), new PathOrGlobMatcher("**/*.tmp"), new PathOrGlobMatcher("**/*.bak"), new PathOrGlobMatcher("**/*~.nib"), new PathOrGlobMatcher(".classpath"), new PathOrGlobMatcher(".settings"), new PathOrGlobMatcher(".loadpath"), new PathOrGlobMatcher(".externalToolBuilders"), new PathOrGlobMatcher(".cproject"), new PathOrGlobMatcher(".buildpath"), // Mac OS temp files new PathOrGlobMatcher(".DS_Store"), new PathOrGlobMatcher(".AppleDouble"), new PathOrGlobMatcher(".LSOverride"), new PathOrGlobMatcher(".Spotlight-V100"), new PathOrGlobMatcher(".Trashes"), // Windows new PathOrGlobMatcher("$RECYCLE.BIN"), // Sublime new PathOrGlobMatcher(".*.sublime-workspace")); private static final Logger LOG = Logger.get(Main.class); private static boolean isSessionLeader; @Nullable private static FileLock resourcesFileLock = null; private static final HangMonitor.AutoStartInstance HANG_MONITOR = new HangMonitor.AutoStartInstance( (input) -> { LOG.info( "No recent activity, dumping thread stacks (`tr , '\\n'` to decode): %s", input); }, HANG_DETECTOR_TIMEOUT); private static final NonReentrantSystemExit NON_REENTRANT_SYSTEM_EXIT = new NonReentrantSystemExit(); @VisibleForTesting public Main(PrintStream stdOut, PrintStream stdErr, InputStream stdIn) { this.stdOut = stdOut; this.stdErr = stdErr; this.stdIn = stdIn; this.architecture = Architecture.detect(); this.platform = Platform.detect(); } /* Define all error handling surrounding main command */ private void runMainThenExit( String[] args, Optional<NGContext> context, final long initTimestamp) { installUncaughtExceptionHandler(context); Path projectRoot = Paths.get("."); int exitCode = FAIL_EXIT_CODE; BuildId buildId = getBuildId(context); // Only post an overflow event if Watchman indicates a fresh instance event // after our initial query. WatchmanWatcher.FreshInstanceAction watchmanFreshInstanceAction = daemonLifecycleManager.hasDaemon() ? WatchmanWatcher.FreshInstanceAction.POST_OVERFLOW_EVENT : WatchmanWatcher.FreshInstanceAction.NONE; // Get the client environment, either from this process or from the Nailgun context. ImmutableMap<String, String> clientEnvironment = getClientEnvironment(context); try { CommandMode commandMode = CommandMode.RELEASE; exitCode = runMainWithExitCode( buildId, projectRoot, context, clientEnvironment, commandMode, watchmanFreshInstanceAction, initTimestamp, args); } catch (IOException e) { if (e.getMessage().startsWith("No space left on device")) { makeStandardConsole(context).printBuildFailure(e.getMessage()); } else { LOG.error(e); } } catch (HumanReadableException e) { makeStandardConsole(context).printBuildFailure(e.getHumanReadableErrorMessage()); } catch (InterruptionFailedException e) { // Command could not be interrupted. if (context.isPresent()) { context.get().getNGServer().shutdown(true); // Exit process to halt command execution. } } catch (BuckIsDyingException e) { LOG.warn(e, "Fallout because buck was already dying"); } catch (Throwable t) { LOG.error(t, "Uncaught exception at top level"); } finally { LOG.debug("Done."); LogConfig.flushLogs(); // Exit explicitly so that non-daemon threads (of which we use many) don't // keep the VM alive. System.exit(exitCode); } } private Console makeStandardConsole(Optional<NGContext> context) { return new Console( Verbosity.STANDARD_INFORMATION, stdOut, stdErr, new Ansi( AnsiEnvironmentChecking.environmentSupportsAnsiEscapes( platform, getClientEnvironment(context)))); } /** * @param buildId an identifier for this command execution. * @param context an optional NGContext that is present if running inside a Nailgun server. * @param initTimestamp Value of System.nanoTime() when process got main()/nailMain() invoked. * @param unexpandedCommandLineArgs command line arguments * @return an exit code or {@code null} if this is a process that should not exit */ @SuppressWarnings("PMD.PrematureDeclaration") public int runMainWithExitCode( BuildId buildId, Path projectRoot, Optional<NGContext> context, ImmutableMap<String, String> clientEnvironment, CommandMode commandMode, WatchmanWatcher.FreshInstanceAction watchmanFreshInstanceAction, final long initTimestamp, String... unexpandedCommandLineArgs) throws IOException, InterruptedException { String[] args = BuckArgsMethods.expandAtFiles(unexpandedCommandLineArgs, projectRoot); // Parse the command line args. BuckCommand command = new BuckCommand(); AdditionalOptionsCmdLineParser cmdLineParser = new AdditionalOptionsCmdLineParser(command); try { cmdLineParser.parseArgument(args); } catch (CmdLineException e) { // Can't go through the console for prettification since that needs the BuckConfig, and that // needs to be created with the overrides, which are parsed from the command line here, which // required the console to print the message that parsing has failed. So just write to stderr // and be done with it. stdErr.println(e.getLocalizedMessage()); stdErr.println("For help see 'buck --help'."); return 1; } { // Return help strings fast if the command is a help request. OptionalInt result = command.runHelp(stdErr); if (result.isPresent()) { return result.getAsInt(); } } // Setup logging. if (commandMode.isLoggingEnabled()) { // Reset logging each time we run a command while daemonized. // This will cause us to write a new log per command. LOG.debug("Rotating log."); LogConfig.flushLogs(); LogConfig.setupLogging(command.getLogConfig()); if (LOG.isDebugEnabled()) { Long gitCommitTimestamp = Long.getLong("buck.git_commit_timestamp"); String buildDateStr; if (gitCommitTimestamp == null) { buildDateStr = "(unknown)"; } else { buildDateStr = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.US) .format(new Date(TimeUnit.SECONDS.toMillis(gitCommitTimestamp))); } String buildRev = System.getProperty("buck.git_commit", "(unknown)"); LOG.debug( "Starting up (build date %s, rev %s), args: %s", buildDateStr, buildRev, Arrays.toString(args)); LOG.debug("System properties: %s", System.getProperties()); } } // Setup filesystem and buck config. Path canonicalRootPath = projectRoot.toRealPath().normalize(); Config config = Configs.createDefaultConfig( canonicalRootPath, command.getConfigOverrides().getForCell(RelativeCellName.ROOT_CELL_NAME)); ProjectFilesystem filesystem = new ProjectFilesystem(canonicalRootPath, config); DefaultCellPathResolver cellPathResolver = new DefaultCellPathResolver(filesystem.getRootPath(), config); BuckConfig buckConfig = new BuckConfig( config, filesystem, architecture, platform, clientEnvironment, cellPathResolver); ImmutableSet<Path> projectWatchList = ImmutableSet.<Path>builder() .add(canonicalRootPath) .addAll( buckConfig.getView(ParserConfig.class).getWatchCells() ? cellPathResolver.getTransitivePathMapping().values() : ImmutableList.of()) .build(); Optional<ImmutableList<String>> allowedJavaSpecificiationVersions = buckConfig.getAllowedJavaSpecificationVersions(); if (allowedJavaSpecificiationVersions.isPresent()) { String specificationVersion = System.getProperty("java.specification.version"); boolean javaSpecificationVersionIsAllowed = allowedJavaSpecificiationVersions.get().contains(specificationVersion); if (!javaSpecificationVersionIsAllowed) { throw new HumanReadableException( "Current Java version '%s' is not in the allowed java specification versions:\n%s", specificationVersion, Joiner.on(", ").join(allowedJavaSpecificiationVersions.get())); } } // Setup the console. Verbosity verbosity = VerbosityParser.parse(args); Optional<String> color; if (context.isPresent() && (context.get().getEnv() != null)) { String colorString = context.get().getEnv().getProperty(BUCKD_COLOR_DEFAULT_ENV_VAR); color = Optional.ofNullable(colorString); } else { color = Optional.empty(); } final Console console = new Console(verbosity, stdOut, stdErr, buckConfig.createAnsi(color)); // No more early outs: if this command is not read only, acquire the command semaphore to // become the only executing read/write command. // This must happen immediately before the try block to ensure that the semaphore is released. boolean commandSemaphoreAcquired = false; boolean shouldCleanUpTrash = false; if (!command.isReadOnly()) { commandSemaphoreAcquired = commandSemaphore.tryAcquire(); if (!commandSemaphoreAcquired) { LOG.warn("Buck server was busy executing a command. Maybe retrying later will help."); return BUSY_EXIT_CODE; } } try { if (commandSemaphoreAcquired) { commandSemaphoreNgClient = context; } if (!command.isReadOnly()) { Optional<String> currentVersion = filesystem.readFileIfItExists(filesystem.getBuckPaths().getCurrentVersionFile()); BuckPaths unconfiguredPaths = filesystem.getBuckPaths().withConfiguredBuckOut(filesystem.getBuckPaths().getBuckOut()); if (!currentVersion.isPresent() || !currentVersion.get().equals(BuckVersion.getVersion()) || (filesystem.exists(unconfiguredPaths.getGenDir(), LinkOption.NOFOLLOW_LINKS) && (filesystem.isSymLink(unconfiguredPaths.getGenDir()) ^ buckConfig.getBuckOutCompatLink()))) { // Migrate any version-dependent directories (which might be huge) to a trash directory // so we can delete it asynchronously after the command is done. moveToTrash( filesystem, console, buildId, filesystem.getBuckPaths().getAnnotationDir(), filesystem.getBuckPaths().getGenDir(), filesystem.getBuckPaths().getScratchDir(), filesystem.getBuckPaths().getResDir()); shouldCleanUpTrash = true; filesystem.mkdirs(filesystem.getBuckPaths().getCurrentVersionFile().getParent()); filesystem.writeContentsToPath( BuckVersion.getVersion(), filesystem.getBuckPaths().getCurrentVersionFile()); } } AndroidBuckConfig androidBuckConfig = new AndroidBuckConfig(buckConfig, platform); AndroidDirectoryResolver androidDirectoryResolver = new DefaultAndroidDirectoryResolver( filesystem.getRootPath().getFileSystem(), clientEnvironment, androidBuckConfig.getBuildToolsVersion(), androidBuckConfig.getNdkVersion()); ProcessExecutor processExecutor = new DefaultProcessExecutor(console); Clock clock; boolean enableThreadCpuTime = buckConfig.getBooleanValue("build", "enable_thread_cpu_time", true); if (BUCKD_LAUNCH_TIME_NANOS.isPresent()) { long nanosEpoch = Long.parseLong(BUCKD_LAUNCH_TIME_NANOS.get(), 10); LOG.verbose("Using nanos epoch: %d", nanosEpoch); clock = new NanosAdjustedClock(nanosEpoch, enableThreadCpuTime); } else { clock = new DefaultClock(enableThreadCpuTime); } ParserConfig parserConfig = buckConfig.getView(ParserConfig.class); try (Watchman watchman = buildWatchman( context, parserConfig, projectWatchList, clientEnvironment, console, clock)) { KnownBuildRuleTypesFactory factory = new KnownBuildRuleTypesFactory(processExecutor, androidDirectoryResolver); Cell rootCell = CellProvider.createForLocalBuild( filesystem, watchman, buckConfig, command.getConfigOverrides(), factory) .getCellByPath(filesystem.getRootPath()); Optional<Daemon> daemon = context.isPresent() && (watchman != Watchman.NULL_WATCHMAN) ? Optional.of(daemonLifecycleManager.getDaemon(rootCell)) : Optional.empty(); if (!daemon.isPresent() && shouldCleanUpTrash) { // Clean up the trash on a background thread if this was a // non-buckd read-write command. (We don't bother waiting // for it to complete; the thread is a daemon thread which // will just be terminated at shutdown time.) TRASH_CLEANER.startCleaningDirectory(filesystem.getBuckPaths().getTrashDir()); } int exitCode; ImmutableList<BuckEventListener> eventListeners = ImmutableList.of(); ExecutionEnvironment executionEnvironment = new DefaultExecutionEnvironment(clientEnvironment, System.getProperties()); ImmutableList.Builder<ProjectFileHashCache> allCaches = ImmutableList.builder(); // Build up the hash cache, which is a collection of the stateful cell cache and some // per-run caches. // // TODO(coneko, ruibm, agallagher): Determine whether we can use the existing filesystem // object that is in scope instead of creating a new rootCellProjectFilesystem. The primary // difference appears to be that filesystem is created with a Config that is used to produce // ImmutableSet<PathOrGlobMatcher> and BuckPaths for the ProjectFilesystem, whereas this one // uses the defaults. ProjectFilesystem rootCellProjectFilesystem = ProjectFilesystem.createNewOrThrowHumanReadableException( rootCell.getFilesystem().getRootPath()); if (daemon.isPresent()) { allCaches.addAll(getFileHashCachesFromDaemon(daemon.get())); } else { rootCell .getAllCells() .stream() .map(cell -> DefaultFileHashCache.createDefaultFileHashCache(cell.getFilesystem())) .forEach(allCaches::add); allCaches.add( DefaultFileHashCache.createBuckOutFileHashCache( rootCellProjectFilesystem, rootCell.getFilesystem().getBuckPaths().getBuckOut())); } // A cache which caches hashes of cell-relative paths which may have been ignore by // the main cell cache, and only serves to prevent rehashing the same file multiple // times in a single run. allCaches.add(DefaultFileHashCache.createDefaultFileHashCache(rootCellProjectFilesystem)); allCaches.addAll(DefaultFileHashCache.createOsRootDirectoriesCaches()); StackedFileHashCache fileHashCache = new StackedFileHashCache(allCaches.build()); Optional<WebServer> webServer = daemon.flatMap(Daemon::getWebServer); Optional<ConcurrentMap<String, WorkerProcessPool>> persistentWorkerPools = daemon.map(Daemon::getPersistentWorkerPools); TestConfig testConfig = new TestConfig(buckConfig); ArtifactCacheBuckConfig cacheBuckConfig = new ArtifactCacheBuckConfig(buckConfig); ExecutorService diskIoExecutorService = MostExecutors.newSingleThreadExecutor("Disk I/O"); ListeningExecutorService httpWriteExecutorService = getHttpWriteExecutorService(cacheBuckConfig); ScheduledExecutorService counterAggregatorExecutor = Executors.newSingleThreadScheduledExecutor( new CommandThreadFactory("CounterAggregatorThread")); // Eventually, we'll want to get allow websocket and/or nailgun clients to specify locale // when connecting. For now, we'll use the default from the server environment. Locale locale = Locale.getDefault(); // Create a cached thread pool for cpu intensive tasks Map<ExecutorPool, ListeningExecutorService> executors = new HashMap<>(); executors.put(ExecutorPool.CPU, listeningDecorator(Executors.newCachedThreadPool())); // Create a thread pool for network I/O tasks executors.put(ExecutorPool.NETWORK, newDirectExecutorService()); executors.put( ExecutorPool.PROJECT, listeningDecorator( MostExecutors.newMultiThreadExecutor("Project", buckConfig.getNumThreads()))); // Create and register the event buses that should listen to broadcast events. // If the build doesn't have a daemon create a new instance. BroadcastEventListener broadcastEventListener = daemon.map(Daemon::getBroadcastEventListener).orElseGet(BroadcastEventListener::new); // The order of resources in the try-with-resources block is important: the BuckEventBus // must be the last resource, so that it is closed first and can deliver its queued events // to the other resources before they are closed. InvocationInfo invocationInfo = InvocationInfo.of( buildId, isSuperConsoleEnabled(console), daemon.isPresent(), command.getSubCommandNameForLogging(), args, unexpandedCommandLineArgs, filesystem.getBuckPaths().getLogDir()); try (GlobalStateManager.LoggerIsMappedToThreadScope loggerThreadMappingScope = GlobalStateManager.singleton() .setupLoggers(invocationInfo, console.getStdErr(), stdErr, verbosity); BuildInfoStoreManager storeManager = new BuildInfoStoreManager(); AbstractConsoleEventBusListener consoleListener = createConsoleEventListener( clock, new SuperConsoleConfig(buckConfig), console, testConfig.getResultSummaryVerbosity(), executionEnvironment, webServer, locale, filesystem.getBuckPaths().getLogDir().resolve("test.log")); AsyncCloseable asyncCloseable = new AsyncCloseable(diskIoExecutorService); DefaultBuckEventBus buildEventBus = new DefaultBuckEventBus(clock, buildId); BroadcastEventListener.BroadcastEventBusClosable broadcastEventBusClosable = broadcastEventListener.addEventBus(buildEventBus); // This makes calls to LOG.error(...) post to the EventBus, instead of writing to // stderr. Closeable logErrorToEventBus = loggerThreadMappingScope.setWriter(createWriterForConsole(consoleListener)); // NOTE: This will only run during the lifetime of the process and will flush on close. CounterRegistry counterRegistry = new CounterRegistryImpl( counterAggregatorExecutor, buildEventBus, buckConfig.getCountersFirstFlushIntervalMillis(), buckConfig.getCountersFlushIntervalMillis()); PerfStatsTracking perfStatsTracking = new PerfStatsTracking(buildEventBus, invocationInfo); ProcessTracker processTracker = buckConfig.isProcessTrackerEnabled() && platform != Platform.WINDOWS ? new ProcessTracker( buildEventBus, invocationInfo, daemon.isPresent(), buckConfig.isProcessTrackerDeepEnabled()) : null; ) { LOG.debug(invocationInfo.toLogLine()); buildEventBus.register(HANG_MONITOR.getHangMonitor()); ArtifactCaches artifactCacheFactory = new ArtifactCaches( cacheBuckConfig, buildEventBus, filesystem, executionEnvironment.getWifiSsid(), httpWriteExecutorService, Optional.of(asyncCloseable)); ProgressEstimator progressEstimator = new ProgressEstimator( filesystem .resolve(filesystem.getBuckPaths().getBuckOut()) .resolve(ProgressEstimator.PROGRESS_ESTIMATIONS_JSON), buildEventBus); consoleListener.setProgressEstimator(progressEstimator); BuildEnvironmentDescription buildEnvironmentDescription = getBuildEnvironmentDescription( executionEnvironment, buckConfig); Iterable<BuckEventListener> commandEventListeners = command.getSubcommand().isPresent() ? command.getSubcommand().get().getEventListeners() : ImmutableList.of(); eventListeners = addEventListeners( buildEventBus, rootCell.getFilesystem(), invocationInfo, rootCell.getBuckConfig(), webServer, clock, consoleListener, counterRegistry, commandEventListeners ); if (commandMode == CommandMode.RELEASE && buckConfig.isPublicAnnouncementsEnabled()) { PublicAnnouncementManager announcementManager = new PublicAnnouncementManager( clock, buildEventBus, consoleListener, buckConfig.getRepository().orElse("unknown"), new RemoteLogBuckConfig(buckConfig), executors.get(ExecutorPool.CPU)); announcementManager.getAndPostAnnouncements(); } // This needs to be after the registration of the event listener so they can pick it up. if (watchmanFreshInstanceAction == WatchmanWatcher.FreshInstanceAction.NONE) { buildEventBus.post(DaemonEvent.newDaemonInstance()); } VersionControlBuckConfig vcBuckConfig = new VersionControlBuckConfig(buckConfig); VersionControlStatsGenerator vcStatsGenerator = new VersionControlStatsGenerator( new DelegatingVersionControlCmdLineInterface( rootCell.getFilesystem().getRootPath(), new PrintStreamProcessExecutorFactory(), vcBuckConfig.getHgCmd(), buckConfig.getEnvironment()), vcBuckConfig.getPregeneratedVersionControlStats()); if (command.subcommand instanceof AbstractCommand) { AbstractCommand subcommand = (AbstractCommand) command.subcommand; if (!commandMode.equals(CommandMode.TEST)) { VersionControlStatsGenerator.Mode mode = subcommand.isSourceControlStatsGatheringEnabled() || vcBuckConfig.shouldGenerateStatistics() ? VersionControlStatsGenerator.Mode.FAST : VersionControlStatsGenerator.Mode.PREGENERATED; vcStatsGenerator.generateStatsAsync(mode, diskIoExecutorService, buildEventBus); } } ImmutableList<String> remainingArgs = args.length > 1 ? ImmutableList.copyOf(Arrays.copyOfRange(args, 1, args.length)) : ImmutableList.of(); CommandEvent.Started startedEvent = CommandEvent.started( args.length > 0 ? args[0] : "", remainingArgs, daemon.isPresent(), getBuckPID()); buildEventBus.post(startedEvent); // Create or get Parser and invalidate cached command parameters. TypeCoercerFactory typeCoercerFactory = null; Parser parser = null; VersionedTargetGraphCache versionedTargetGraphCache = null; ActionGraphCache actionGraphCache = null; Optional<RuleKeyCacheRecycler<RuleKey>> defaultRuleKeyFactoryCacheRecycler = Optional.empty(); if (daemon.isPresent()) { try { WatchmanWatcher watchmanWatcher = new WatchmanWatcher( watchman.getProjectWatches(), daemon.get().getFileEventBus(), ImmutableSet.<PathOrGlobMatcher>builder() .addAll(filesystem.getIgnorePaths()) .addAll(DEFAULT_IGNORE_GLOBS) .build(), watchman, daemon.get().getWatchmanCursor()); Pair<TypeCoercerFactory, Parser> pair = getParserFromDaemon( daemon.get(), context.get(), startedEvent, buildEventBus, watchmanWatcher, watchmanFreshInstanceAction); typeCoercerFactory = pair.getFirst(); parser = pair.getSecond(); versionedTargetGraphCache = daemon.get().getVersionedTargetGraphCache(); actionGraphCache = daemon.get().getActionGraphCache(); if (buckConfig.getRuleKeyCaching()) { LOG.debug("Using rule key calculation caching"); defaultRuleKeyFactoryCacheRecycler = Optional.of(daemon.get().getDefaultRuleKeyFactoryCacheRecycler()); } } catch (WatchmanWatcherException | IOException e) { buildEventBus.post( ConsoleEvent.warning( "Watchman threw an exception while parsing file changes.\n%s", e.getMessage())); } } if (versionedTargetGraphCache == null) { versionedTargetGraphCache = new VersionedTargetGraphCache(); } if (actionGraphCache == null) { actionGraphCache = new ActionGraphCache(broadcastEventListener); } if (typeCoercerFactory == null || parser == null) { typeCoercerFactory = new DefaultTypeCoercerFactory(); parser = new Parser( broadcastEventListener, rootCell.getBuckConfig().getView(ParserConfig.class), typeCoercerFactory, new ConstructorArgMarshaller(typeCoercerFactory)); } // Because the Parser is potentially constructed before the CounterRegistry, // we need to manually register its counters after it's created. // // The counters will be unregistered once the counter registry is closed. counterRegistry.registerCounters(parser.getCounters()); JavaUtilsLoggingBuildListener.ensureLogFileIsWritten(rootCell.getFilesystem()); Optional<ProcessManager> processManager; if (platform == Platform.WINDOWS) { processManager = Optional.empty(); } else { processManager = Optional.of(new PkillProcessManager(processExecutor)); } Supplier<AndroidPlatformTarget> androidPlatformTargetSupplier = createAndroidPlatformTargetSupplier( androidDirectoryResolver, androidBuckConfig, buildEventBus); // At this point, we have parsed options but haven't started // running the command yet. This is a good opportunity to // augment the event bus with our serialize-to-file // event-listener. if (command.subcommand instanceof AbstractCommand) { AbstractCommand subcommand = (AbstractCommand) command.subcommand; Optional<Path> eventsOutputPath = subcommand.getEventsOutputPath(); if (eventsOutputPath.isPresent()) { BuckEventListener listener = new FileSerializationEventBusListener(eventsOutputPath.get()); buildEventBus.register(listener); } } buildEventBus.post( new BuckInitializationDurationEvent( TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - initTimestamp))); try { exitCode = command.run( CommandRunnerParams.builder() .setConsole(console) .setStdIn(stdIn) .setCell(rootCell) .setAndroidPlatformTargetSupplier(androidPlatformTargetSupplier) .setArtifactCacheFactory(artifactCacheFactory) .setBuckEventBus(buildEventBus) .setTypeCoercerFactory(typeCoercerFactory) .setParser(parser) .setPlatform(platform) .setEnvironment(clientEnvironment) .setJavaPackageFinder( rootCell .getBuckConfig() .getView(JavaBuckConfig.class) .createDefaultJavaPackageFinder()) .setClock(clock) .setVersionControlStatsGenerator(vcStatsGenerator) .setProcessManager(processManager) .setPersistentWorkerPools(persistentWorkerPools) .setWebServer(webServer) .setBuckConfig(buckConfig) .setFileHashCache(fileHashCache) .setExecutors(executors) .setBuildEnvironmentDescription(buildEnvironmentDescription) .setVersionedTargetGraphCache(versionedTargetGraphCache) .setActionGraphCache(actionGraphCache) .setKnownBuildRuleTypesFactory(factory) .setInvocationInfo(Optional.of(invocationInfo)) .setDefaultRuleKeyFactoryCacheRecycler(defaultRuleKeyFactoryCacheRecycler) .setBuildInfoStoreManager(storeManager) .build()); } catch (InterruptedException | ClosedByInterruptException e) { exitCode = INTERRUPTED_EXIT_CODE; buildEventBus.post(CommandEvent.interrupted(startedEvent, INTERRUPTED_EXIT_CODE)); throw e; } // We've reserved exitCode 2 for timeouts, and some commands (e.g. run) may violate this // Let's avoid an infinite loop if (exitCode == BUSY_EXIT_CODE) { exitCode = FAIL_EXIT_CODE; // Some loss of info here, but better than looping LOG.error( "Buck return with exit code %d which we use to indicate busy status. " + "This is probably propagating an exit code from a sub process or tool. " + "Coercing to %d to avoid retries.", BUSY_EXIT_CODE, FAIL_EXIT_CODE); } // Wait for HTTP writes to complete. closeHttpExecutorService( cacheBuckConfig, Optional.of(buildEventBus), httpWriteExecutorService); closeExecutorService( "CounterAggregatorExecutor", counterAggregatorExecutor, COUNTER_AGGREGATOR_SERVICE_TIMEOUT_SECONDS); buildEventBus.post(CommandEvent.finished(startedEvent, exitCode)); } catch (Throwable t) { LOG.debug(t, "Failing build on exception."); closeHttpExecutorService(cacheBuckConfig, Optional.empty(), httpWriteExecutorService); closeDiskIoExecutorService(diskIoExecutorService); flushEventListeners(console, buildId, eventListeners); throw t; } finally { if (commandSemaphoreAcquired) { commandSemaphoreNgClient = Optional.empty(); BgProcessKiller.disarm(); commandSemaphore.release(); // Allow another command to execute while outputting traces. commandSemaphoreAcquired = false; } if (daemon.isPresent() && shouldCleanUpTrash) { // Clean up the trash in the background if this was a buckd // read-write command. (We don't bother waiting for it to // complete; the cleaner will ensure subsequent cleans are // serialized with this one.) TRASH_CLEANER.startCleaningDirectory(filesystem.getBuckPaths().getTrashDir()); } // shut down the cached thread pools for (ExecutorPool p : executors.keySet()) { closeExecutorService(p.toString(), executors.get(p), EXECUTOR_SERVICES_TIMEOUT_SECONDS); } } if (context.isPresent() && !rootCell.getBuckConfig().getFlushEventsBeforeExit()) { context.get().in.close(); // Avoid client exit triggering client disconnection handling. context.get().exit(exitCode); // Allow nailgun client to exit while outputting traces. } closeDiskIoExecutorService(diskIoExecutorService); flushEventListeners(console, buildId, eventListeners); return exitCode; } } finally { if (commandSemaphoreAcquired) { commandSemaphoreNgClient = Optional.empty(); BgProcessKiller.disarm(); commandSemaphore.release(); } } } private void flushEventListeners( Console console, BuildId buildId, ImmutableList<BuckEventListener> eventListeners) throws InterruptedException { for (BuckEventListener eventListener : eventListeners) { try { eventListener.outputTrace(buildId); } catch (RuntimeException e) { PrintStream stdErr = console.getStdErr(); stdErr.println("Ignoring non-fatal error! The stack trace is below:"); e.printStackTrace(stdErr); } } } private static void moveToTrash( ProjectFilesystem filesystem, Console console, BuildId buildId, Path... pathsToMove) throws IOException { Path trashPath = filesystem.getBuckPaths().getTrashDir().resolve(buildId.toString()); filesystem.mkdirs(trashPath); for (Path pathToMove : pathsToMove) { try { // Technically this might throw AtomicMoveNotSupportedException, // but we're moving a path within buck-out, so we don't expect this // to throw. // // If it does throw, we'll complain loudly and synchronously delete // the file instead. filesystem.move( pathToMove, trashPath.resolve(pathToMove.getFileName()), StandardCopyOption.ATOMIC_MOVE); } catch (NoSuchFileException e) { LOG.verbose(e, "Ignoring missing path %s", pathToMove); } catch (AtomicMoveNotSupportedException e) { console .getStdErr() .format("Atomic moves not supported, falling back to synchronous delete: %s", e); MoreFiles.deleteRecursivelyIfExists(pathToMove); } } } private static final Watchman buildWatchman( Optional<NGContext> context, ParserConfig parserConfig, ImmutableSet<Path> projectWatchList, ImmutableMap<String, String> clientEnvironment, Console console, Clock clock) throws InterruptedException { Watchman watchman; if (context.isPresent() || parserConfig.getGlobHandler() == ParserConfig.GlobHandler.WATCHMAN) { watchman = Watchman.build( projectWatchList, clientEnvironment, console, clock, parserConfig.getWatchmanQueryTimeoutMs()); LOG.debug( "Watchman capabilities: %s Project watches: %s Glob handler config: %s " + "Query timeout ms config: %s", watchman.getCapabilities(), watchman.getProjectWatches(), parserConfig.getGlobHandler(), parserConfig.getWatchmanQueryTimeoutMs()); } else { watchman = Watchman.NULL_WATCHMAN; LOG.debug( "Not using Watchman, context present: %s, glob handler: %s", context.isPresent(), parserConfig.getGlobHandler()); } return watchman; } private static void closeExecutorService( String executorName, ExecutorService executorService, long timeoutSeconds) throws InterruptedException { executorService.shutdown(); LOG.info( "Awaiting termination of %s executor service. Waiting for all jobs to complete, " + "or up to maximum of %s seconds...", executorName, timeoutSeconds); executorService.awaitTermination(timeoutSeconds, TimeUnit.SECONDS); if (!executorService.isTerminated()) { LOG.warn( "%s executor service is still running after shutdown request and " + "%s second timeout. Shutting down forcefully..", executorName, timeoutSeconds); executorService.shutdownNow(); } } private static void closeDiskIoExecutorService(ExecutorService diskIoExecutorService) throws InterruptedException { closeExecutorService("Disk IO", diskIoExecutorService, DISK_IO_STATS_TIMEOUT_SECONDS); } private static void closeHttpExecutorService( ArtifactCacheBuckConfig buckConfig, Optional<BuckEventBus> eventBus, ListeningExecutorService httpWriteExecutorService) throws InterruptedException { closeExecutorService( "HTTP Write", httpWriteExecutorService, buckConfig.getHttpWriterShutdownTimeout()); if (eventBus.isPresent()) { eventBus.get().post(HttpArtifactCacheEvent.newShutdownEvent()); } } private static ListeningExecutorService getHttpWriteExecutorService( ArtifactCacheBuckConfig buckConfig) { if (buckConfig.hasAtLeastOneWriteableCache()) { ExecutorService executorService = MostExecutors.newMultiThreadExecutor( "HTTP Write", buckConfig.getHttpMaxConcurrentWrites()); return listeningDecorator(executorService); } else { return newDirectExecutorService(); } } @VisibleForTesting static Supplier<AndroidPlatformTarget> createAndroidPlatformTargetSupplier( final AndroidDirectoryResolver androidDirectoryResolver, final AndroidBuckConfig androidBuckConfig, final BuckEventBus eventBus) { // TODO(mbolin): Only one such Supplier should be created per Cell per Buck execution. // Currently, only one Supplier is created per Buck execution because Main creates the Supplier // and passes it from above all the way through, but it is not parameterized by Cell. // // TODO(mbolin): Every build rule that uses AndroidPlatformTarget must include the result // of its getCacheName() method in its RuleKey. return new Supplier<AndroidPlatformTarget>() { @Nullable private AndroidPlatformTarget androidPlatformTarget; @Nullable private RuntimeException exception; @Override public AndroidPlatformTarget get() { if (androidPlatformTarget != null) { return androidPlatformTarget; } else if (exception != null) { throw exception; } String androidPlatformTargetId; Optional<String> target = androidBuckConfig.getAndroidTarget(); if (target.isPresent()) { androidPlatformTargetId = target.get(); } else { androidPlatformTargetId = AndroidPlatformTarget.DEFAULT_ANDROID_PLATFORM_TARGET; eventBus.post( ConsoleEvent.warning( "No Android platform target specified. Using default: %s", androidPlatformTargetId)); } try { androidPlatformTarget = AndroidPlatformTarget.getTargetForId( androidPlatformTargetId, androidDirectoryResolver, androidBuckConfig.getAaptOverride(), androidBuckConfig.getAapt2Override()); return androidPlatformTarget; } catch (RuntimeException e) { exception = e; throw e; } } }; } private static ConsoleHandlerState.Writer createWriterForConsole( final AbstractConsoleEventBusListener console) { return new ConsoleHandlerState.Writer() { @Override public void write(String line) throws IOException { console.printSevereWarningDirectly(line); } @Override public void flush() throws IOException { // Intentional no-op. } @Override public void close() throws IOException { // Intentional no-op. } }; } /** * @return the client environment, which is either the process environment or the environment sent * to the daemon by the Nailgun client. This method should always be used in preference to * System.getenv() and should be the only call to System.getenv() within the Buck codebase to * ensure that the use of the Buck daemon is transparent. */ @SuppressWarnings({"unchecked", "rawtypes"}) // Safe as Property is a Map<String, String>. private static ImmutableMap<String, String> getClientEnvironment(Optional<NGContext> context) { if (context.isPresent()) { return ImmutableMap.<String, String>copyOf((Map) context.get().getEnv()); } else { return ImmutableMap.copyOf(System.getenv()); } } /** Wire up daemon to new client and get cached Parser. */ private Pair<TypeCoercerFactory, Parser> getParserFromDaemon( Daemon daemonForParser, NGContext context, CommandEvent commandEvent, BuckEventBus eventBus, WatchmanWatcher watchmanWatcher, WatchmanWatcher.FreshInstanceAction watchmanFreshInstanceAction) throws IOException, InterruptedException { context.addClientListener( () -> { if (Main.isSessionLeader && Main.commandSemaphoreNgClient.orElse(null) == context) { LOG.info("killing background processes on client disconnection"); // Process no longer wants work done on its behalf. BgProcessKiller.killBgProcesses(); } daemonForParser.interruptOnClientExit(context.err); }); daemonForParser.watchFileSystem( commandEvent, eventBus, watchmanWatcher, watchmanFreshInstanceAction); return new Pair<>(daemonForParser.getTypeCoercerFactory(), daemonForParser.getParser()); } private ImmutableList<ProjectFileHashCache> getFileHashCachesFromDaemon(Daemon daemon) throws IOException { return daemon.getFileHashCaches(); } private void loadListenersFromBuckConfig( ImmutableList.Builder<BuckEventListener> eventListeners, ProjectFilesystem projectFilesystem, BuckConfig config) { final ImmutableSet<String> paths = config.getListenerJars(); if (paths.isEmpty()) { return; } URL[] urlsArray = new URL[paths.size()]; try { int i = 0; for (String path : paths) { String urlString = "file://" + projectFilesystem.resolve(Paths.get(path)); urlsArray[i] = new URL(urlString); i++; } } catch (MalformedURLException e) { throw new HumanReadableException(e.getMessage()); } // This ClassLoader is disconnected to allow searching the JARs (and just the JARs) for classes. ClassLoader isolatedClassLoader = URLClassLoader.newInstance(urlsArray, null); ImmutableSet<ClassPath.ClassInfo> classInfos; try { ClassPath classPath = ClassPath.from(isolatedClassLoader); classInfos = classPath.getTopLevelClasses(); } catch (IOException e) { throw new HumanReadableException(e.getMessage()); } // This ClassLoader will actually work, because it is joined to the parent ClassLoader. URLClassLoader workingClassLoader = URLClassLoader.newInstance(urlsArray); for (ClassPath.ClassInfo classInfo : classInfos) { String className = classInfo.getName(); try { Class<?> aClass = Class.forName(className, true, workingClassLoader); if (BuckEventListener.class.isAssignableFrom(aClass)) { BuckEventListener listener = aClass.asSubclass(BuckEventListener.class).newInstance(); eventListeners.add(listener); } } catch (ReflectiveOperationException e) { throw new HumanReadableException( "Error loading event listener class '%s': %s: %s", className, e.getClass(), e.getMessage()); } } } @SuppressWarnings("PMD.PrematureDeclaration") private ImmutableList<BuckEventListener> addEventListeners( BuckEventBus buckEventBus, ProjectFilesystem projectFilesystem, InvocationInfo invocationInfo, BuckConfig buckConfig, Optional<WebServer> webServer, Clock clock, AbstractConsoleEventBusListener consoleEventBusListener, CounterRegistry counterRegistry, Iterable<BuckEventListener> commandSpecificEventListeners ) { ImmutableList.Builder<BuckEventListener> eventListenersBuilder = ImmutableList.<BuckEventListener>builder() .add(new JavaUtilsLoggingBuildListener()) .add(consoleEventBusListener) .add(new LoggingBuildListener()); ChromeTraceBuckConfig chromeTraceConfig = buckConfig.getView(ChromeTraceBuckConfig.class); if (chromeTraceConfig.isChromeTraceCreationEnabled()) { try { eventListenersBuilder.add( new ChromeTraceBuildListener( projectFilesystem, invocationInfo, clock, chromeTraceConfig)); } catch (IOException e) { LOG.error("Unable to create ChromeTrace listener!"); } } else { LOG.warn("::: ChromeTrace listener disabled"); } if (webServer.isPresent()) { eventListenersBuilder.add(webServer.get().createListener()); } loadListenersFromBuckConfig(eventListenersBuilder, projectFilesystem, buckConfig); if (buckConfig.isRuleKeyLoggerEnabled()) { eventListenersBuilder.add( new RuleKeyLoggerListener( projectFilesystem, invocationInfo, MostExecutors.newSingleThreadExecutor( new CommandThreadFactory(getClass().getName())))); } if (buckConfig.getRuleKeyDiagnosticsMode() != RuleKeyDiagnosticsMode.NEVER) { eventListenersBuilder.add( new RuleKeyDiagnosticsListener( projectFilesystem, invocationInfo, MostExecutors.newSingleThreadExecutor( new CommandThreadFactory(getClass().getName())))); } if (buckConfig.isMachineReadableLoggerEnabled()) { try { eventListenersBuilder.add( new MachineReadableLoggerListener( invocationInfo, projectFilesystem, MostExecutors.newSingleThreadExecutor( new CommandThreadFactory(getClass().getName())))); } catch (FileNotFoundException e) { LOG.warn("Unable to open stream for machine readable log file."); } } eventListenersBuilder.add(new LoadBalancerEventsListener(counterRegistry)); eventListenersBuilder.add(new CacheRateStatsListener(buckEventBus)); eventListenersBuilder.add(new WatchmanDiagnosticEventListener(buckEventBus)); eventListenersBuilder.addAll(commandSpecificEventListeners); ImmutableList<BuckEventListener> eventListeners = eventListenersBuilder.build(); eventListeners.forEach(buckEventBus::register); return eventListeners; } private BuildEnvironmentDescription getBuildEnvironmentDescription( ExecutionEnvironment executionEnvironment, BuckConfig buckConfig) { ImmutableMap.Builder<String, String> environmentExtraData = ImmutableMap.builder(); return BuildEnvironmentDescription.of( executionEnvironment, new ArtifactCacheBuckConfig(buckConfig).getArtifactCacheModesRaw(), environmentExtraData.build()); } private AbstractConsoleEventBusListener createConsoleEventListener( Clock clock, SuperConsoleConfig config, Console console, TestResultSummaryVerbosity testResultSummaryVerbosity, ExecutionEnvironment executionEnvironment, Optional<WebServer> webServer, Locale locale, Path testLogPath) { if (isSuperConsoleEnabled(console)) { SuperConsoleEventBusListener superConsole = new SuperConsoleEventBusListener( config, console, clock, testResultSummaryVerbosity, executionEnvironment, webServer, locale, testLogPath, TimeZone.getDefault()); superConsole.startRenderScheduler( SUPER_CONSOLE_REFRESH_RATE.toMillis(), TimeUnit.MILLISECONDS); return superConsole; } return new SimpleConsoleEventBusListener( console, clock, testResultSummaryVerbosity, locale, testLogPath, executionEnvironment); } private boolean isSuperConsoleEnabled(Console console) { return Platform.WINDOWS != Platform.detect() && console.getAnsi().isAnsiTerminal() && !console.getVerbosity().shouldPrintCommand() && console.getVerbosity().shouldPrintStandardInformation(); } /** * A helper method to retrieve the process ID of Buck. The return value from the JVM has to match * the following pattern: {PID}@{Hostname}. It it does not match the return value is 0. * * @return the PID or 0L. */ private static long getBuckPID() { String pid = ManagementFactory.getRuntimeMXBean().getName(); return (pid != null && pid.matches("^\\d+@\\w+$")) ? Long.parseLong(pid.split("@")[0]) : 0L; } private static BuildId getBuildId(Optional<NGContext> context) { String specifiedBuildId; if (context.isPresent()) { specifiedBuildId = context.get().getEnv().getProperty(BUCK_BUILD_ID_ENV_VAR); } else { specifiedBuildId = System.getenv().get(BUCK_BUILD_ID_ENV_VAR); } if (specifiedBuildId == null) { throw new RuntimeException("Missing build id value."); } else { return new BuildId(specifiedBuildId); } } private static void installUncaughtExceptionHandler(final Optional<NGContext> context) { // Override the default uncaught exception handler for background threads to log // to java.util.logging then exit the JVM with an error code. // // (We do this because the default is to just print to stderr and not exit the JVM, // which is not safe in a multithreaded environment if the thread held a lock or // resource which other threads need.) Thread.setDefaultUncaughtExceptionHandler( (t, e) -> { LOG.error(e, "Uncaught exception from thread %s", t); if (context.isPresent()) { // Shut down the Nailgun server and make sure it stops trapping System.exit(). // // We pass false for exitVM because otherwise Nailgun exits with code 0. context.get().getNGServer().shutdown(/* exitVM */ false); } NON_REENTRANT_SYSTEM_EXIT.shutdownSoon(FAIL_EXIT_CODE); }); } public static void main(String[] args) { new Main(System.out, System.err, System.in) .runMainThenExit(args, Optional.empty(), System.nanoTime()); } private static void markFdCloseOnExec(int fd) { int fdFlags; fdFlags = Libc.INSTANCE.fcntl(fd, Libc.Constants.rFGETFD); if (fdFlags == -1) { throw new LastErrorException(Native.getLastError()); } fdFlags |= Libc.Constants.rFDCLOEXEC; if (Libc.INSTANCE.fcntl(fd, Libc.Constants.rFSETFD, fdFlags) == -1) { throw new LastErrorException(Native.getLastError()); } } private static void daemonizeIfPossible() { String osName = System.getProperty("os.name"); Libc.OpenPtyLibrary openPtyLibrary; Platform platform = Platform.detect(); if (platform == Platform.LINUX) { Libc.Constants.rTIOCSCTTY = Libc.Constants.LINUX_TIOCSCTTY; Libc.Constants.rFDCLOEXEC = Libc.Constants.LINUX_FD_CLOEXEC; Libc.Constants.rFGETFD = Libc.Constants.LINUX_F_GETFD; Libc.Constants.rFSETFD = Libc.Constants.LINUX_F_SETFD; openPtyLibrary = (Libc.OpenPtyLibrary) Native.loadLibrary("libutil", Libc.OpenPtyLibrary.class); } else if (platform == Platform.MACOS) { Libc.Constants.rTIOCSCTTY = Libc.Constants.DARWIN_TIOCSCTTY; Libc.Constants.rFDCLOEXEC = Libc.Constants.DARWIN_FD_CLOEXEC; Libc.Constants.rFGETFD = Libc.Constants.DARWIN_F_GETFD; Libc.Constants.rFSETFD = Libc.Constants.DARWIN_F_SETFD; openPtyLibrary = (Libc.OpenPtyLibrary) Native.loadLibrary(com.sun.jna.Platform.C_LIBRARY_NAME, Libc.OpenPtyLibrary.class); } else { LOG.info("not enabling process killing on nailgun exit: unknown OS %s", osName); return; } // Making ourselves a session leader with setsid disconnects us from our controlling terminal int ret = Libc.INSTANCE.setsid(); if (ret < 0) { LOG.warn("cannot enable background process killing: %s", Native.getLastError()); return; } LOG.info("enabling background process killing for buckd"); IntByReference master = new IntByReference(); IntByReference slave = new IntByReference(); if (openPtyLibrary.openpty(master, slave, Pointer.NULL, Pointer.NULL, Pointer.NULL) != 0) { throw new RuntimeException("Failed to open pty"); } // Deliberately leak the file descriptors for the lifetime of this process; NuProcess can // sometimes leak file descriptors to children, so make sure these FDs are marked close-on-exec. markFdCloseOnExec(master.getValue()); markFdCloseOnExec(slave.getValue()); // Make the pty our controlling terminal; works because we disconnected above with setsid. if (Libc.INSTANCE.ioctl(slave.getValue(), Pointer.createConstant(Libc.Constants.rTIOCSCTTY), 0) == -1) { throw new RuntimeException("Failed to set pty"); } LOG.info("enabled background process killing for buckd"); isSessionLeader = true; } public static final class DaemonBootstrap { private static final int AFTER_COMMAND_AUTO_GC_DELAY_MS = 5000; private static @Nullable DaemonKillers daemonKillers; private static AtomicReference<ScheduledFuture<?>> scheduledGC = new AtomicReference<>(); /** Single thread for running short-lived tasks outside the command context. */ private static final ScheduledExecutorService housekeepingExecutorService = Executors.newSingleThreadScheduledExecutor(); public static void main(String[] args) throws Exception { try { daemonizeIfPossible(); if (isSessionLeader) { BgProcessKiller.init(); LOG.info("initialized bg session killer"); } } catch (Throwable ex) { System.err.println(String.format("buckd: fatal error %s", ex)); System.exit(1); } if (args.length != 2) { System.err.println("Usage: buckd socketpath heartbeatTimeout"); return; } String socketPath = args[0]; int heartbeatTimeout = Integer.parseInt(args[1]); // Strip out optional local: prefix. This server only use domain sockets. if (socketPath.startsWith("local:")) { socketPath = socketPath.substring("local:".length()); } NGServer server = new NGServer( new NGListeningAddress(socketPath), NGServer.DEFAULT_SESSIONPOOLSIZE, heartbeatTimeout); daemonKillers = new DaemonKillers(housekeepingExecutorService, server, Paths.get(socketPath)); server.run(); } static DaemonKillers getDaemonKillers() { return Preconditions.checkNotNull(daemonKillers, "Daemon killers should be initialized."); } static void cancelGC() { ScheduledFuture<?> oldScheduledGC = scheduledGC.getAndSet(null); if (oldScheduledGC != null) { oldScheduledGC.cancel(false); } } static void scheduleGC() { ScheduledFuture<?> oldScheduledGC = scheduledGC.getAndSet( housekeepingExecutorService.schedule( System::gc, AFTER_COMMAND_AUTO_GC_DELAY_MS, TimeUnit.MILLISECONDS)); if (oldScheduledGC != null) { oldScheduledGC.cancel(false); } } } private static class DaemonKillers { private final NGServer server; private final IdleKiller idleKiller; private final SocketLossKiller unixDomainSocketLossKiller; DaemonKillers(ScheduledExecutorService executorService, NGServer server, Path socketPath) { this.server = server; this.idleKiller = new IdleKiller(executorService, DAEMON_SLAYER_TIMEOUT, this::killServer); this.unixDomainSocketLossKiller = Platform.detect() == Platform.WINDOWS ? null : new SocketLossKiller( executorService, socketPath.toAbsolutePath(), this::killServer); } IdleKiller.CommandExecutionScope newCommandExecutionScope() { if (unixDomainSocketLossKiller != null) { unixDomainSocketLossKiller.arm(); // Arm the socket loss killer also. } return idleKiller.newCommandExecutionScope(); } private void killServer() { server.shutdown(true); } } /** * To prevent 'buck kill' from deleting resources from underneath a 'live' buckd we hold on to the * FileLock for the entire lifetime of the process. We depend on the fact that on Linux and MacOS * Java FileLock is implemented using the same mechanism as the Python fcntl.lockf method. Should * this not be the case we'll simply have a small race between buckd start and `buck kill`. */ private static void obtainResourceFileLock() { if (resourcesFileLock != null) { return; } String resourceLockFilePath = System.getProperties().getProperty("buck.resource_lock_path"); if (resourceLockFilePath == null) { // Running from ant, no resource lock needed. return; } try { // R+W+A is equivalent to 'a+' in Python (which is how the lock file is opened in Python) // because WRITE in Java does not imply truncating the file. FileChannel fileChannel = FileChannel.open( Paths.get(resourceLockFilePath), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE); resourcesFileLock = fileChannel.tryLock(0L, Long.MAX_VALUE, true); } catch (IOException | OverlappingFileLockException e) { LOG.debug(e, "Error when attempting to acquire resources file lock."); } } /** * When running as a daemon in the NailGun server, {@link #nailMain(NGContext)} is called instead * of {@link #main(String[])} so that the given context can be used to listen for client * disconnections and interrupt command processing when they occur. */ public static void nailMain(final NGContext context) throws InterruptedException { obtainResourceFileLock(); try (IdleKiller.CommandExecutionScope ignored = DaemonBootstrap.getDaemonKillers().newCommandExecutionScope()) { DaemonBootstrap.cancelGC(); new Main(context.out, context.err, context.in) .runMainThenExit(context.getArgs(), Optional.of(context), System.nanoTime()); } finally { // Reclaim memory after a command finishes. DaemonBootstrap.scheduleGC(); } } /** Used to clean up the daemon after running integration tests that exercise it. */ @VisibleForTesting static void resetDaemon() { daemonLifecycleManager.resetDaemon(); } }