Coder Social home page Coder Social logo

ch.vorburger.exec's Introduction

ch.vorburger.exec Maven Central Javadocs

This is a small library allowing to launch external processes from Java code in the background, and conveniently correctly pipe their output e.g. into slf4j, await either their termination or specific output, etc.

If you like/use this project, Sponsoring me or a Star / Watch / Follow me on GitHub is very much appreciated!

Release Notes are in CHANGES.md.

Usage

Launching external processes from Java using the raw java.lang.ProcessBuilder API directly can be a little cumbersome. Apache Commons Exec makes it a bit easier, but lacks some convenience. This library makes it truly convenient:

ManagedProcessBuilder pb = new ManagedProcessBuilder("someExec")
    .addArgument("arg1")
    .setWorkingDirectory(new File("/tmp"))
    .getEnvironment().put("ENV_VAR", "...")
    .setDestroyOnShutdown(true)
    .addStdOut(new BufferedOutputStream(new FileOutputStream(outputFile)))
    .setConsoleBufferMaxLines(7000);  // used by startAndWaitForConsoleMessageMaxMs

ManagedProcess p = pb.build();
p.start();
p.isAlive();
p.waitForExit();
// OR: p.waitForExitMaxMsOrDestroy(5000);
// OR: p.startAndWaitForConsoleMessageMaxMs("Successfully started", 3000);
p.exitValue();
// OR: p.destroy();

// This works even while it's running, not just when it exited
String output = p.getConsole();

If you need to, you can also attach a listener to get notified when the external process ends, by using setProcessListener() on the ManagedProcessBuilder with a ManagedProcessListener that implements onProcessComplete() and onProcessFailed().

We currently internally use Apache Commons Exec by building on top, extending and wrapping it, but without exposing this in its API, so that theoretically in the future this implementation detail could be changed.

Advantages

  • automatically logs external process's STDOUT and STDERR using SLF4j out of the box (can be customized)
  • automatically logs and throws for common errors (e.g. executable not found), instead of silently ignoring like j.l.Process
  • automatically destroys external process with JVM shutdown hook (can be disabled)
  • lets you await appearance of certain messages on the console
  • lets you write tests against the expected output

History

Historically, this code was part of MariaDB4j (and this is why it's initial version was 3.0.0), but was it later split into a separate project. This was done to make it usable in separate projects (originally to launch Ansible Networking CLI commands from OpenDaylight, later to manage etcd servers in tests, both from OpenDaylight); later for use in https://enola.dev.

Similar Projects

For the exec functionality, zt-exec (with zt-process-killer) is similar (but refused to backlink us).

NuProcess is another similar library in the same space.

Related Projects

For the expect-like functionality, from https://en.wikipedia.org/wiki/Expect#Java, note (in no particular order):

Release

First test that GPG is set up correctly (gpg: no default secret key: No secret key gpg: signing failed: No secret key), and that the settings.xml has the credz for oss.sonatype.org (status: 401 unauthorized):

git checkout main
./mvnw verify -Pgpg

./mvnw deploy

Once that works, the next release can be done similarly similarly to https://github.com/vorburger/MariaDB4j#release:

git checkout main
./mvnw release:prepare
./mvnw release:perform -Pgpg
./mvnw release:clean
git push

If ./mvnw release:prepare fails with the following error, then comment out forceSignAnnotated = true under [tag] in ~/.gitconfig:

The git-tag command failed.
[ERROR] Command output:
[ERROR] error: gpg failed to sign the data
[ERROR] error: unable to sign the tag

ToDo

This library is currently used to control daemon style external executables. To launch a process which returns binary (or massive textual) output to its STDOUT (and, presumably, have that piped into a java.io.OutputStream), it would need some tweaks. This would include making the enabled-by-default logging into slf4j, and the built-in startAndWaitForConsoleMessageMaxMs which collects output, a more configurable option.

Contributions & patches more than welcome!

ch.vorburger.exec's People

Contributors

blanco27 avatar cardamon avatar dependabot-preview[bot] avatar dependabot-support avatar dependabot[bot] avatar duttonw avatar mosesn avatar vorburger avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

ch.vorburger.exec's Issues

Unexpected error when calling waitForExit

Here's the issue that we're seeing:

ManagedProcess process = builder.build();
process.start();
process.waitForExit(); // Crashes the error below:

waitForExit fails with this:

Caused by: ch.vorburger.exec.ManagedProcessException: Asked to waitFor Program 
[/tmp/75968747865555/MysqlDB4j/base/bin/mysql, -uroot, --socket=/tmp/75968747865555/MysqlDB4j.33803.sock] 
(in working directory /tmp/75968747865555/MysqlDB4j/base), but it was never even start()'ed! |
  |   | at ch.vorburger.exec.ManagedProcess.assertWaitForIsValid(ManagedProcess.java:478)
  |   | at ch.vorburger.exec.ManagedProcess.waitForExitMaxMsWithoutLog(ManagedProcess.java:439) |  
  |   | at ch.vorburger.exec.ManagedProcess.waitForExit(ManagedProcess.java:418) |  
  |   | at com.airbnb.mysql4j.DB.run(DB.java:405) |  
[...]

This is failing randomly, at a rate of say 5%. Succeeds after retries.

Based on the comment from here:

// We now must give the system a say 100ms chance to run the background

... I suspect (without being able to prove this via a test) that what's happening is this, inside startExecute()

  1. executor.execute starts execution of something that will fail
  2. this.wait(100) returns with no error, but incorrectly fails to detect that the background thread has an error
  3. isAlive = watchDog.isWatching() correctly returns False, because the background thread has indeed failed
  4. waitForExit will call assertWaitForIsValid(), which throws because isAlive = False (from 3) and hasResult = False (from 2)

Remove @SuppressWarnings("deprecation") used in commons-exec upgrade from 1.3 to 1.4.0

The build failures due to a bunch of new @Deprecated in the commons-exec upgrade from 1.3 to 1.4.0 in #179, which I would like to adopt in order to fix #9, seem to be a little peskier (PITA) to address than I initially thought.

In the interest of time, I'll add some @SuppressWarnings("deprecation") for now, in order to be able to #187 sooner rather than later.

The goal of resolving this issue will be to completely remove those @SuppressWarnings("deprecation") again.

This may be required for an eventual future commons-exec upgrade from 1.4.0 to a 1.5.0 or 2.0.0 if they ever release that (and remove their @Deprecated as part of that). Which is a lot of if... 🤣

@mosesn or gsylvie@ or @cardamon or @Blanco27 or @duttonw your contributions for this are most welcome!

THREAD_SAFETY_VIOLATION (from Sonatype Lift)

Sonatype Lift raised a THREAD_SAFETY_VIOLATION in ManagedProcess#assertWaitForIsValid() :

THREAD_SAFETY_VIOLATION: Read/Write race. Non-private method ManagedProcess.assertWaitForIsValid() indirectly reads without synchronization from this.isAlive. Potentially races with write in method ManagedProcess.startExecute().
Reporting because another access to the same memory occurs on a background thread, although this access may not.

@cardamon is this the sort of thing you would enjoy contributing a fix for?

I'm hesitant whether or not to already merge your PR #96 as-is. Thoughts?

Concurrency Bug between isAlive() and exitValue() in ManagedProcess and/or in DefaultExecuteResultHandler

enola-dev/enola#163 may be due to a Concurrency Bug between isAlive() and exitValue() ?

In ManagedProcess#waitForExitMaxMsWithoutLog() there is currently this:

if (maxWaitUntilReturning != -1) {
    resultHandler.waitFor(maxWaitUntilReturning);
    checkResult();
    if (!isAlive()) {
        return exitValue();
    }
    return INVALID_EXITVALUE;
}

My original thinking was probably that because checkResult() does resultHandler.hasResult() this should be safe? But on 2nd thought, 10 years 😄 later, I wonder:

  • Firstly, why not just check for hasResult() (or, better, add a new hasExitValue()) instead of !isAlive()
  • secondly, if there could be a concurrency issue wherein the DefaultExecuteResultHandler x3 volatile fields hasResult and exitValue and exception are not set "atomically"... that could be written better.

Also, it's probably better to introduce separate new special values for INVALID_EXITVALUE instead of using that for several things would also make debug clearer. Or, probably better, just throw a new exception, in case of such clear inconsistent states - that INVALID_EXITVALUE is more confusing that really providing any useful feedback to a caller.

I'll raise a PR with some work around this - and see if it helps me to stop runing into enola-dev/enola#163 while using https://docs.enola.dev/use/execmd.

@mosesn (from #103) and @cardamon (from #96) and @duttonw FYI.

Parse full existing command line

When using this library somewhere where there already is a "command line" (as in an executable + arguments), then the ManagedProcessBuilder as-is currently is more in the way than helpful, as you have to write code to split it:

        var execName = cmdLine.substring(0, cmdLine.indexOf(' ')).strip();
        var procBuilder = new ManagedProcessBuilder(execName);
        var execArgs = cmdLine.substring(execName.length());
        for (String arg : Splitter.on(' ').trimResults().omitEmptyStrings().split(execArgs)) {
            procBuilder.addArgument(arg);
        }

For such uses cases, it would be handy if the ManagedProcessBuilder had a static factory method such as fromCommandLine(String commandLine) which did something like above. It should take quotes appropriately into account, like a shell.

This would be the ManagedProcessBuilder time equivalent of the already existing ch.vorburger.exec.ManagedProcess.getProcLongName() method (which could be renamed to getCommandLine() at this occassion; and a toString() could be added which uses getCommandLine(), getWorkingDirectory() (as getProcLongName() does) and other state information.

Add an exec testing framework or mock

A testing mock or other suggestions for adding UT's would be useful. Possibly adding an exec mock that could allow simulating a test program and returning different error codes and console output.

Maybe adding a simple mock program or shell script to provide the different error codes and output would work.

exec hangs when program not found

Michael,

with the exec package, it seems to hang when the program to run is not found.

Debugger shows that the IOexception was not caught and things kept moving forward and the code gets stuck at a wait() in ExecuteWatchdog:ensureStarted(), line 114. So the ManagedProcessExeception was never thrown.

Can you look at [1] to see if we are calling this correctly.

Thanks, Sam

[1] https://github.com/shague/opendaylight-ansible/blob/master/southbound/impl/src/main/java/org/opendaylight/ansible/southbound/AnsibleCommandServiceImpl.java#L82

[main] INFO ch.vorburger.exec.ManagedProcess - Starting Program [ansible-runner, -j, --hosts, localhost, -p, file, run, path] (in working directory /home/shague/git/ansible/southbound/impl/.)
[Exec Default Executor] ERROR ch.vorburger.exec.LoggingExecuteResultHandler - Program [ansible-runner, -j, --hosts, localhost, -p, file, run, path] (in working directory /home/shague/git/ansible/southbound/impl/.) failed unexpectedly
org.apache.commons.exec.ExecuteException: Execution failed (Exit value: -559038737. Caused by java.io.IOException: Cannot run program "ansible-runner" (in directory "."): error=2, No such file or directory)
at org.apache.commons.exec.DefaultExecutor$1.run(DefaultExecutor.java:205)
at java.lang.Thread.run(Thread.java:748)
Caused by: java.io.IOException: Cannot run program "ansible-runner" (in directory "."): error=2, No such file or directory
at java.lang.ProcessBuilder.start(ProcessBuilder.java:1048)
at java.lang.Runtime.exec(Runtime.java:620)
at org.apache.commons.exec.launcher.Java13CommandLauncher.exec(Java13CommandLauncher.java:61)
at org.apache.commons.exec.DefaultExecutor.launch(DefaultExecutor.java:279)
at org.apache.commons.exec.DefaultExecutor.executeInternal(DefaultExecutor.java:336)
at org.apache.commons.exec.DefaultExecutor.access$200(DefaultExecutor.java:48)
at org.apache.commons.exec.DefaultExecutor$1.run(DefaultExecutor.java:200)
... 1 more
Caused by: java.io.IOException: error=2, No such file or directory
at java.lang.UNIXProcess.forkAndExec(Native Method)
at java.lang.UNIXProcess.(UNIXProcess.java:247)
at java.lang.ProcessImpl.start(ProcessImpl.java:134)
at java.lang.ProcessBuilder.start(ProcessBuilder.java:1029)
... 7 more

Release 3.1.0 ?

@duttonw was there anything else you wanted to contribute here before I release, other than #5 ? If not, I'll wip up a short summary of all recent commit in CHANGES.md (or you can?), and push current master as a 3.1.0 to Maven central...

GitHub Build STUCK

https://github.com/vorburger/ch.vorburger.exec/actions/runs/4973902463/jobs/8900036570?pr=119

is stuck "crash looping" like this since 7 minutes - it's usually just 15s:

Received 47164124 of 51358428 (91.8%), 0.1 MBs/sec
Received 47164124 of 51358428 (91.8%), 0.1 MBs/sec
Received 47164124 of 51358428 (91.8%), 0.1 MBs/sec
Received 47164124 of 51358428 (91.8%), 0.1 MBs/sec
Received 47164124 of 51358428 (91.8%), 0.1 MBs/sec
Received 47164124 of 51358428 (91.8%), 0.1 MBs/sec
Received 47164124 of 51358428 (91.8%), 0.1 MBs/sec
Received 47164124 of 51358428 (91.8%), 0.1 MBs/sec
Received 47164124 of 51358428 (91.8%), 0.1 MBs/sec
Received 47164124 of 51358428 (91.8%), 0.1 MBs/sec
Received 47164124 of 51358428 (91.8%), 0.1 MBs/sec
Received 47164124 of 51358428 (91.8%), 0.1 MBs/sec
Received 47164124 of 51358428 (91.8%), 0.1 MBs/sec
Received 47164124 of 51358428 (91.8%), 0.1 MBs/sec
Received 47164124 of 51358428 (91.8%), 0.1 MBs/sec
Received 47164124 of 51358428 (91.8%), 0.1 MBs/sec
Received 47164124 of 51358428 (91.8%), 0.1 MBs/sec

I will cancel it and relaunch it. This smells like a bug on GitHub infra.

Java 8 instead of 11

@mosesn raised (here) that he uses this library in an environment that is still on on Java 8 ("likely for the next year").

This project is currently targeting Java 11. The README does not explicitly state that (but it should); more imporantly the CI build is on Java here which de facto permits using Java 11 in contributions.

I'm personally very open to contributions to "downgrade" that back to Java 8 again, specifically:

  1. Rewrite the Java 9+ AtomicReference#compareAndExchange() introduced in #110 to Java 8
  2. Change the CI build from Java 11 to 8 (here), and add a line about it to the README
  3. Raise a PR with 2 commits for 1. & 2. and see if that passes - fix additional problems, if any

Would someone reading this like to make such a contribution? @mosesn perhaps?

Performance issue

The wait(100) in ch.vorburger.exec.ManagedProcess.startExecute() isn't great.

Wouldn't some sort of yield() (:question:) suffice?

checkResult() should be optional

The waitForExit() methods currently internally (all?) use checkResult().

This makes it impossible to wait for a non-sucessful exit - which could be handy for some use cases (e.g. a CLI testing tool).

Enforce Code Format with Checkstyle and/or Spotless and/or Google Java Format

Given that this project is starting to get PR contributions, it would IMHO be good to enforce a good format.

I'm already working on a PR with Checkstyle - which I'm hoping is quick - because I'll just copy/paste this and the respective lines from the MariaDB4j pom.xml .

This ch.vorburger.exec code historically actually originates from MariaDB4j and used to be formatted like that, so I expect that it should need relatively few changes only.

In #126 @mosesn has raised possibly using https://github.com/diffplug/spotless as a possible alternative. I am personally less motivated for doing the work for that, just because it would take me longer to figure out how to set up. (But years ago I actually was a Google Summer of Code GSoC mentor advising a student to add Spotless to https://github.com/apache/fineract/ and it seemed great, so I have no strong objections to replace Checkstlye with Spotless - if someone else wants to put in the work for it!) I don't suppose Spotless can use a Checkstyle configuration, can it?

An alternative could be using https://github.com/google/google-java-format. I have adopted that in my latest project, for https://github.com/enola-dev/enola, see https://docs.enola.dev, and quite like it too. I haven't looked much into the available Maven plugins, although looking at https://github.com/google/google-java-format#third-party-integrations it seems like Spotless may actually use Google Java Format internally? I'm OK if someone wants to raise a PR for that.

BTW on this sort of things, for Enola.dev I've also had loads of fun with a .pre-commit-config.yaml, and loads of related configuration files like .editorconfig and .prettierrc.yaml and .clang-format and .markdownlint.yaml and .protolint.yaml, but I think for this ch.vorburger.exec project I want to keep the "barrier to entry" lower by sticking to a pure Java and fully Maven integrated approach, only.

Thoughts - anyone?

MultiCauseIOException fails on errorprone

On master

[INFO] -------------------------------------------------------------
[WARNING] COMPILATION WARNING :
[INFO] -------------------------------------------------------------
[WARNING] /Users/moses/projects/ch.vorburger.exec/src/main/java/ch/vorburger/exec/MultiCauseIOException.java:[41,33] non-transient instance field of a serializable class declared with a non-serializable type
[INFO] 1 warning
[INFO] -------------------------------------------------------------
[INFO] -------------------------------------------------------------
[ERROR] COMPILATION ERROR :
[INFO] -------------------------------------------------------------
[ERROR] /Users/moses/projects/ch.vorburger.exec/src/main/java/ch/vorburger/exec/MultiCauseIOException.java: warnings found and -Werror specified
[INFO] 1 error
[INFO] -------------------------------------------------------------
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------

startWaitGetOutput()

ManagedProcess ideally should have something like a startWaitGetOutput() which would:

p.start();
if (p.waitForExit() != 0) {
    throw fail(p.getProcLongName() + " did not return 0");
}
return p.getConsole();

Ideally it perhaps shouldn't just return the String from getConsole() (which is misnamed..), but an object allowing separate access to the captured STDOUT & STDERR (and combined?). With and without ANSI colour codes, while we're dreaming. 😄

Do not log exception when failure (non-0) exit value is expected

As a follow-up to the work in #108, and purely for "logging cosmetics" (without further functional/concurrency changes), it would be nice if e.g. when used for https://docs.enola.dev/use/execmd/ it would not log this confusing internal exception anymore in cases where this is actually completely expected, when a client of the library set the new SuccessExitValueChecker introduced in #115:

./enola execmd -i /home/vorburger/git/github.com/vorburger/enola/docs/use/help/index.md ...
2023-05-13 15:37:35 INFO ch.vorburger.exec.ManagedProcess startPreparation Starting Program [/usr/bin/env, bash, -c, cd .././.././.. && ./enola] (in working directory /home/vorburger/git/github.com/vorburger/enola/docs/use/help)
2023-05-13 15:37:35 INFO ch.vorburger.exec.ManagedProcess waitForExitMaxMs Thread is now going to wait max. 7000ms for process to terminate itself: Program [/usr/bin/env, bash, -c, cd .././.././.. && ./enola] (in working directory /home/vorburger/git/github.com/vorburger/enola/docs/use/help)
2023-05-13 15:37:36 SEVERE ch.vorburger.exec.SLF4jLogOutputStream processLine env: Missing required subcommand
2023-05-13 15:37:36 SEVERE ch.vorburger.exec.SLF4jLogOutputStream processLine env: Usage: enola [-hVv] COMMAND
2023-05-13 15:37:36 SEVERE ch.vorburger.exec.SLF4jLogOutputStream processLine env: https://enola.dev
2023-05-13 15:37:36 SEVERE ch.vorburger.exec.SLF4jLogOutputStream processLine env:   -h, --help      Show this help message and exit.
2023-05-13 15:37:36 SEVERE ch.vorburger.exec.SLF4jLogOutputStream processLine env:   -v, --verbose   Specify multiple -v options to increase verbosity. For
2023-05-13 15:37:36 SEVERE ch.vorburger.exec.SLF4jLogOutputStream processLine env:                     example, `-v -v -v` or `-vvv`
2023-05-13 15:37:36 SEVERE ch.vorburger.exec.SLF4jLogOutputStream processLine env:   -V, --version   Print version information and exit.
2023-05-13 15:37:36 SEVERE ch.vorburger.exec.SLF4jLogOutputStream processLine env: Commands:
2023-05-13 15:37:36 SEVERE ch.vorburger.exec.SLF4jLogOutputStream processLine env:   help                 Display help information about the specified command.
2023-05-13 15:37:36 SEVERE ch.vorburger.exec.SLF4jLogOutputStream processLine env:   generate-completion  Generate bash/zsh completion script for enola.
2023-05-13 15:37:36 SEVERE ch.vorburger.exec.SLF4jLogOutputStream processLine env:   docgen               Generate Markdown Documentation
2023-05-13 15:37:36 SEVERE ch.vorburger.exec.SLF4jLogOutputStream processLine env:   list-kinds           List known Entity Kinds
2023-05-13 15:37:36 SEVERE ch.vorburger.exec.SLF4jLogOutputStream processLine env:   list                 List Entities
2023-05-13 15:37:36 SEVERE ch.vorburger.exec.SLF4jLogOutputStream processLine env:   get                  Get Entity
2023-05-13 15:37:36 SEVERE ch.vorburger.exec.SLF4jLogOutputStream processLine env:   rosetta              Transform YAML <=> JSON <=> TextProto
2023-05-13 15:37:36 SEVERE ch.vorburger.exec.SLF4jLogOutputStream processLine env:   server               Start HTTP Server
2023-05-13 15:37:36 SEVERE ch.vorburger.exec.SLF4jLogOutputStream processLine env:   execmd               Execute Commands in Markdown
2023-05-13 15:37:36 SEVERE ch.vorburger.exec.LoggingExecuteResultHandler onProcessFailed Program [/usr/bin/env, bash, -c, cd .././.././.. && ./enola] (in working directory /home/vorburger/git/github.com/vorburger/enola/docs/use/help) failed unexpectedly
org.apache.commons.exec.ExecuteException: Process exited with an error: 1 (Exit value: 1)
        at org.apache.commons.exec.DefaultExecutor.executeInternal(DefaultExecutor.java:404)
        at org.apache.commons.exec.DefaultExecutor.access$200(DefaultExecutor.java:48)
        at org.apache.commons.exec.DefaultExecutor$1.run(DefaultExecutor.java:200)
        at java.base/java.lang.Thread.run(Thread.java:829)

2023-05-13 15:37:36 SEVERE ch.vorburger.exec.ManagedProcess checkResult Program [/usr/bin/env, bash, -c, cd .././.././.. && ./enola] (in working directory /home/vorburger/git/github.com/vorburger/enola/docs/use/help) failed

Noe that above there are technically 2 separate log messages to look into surpressing here - both SEVERE, one from the LoggingExecuteResultHandler and the other one from the ManagedProcess.

This change is not really relevant e.g. for MariaDB4j (because it expectes the DB binaries it launches to always return 0), but only e.g. for Enola (because it supports exec with expected non-zero exit value in Executable Markdown).

Replace JDT @Nullable annotation with JSpecify

I need to use this library in an environment where the JDT @Nullable annotation (AKA org.eclipse.jdt:org.eclipse.jdt.annotation) is not available, or could only be made available if I took on additional maintenance burden.

That environment recommends to use https://jspecify.dev for modern "Type-use" null annotations. I will therefore switch this project to that.

@cpovirk FYI

Partial output for Git command

I need to run a bunch of git clone commands and I'm using your library to make things easier to manage.
I also need to show the progress of the clone operations, but when I check the process output with cmd.getConsole() I only get this line: Cloning into path...

It's missing lines:
remote: Enumerating objects:...
Receiving objects:...

Can you look into it please?

separate core functionality from default logging

For a new personal project I'm toying with, I'd actually prefer it now if this great old library of mine would separate its core functionality from the currently built-in and on by default logging it does.

If I find the time for it, I therefore may split exec into exec-core (which would have no dependency on slf4j, or any other logging framework) and exec-slf4j some day.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.